Skip to content

feat(pcb): add atomic schematic-to-board update - #215

Merged
mixelpixx merged 6 commits into
mixelpixx:mainfrom
neusse:codex/update-pcb-from-schematic
Aug 15, 2026
Merged

feat(pcb): add atomic schematic-to-board update#215
mixelpixx merged 6 commits into
mixelpixx:mainfrom
neusse:codex/update-pcb-from-schematic

Conversation

@neusse

@neusse neusse commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #187.

Adds update_pcb_from_schematic to the sch_export toolset as the accepted live-KiCad, plan/apply workflow:

  • exports the saved hierarchy with kicad-cli sch export netlist --format kicadsexpr
  • returns ready, noop, or conflict for dry runs and requires expected_plan_revision for apply
  • preserves board-owned footprint identity, placement, side, rotation, lock state, and artwork
  • stages resolvable missing footprints deterministically outside the reviewed board bounds
  • preserves and reports board-only footprints; rejects footprint swaps, ambiguous matches, invalid exports, unsaved hierarchy, and routed-net reassignment
  • applies all mutations through attempt_ipc_write in one run_commit, with no file fallback and one KiCad undo entry
  • reports paired planned/applied coverage plus structured diagnostics

Design and compatibility

The planner is a pure module with focused tests. Export, hierarchy validation, footprint resolution, plan construction, revision validation, and routing preflight all happen before the commit. Only IPC mutations run inside the single commit, and every per-item result propagates with ? so any failure drops the whole batch.

This adds one tool without changing existing tool inputs or behavior. Registry/documentation totals move to 189 registered tools plus 6 meta tools (195 total). README, DEV guide, tool directory, troubleshooting, and the bundled KiCad PCB skill are updated together.

Verification

  • cargo test --workspace --locked --lib --tests
  • cargo test --workspace --locked --doc
  • cargo clippy --workspace --locked --all-targets -- -D warnings
  • cargo fmt --all -- --check
  • live KiCad 10.0.5 hierarchical round trip on a disposable project:
    cargo test -p konnect --locked --test live_kicad_tools schematic_sync_apply_then_dry_run_is_noop -- --ignored --exact --nocapture

The live test verified dry-run ready -> apply applied -> dry-run noop against an open board and saved hierarchical schematic.

Risk and rollback

The tool is deliberately live-IPC-only. A missing/rejected IPC connection returns a conflict instead of touching the board file. Routed-net changes and unevaluable inputs fail closed before begin_commit; apply mutations share one KiCad commit, so failures drop the batch and Ctrl-Z reverses a successful sync as one action.

@neusse
neusse marked this pull request as ready for review August 15, 2026 17:22
Merge of main plus one blocking fix found by running the PR's own live
acceptance test against KiCAD 10.0.5.

plan_revision hashed the raw netlist source, and `kicad-cli sch export
netlist` stamps (date "...T14:48:16") and its own version into every
export. The revision therefore changed EVERY SECOND for a design nobody
touched, and since apply requires the revision a dry run returned, apply
could only succeed when both calls landed inside the same wall-clock
second. Two consecutive dry runs on an unchanged design produced
identical plans (68 additions each) and different revisions; the live
test failed with stale_plan_revision 1.2s after its own dry run.

The revision now covers what the plan reads -- components and nets --
with the volatile export header excluded. Nested (date ...) inside
component fields is design content and still counts. A KiCad upgrade no
longer invalidates outstanding plans either.

Verified against KiCAD 10.0.5 on a disposable copy of the
complex_hierarchy demo with its 68 board footprints stripped: dry run
`ready` (68 planned) -> apply `applied` -> dry run `noop`, the PR's own
acceptance criterion, which failed before this fix and passes after.
Independently confirmed by asking KiCad to save its own live state:
the board file went 0 -> 68 footprints across 68 distinct references,
and kicad-cli DRC parses the result with a live ratsnest.

Counts: sch_export 6 -> 7, 196 registered + 6 meta = 202.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mixelpixx

Copy link
Copy Markdown
Owner

Reviewed against the contract, and this implements it faithfully — I checked each ruling in the code rather than against the description:

  • Atomicityattempt_ipc_write → planning → revision check → then run_commit wrapping only create_items_in/update_items_in, both with ?. Nothing that can fail without mutating happens inside the commit. BoardWrite::File returns a conflict rather than falling back, which was the rule I said should hold permanently rather than "in v1".
  • Never swallow a per-item status — I checked the new _in variants specifically, since that was the failure mode I was most worried about. update_items_in carries the ItemStatusCode check and additionally bails on a missing status, which is stricter than the old path.
  • Conflict policyfootprint_id_changed, on_board_exclusion_conflict, routed_pad_net_change, reference_identity_conflict, reference_only_rename_ambiguous, reference_rename_collision, the duplicate_* family. DNP is compared and carried onto the board; in_bom is absent from the module entirely, which is right. No force flag, and dry_run defaults to true.
  • Success semanticsstatus/coverage/diagnostics in fix(review): fail closed on incomplete coverage #185's shape, paired planned/applied counts, no partial-success state.

Then I ran your own acceptance test against KiCAD 10.0.5, and it failed.

The blocker: the plan revision changed every second

plan_revision hashed the raw netlist source, and kicad-cli sch export netlist stamps the export time into every file:

5c5
<     (date "2026-08-15T14:48:16")
---
>     (date "2026-08-15T14:48:18")

So the revision moved on the clock, not on the design. Two consecutive dry runs on an untouched design produced identical plans — 68 additions each — and different revisions. Since apply requires the revision a dry run returned, apply could only ever succeed if both calls landed inside the same wall-clock second. Your live test failed for me at status: conflict / stale_plan_revision 1.2 s after its own dry run.

That is why it passed for you and failed for me: it is a race, and the losing case is the one that matters — a human reading the plan before applying it, which is the entire point of having a plan.

Fixed on your branch (a3c0233): the revision now covers what the plan reads — components and nets — with the volatile export header excluded. A nested (date …) inside a component's fields is design content and still counts. The tool version is excluded for the same reason: a KiCad upgrade shouldn't invalidate every outstanding plan for an unchanged design. New unit test asserts all four cases, including that a footprint swap still moves the revision.

Verification after the fix

Disposable copy of KiCad's complex_hierarchy demo with its 68 board footprints stripped, schematic untouched:

dry run  -> ready,   footprints_added {planned: 68, applied: 0}
apply    -> applied, footprints_added {planned: 68, applied: 68}
dry run  -> noop

Your acceptance criterion, passing. Then, independently of the tool under test, I asked KiCad to save its own live state: the board file went 0 → 68 footprints across 68 distinct references, and kicad-cli pcb drc parses the result with a live ratsnest (164 unconnected items, i.e. the pads carry nets).

Two notes, neither blocking

Excellent work, and thank you for holding implementation until the contract was settled — it made this reviewable in a way a 1,800-line PR usually isn't. Merging once CI confirms.

@neusse

neusse commented Aug 15, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up fix pushed in 1bb5a62 after auditing the timestamp correction.

The previous find("(date ") / find("(tool ") scanner removed every matching substring, including nested component nodes and tool-like text inside quoted values. That contradicted the intended header-only behavior and could make genuinely different design content share a plan revision.

The revision identity now structurally encodes only the parsed top-level components and nets trees. Export timestamp/tool metadata remains excluded, while all nested nodes and quoted content remain revision-sensitive. A regression test covers both cases; it failed against the scanner and passes with the structural encoding.

Local verification:

  • cargo test --workspace --locked --lib --tests
  • cargo test --workspace --locked --doc
  • cargo clippy --workspace --locked --all-targets -- -D warnings
  • cargo fmt --all -- --check

@mixelpixx

Copy link
Copy Markdown
Owner

Took your structural hash over my string-stripping — yours is right and mine had a real hole: find("(date ") isn't string-aware, so a component field whose value contained that literal would have been mangled. Hashing the parsed components and nets trees can't have that problem. Merged, and my revision-stability test now runs against your implementation and passes, which is a nice cross-check: my test, your code.

Re-verified live against KiCAD 10.0.5 on a fresh fixture (a second disposable copy of complex_hierarchy with its 68 footprints stripped): dry run ready → apply applied → dry run noop, and KiCad saving its own state takes the board file 0 → 68 footprints.

Also rebased the counts — main moved again while this was in review (#170 added the sch_bus toolset, #199 landed), so it's 200 registered + 6 meta = 206 across 19 toolsets now.

@mixelpixx
mixelpixx merged commit e8fbadf into mixelpixx:main Aug 15, 2026
8 checks passed
mixelpixx added a commit that referenced this pull request Aug 15, 2026
A minor: new tools, a new toolset, and update_pcb_from_schematic.

Schematic-to-board sync (#187/#215) lands the tool the PCB skill had been
instructing for months without it existing. Plan/apply with an explicit
revision, one KiCad undo entry per apply.

Three data-corruption fixes: (paper "User" W H) was being destroyed on every
typed-model write, making the sheet unloadable (#208); netclasses were written
into the board file, which KiCad then rejected (#190); and zones landed on
net 0 on KiCad 10 boards (#192).

Behaviour changes: netclasses and design rules write .kicad_pro; zones refuse
an undeclared net and refuse entirely while KiCad holds the board; add_zone no
longer returns net_id; annotations upsert rather than append; #PWR numbers are
reused after deletion; place_component fails on a footprint with no reference
field; the plugin stops its HTTP server on a clean KiCAD exit.

19 toolsets, 200 tools.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Add atomic update_pcb_from_schematic workflow

2 participants