app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Reference implementation of the KiCad bridge: multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.
← Commit history
0.9.340: routing tests, demo runner + evidence (arav-rog 2026-09-07), vendoring and overlay tools, release notes
19 files changed
+5898−6
README.md+9RELEASE_NOTES.md+277SKILL.md+51demo/routing/README.md+87demo/routing/ab-dev-pin-fix.patch+470demo/routing/evidence/AdomLapper-2026-09-05.json+60demo/routing/evidence/arav-rog-2026-09-07.json+56demo/routing/evidence/per-net-results.json+2095demo/routing/evidence/per-trace-results.json+2154demo/routing/make_fixture.py+42demo/routing/run_demo.py+79tests/test_ipc_api.py+30tests/test_live_routing.py+109tests/test_routing_ipc.py+151tools/build_release.py+29−4tools/build_routing_dev.py+17tools/install_routing_dev.py+92tools/release_files.json+39−2tools/vendor_routing_deps.py+51README.md+9@@ -44,6 +44,15 @@ explains it. The same recording plays inline in the dashboard. How the dock card, the dashboard and this demo fit together: [docs/dashboard.md](docs/dashboard.md). +## Live routing (from 0.9.340)++Four verbs edit copper in the open PCB editor through KiCad's official IPC API (KiCad 10.0.1++with the IPC API server enabled): `kicad_routing_state`, `kicad_route_net`, `kicad_remove_route`+and `kicad_routing_validate`. The AI chooses the waypoints; each call lands as one native Undo step,+carries a revision guard, and is DRC-checked against a snapshot before it commits. This is not an+autorouter and not an electrical sign-off. Details and the regression demo: [SKILL.md](SKILL.md#live-routing-from-09340)+and [demo/routing/README.md](demo/routing/README.md).+ ## The demo verb — `kicad_demo` One verb that shows the whole bridge off, built for the AI that demos Adom Bridge
RELEASE_NOTES.md+277@@ -10,6 +10,283 @@ unnamed. These entries were reconstructed after the fact from the work they contain, which is exactly the situation the mandatory-notes gate now prevents. Treat them as accurate but secondhand. +## 0.9.340 (2026-09-07 16:56 UTC)++Live routing verbs through KiCad's IPC API (routing_state, route_net, remove_route, routing_validate), offline copper verbs (board_pads, add_track, add_via, route), kicad_ipc_api, KiCad 10 name-based nets, vendored routing deps++## 0.9.340 (2026-09-07): live routing through KiCad's IPC API++- New live copper verbs from the per-net routing branch: `kicad_routing_state`, `kicad_route_net`,+ `kicad_remove_route`, `kicad_routing_validate`. Official KiCad IPC editing (KiCad 10.0.1+ with the+ IPC API server on), native Undo per call, revision and net guards, snapshot DRC before commit.+- Offline file verbs for closed boards: `kicad_board_pads`, `kicad_add_track`, `kicad_add_via`, `kicad_route`.+- `kicad_ipc_api`: read or flip KiCad's IPC API server switch (needs a KiCad restart).+- The board parser reads KiCad 10 name-based nets as well as numbered nets.+- The release zip now vendors the IPC client (`routing_deps/`: kicad-python 0.8.0, protobuf, pynng,+ cffi; Windows wheels for CPython 3.11 to 3.13). Nothing is pip-installed on the desktop.+- Not an autorouter and not an electrical sign-off: the caller chooses every waypoint.+- Tour work from 0.9.339 (two-part beats, 98% layout, 3D bodies) is unchanged.++## 0.9.339 (2026-09-05 19:16 UTC)++Tour motion: zoom depth per beat (one step for a single part, two plus one for a sheet or board)++## 0.9.338 (2026-09-05 18:55 UTC)++Tour: reopen the board when the file on disk is newer than the running pcbnew, so attached 3D bodies actually show++## 0.9.337 (2026-09-05 18:50 UTC)++Tour: a 3D model file that is not installed on this box counts as dead too; the MOSFETs then get KiCad's TDSON-8-1 body and the board reloads++## 0.9.336 (2026-09-05 18:46 UTC)++Tour: dead 3D model references are removed as balanced blocks (the regex missed the MOSFETs), so KiCad's TDSON-8-1 body attaches and the board reloads++## 0.9.335 (2026-09-05 18:43 UTC)++Tour: 3D body attach report lists what it saw for the MOSFET footprints (debug)++## 0.9.334 (2026-09-05 18:39 UTC)++Tour: two-part beats (A loads with its own line, B explains and moves once verified), part timestamps on the record, 3D body attach report says where it looked++## 0.9.333 (2026-09-05 18:36 UTC)++Tour: every beat in two parts (A loads the window with its own line, B starts once verified with the explaining line and the motion); part timestamps on the record; dead 3D bodies replaced by KiCad standard ones++## 0.9.332 (2026-09-05 18:29 UTC)++Footprint open: a hidden Library Tree filter box counts as missing (wx hides the pane, the control survives), so the pane is shown and waited for; tour prep summary (3D bodies attached, cache restart) on the progress record++## 0.9.331 (2026-09-05 18:22 UTC)++Footprint open: the plugin-free background loader also shows a hidden Library Tree pane before typing (arav-rog: the pane fix only covered the fallback path); tour reloads the board after attaching 3D bodies; prep summary on the record++## 0.9.330 (2026-09-05 18:11 UTC)++Tour: attach KiCad's standard 3D bodies to footprints the board left without one (six MOSFETs get TDSON-8-1); Footprint Editor Library Tree pane is shown once and waited for, never toggled back off++## 0.9.329 (2026-09-05 17:59 UTC)++Tour: restart KiCad during prepare when the footprint file is newer than the running KiCad (process start time vs file mtime), not only when it is new; startedAt and narrationAt on each step record++## 0.9.328 (2026-09-05 17:52 UTC)++Tour: footprint install leaves an identical file untouched (mtime bump made KiCad's cache look stale on every tour after the first); per-beat startedAt and narrationAt on the record for syncing a recording++## 0.9.327 (2026-09-05 17:32 UTC)++Tour: editor and viewer windows at 98 percent of the work area, centered (John)++## 0.9.326 (2026-09-05 17:21 UTC)++Tour: all editor and viewer windows at 95 percent of the work area, centered; 2D motion aims at the featured part (position parsed from the board and schematic) and checks the canvas after each gesture; 3D motion re-frames when the board is clipped or tiny++## 0.9.325 (2026-09-05 13:35 UTC)++Footprint open: when the Footprint Editor's Library Tree pane is hidden, show it through the frame's own View menu (plugin wm_command) before navigating; arav-rog had it hidden and every load failed++## 0.9.324 (2026-09-05 13:21 UTC)++Footprint open: never restart KiCad for a stale cache when the editor never opened (plugin_not_running restarted KiCad twice on arav-rog); tour footprint beat ensures pcbnew is open and its plugin alive; window rows carry minimized/image so 'in front' skips minimized windows++## 0.9.323 (2026-09-05 13:08 UTC)++Tour: a brand-new footprint restarts KiCad during prepare (before any window), never mid-beat; the footprint beat waits retryAfterMs on kicad_restarted_retry++## 0.9.322 (2026-09-05 13:07 UTC)++Tour: a brand-new footprint restarts KiCad during prepare (before any window), not mid-beat; install_footprint reports existedBefore; the footprint beat waits retryAfterMs on kicad_restarted_retry++## 0.9.321 (2026-09-05 13:03 UTC)++Tour: 'in front' ignores shell popups, minimized windows and our own overlays (arav-rog measured RustDesk minimized and PopupHost as the top window); footprint beat retries twice without the plugin wait; 2D motion recenters when a pan or zoom empties the canvas++## 0.9.320 (2026-09-05 12:50 UTC)++Tour: leave KiCad's main window as KiCad opens it; footprint beat waits for the project manager plugin and retries (cold KiCad on arav-rog failed both footprint beats)++## 0.9.319 (2026-09-05 12:47 UTC)++Tour: read the unwrapped desktop_list_windows payload (verify and foreground worked off a local list with no z-order on arav-rog); beat captions bottom-left clear of the demo panel; small top captions announce each foreground and the acks++## 0.9.318 (2026-09-04 23:13 UTC)++Wiki #48: the 20 s idle healer measured window reachability against the primary monitor only and pulled windows on a second monitor back to the primary; it now measures against the monitor the window overlaps++## 0.9.317 (2026-09-04 17:05 UTC)++Tour remote: dashboard presses recorded, acked with a caption, and honoured at the next gesture like panel presses++## 0.9.316 (2026-09-04 17:02 UTC)++Tour remote: FIFO press queue (no press lost), poll-drained presses recorded, presses on progress++## 0.9.315 (2026-09-04 16:53 UTC)++Tour remote: prev/next/pause cut the beat in flight at the next gesture; back goes to the beat before the one on screen; presses and acks recorded++## 0.9.314 (2026-09-04 16:25 UTC)++Tour layout: restore a window's bounds before the tour closes it so KiCad's sticky geometry is never corrupted++## 0.9.313 (2026-09-04 16:21 UTC)++Tour layout: read the unwrapped desktop_list_monitors payload so windows are placed in their 80 percent slots++## 0.9.312 (2026-09-04 16:20 UTC)++kicad_demo layoutProbe diagnostic; layout uses the monitor work area++## 0.9.311 (2026-09-04 16:14 UTC)++Fix: demo.py lacked the json import used by the new window-bounds save/restore (0.9.310 would have silently skipped saving bounds).++## 0.9.310 (2026-09-04 16:13 UTC)++Tour layout for the recording (John): each beat's window is placed at 80% of the primary screen in its slot (symbol upper-left, footprint nudged, chip 3D upper-right, schematic lower-left nudged, board 2D lower-left, board 3D lower-right) and the KiCad manager is centered at its own size; every bound is saved first and restored at the tour's end, stop, failure, or the next start (leftovers file), so KiCad's sticky sizes end up as the user had them. Every caption's reply is recorded on progress; a press on the remote gets an immediate acknowledgement caption at the top of the screen; the settle check waits up to 8 s for two identical captures instead of sampling once.++## 0.9.309 (2026-09-04 15:59 UTC)++Tour 3D beats no longer end with Zoom to Fit: in KiCad's 3D viewer that command also resets the orientation, so the board beat ended top-down on John's laptop; the beat now ends on the isometric it built (zoom in/out net to zero, framed at the opening fit).++## 0.9.308 (2026-09-04 15:59 UTC)++Motion gate: GetForegroundWindow can lag AD's z-order right after a raise, which aborted the symbol beat's motion at its first gesture on John's laptop while AD had already measured the window on top; the gate now waits up to 1.5 s for the OS to agree. Panel record keeps the callback URL across stage-advance shows.++## 0.9.307 (2026-09-04 15:52 UTC)++Tour verification (John: 'tool your bridge code to definitively know that the correct thing is showing'): every beat now MEASURES, after its raise, that its own window is the top of AD's z-order, that the live title carries the expected document, that the GL canvas is rendered (kicad_ui canvas probe) and that two captures 0.8 s apart are identical (settled). The result rides on the step and on progress as ; motion runs only on a verified window, otherwise the step says exactly which fact failed and which window was actually on top. Zoom discipline: 2D beats start and end on extents with two wheel clicks in and back out; 3D beats zoom one menu step in and out and end on Zoom to Fit while staying isometric.++## 0.9.306 (2026-09-04 15:46 UTC)++Tour 3D motion is now deterministic: KiCad's own View menu commands (Rotate X/Z by 10 degrees, Zoom In/Out, Zoom to Fit) resolved from the live menu bar and posted as WM_COMMAND, so the isometric and the orbit are the same on every DPI and need no mouse or foreground; mouse drags remain only as the fallback (they flipped the chip viewer and did nothing in the board viewer on 2026-09-03). Each step in the result now carries the window handle and title it drove.++## 0.9.305 (2026-09-04 15:38 UTC)++Tour window targeting (John's laptop: fourteen KiCad windows from days of demos; the 2D beat showed an empty untitled PCB Editor and the 3D beat the old sample cube): every beat now targets the tour's own document titles (esc-g431 schematic and board, the Adom STM32 symbol, the LQFP footprint), reuses an existing window instead of opening a duplicate, and opens each 3D view from that exact editor (open_3d_viewer accepts pcbHwnd/fpHwnd). The clean slate at start also closes old adom-demo documents and an empty untitled PCB Editor.++## 0.9.304 (2026-09-04 15:29 UTC)++Tour identity fix (John's dashboard run, 2026-09-03): ab refused the demo panel, every caption, every raise and every window close from the tour thread with 'carries no caller identity' (toast fallback, no captions, nothing in front). Every loopback call the bridge makes now carries identity: the X-Adom-Caller headers of the request being served, or the bridge's own self caller otherwise; the tour thread and ab's callback thread adopt the identity of the request that started the tour. Also: a failed panel show no longer demotes later tours to the toast (per-tour reset, one retry), and every 3D viewer plus the Footprint Editor are closed before the beats so the footprint beat opens a fresh, re-indexed editor.++## 0.9.303 (2026-09-04 15:15 UTC)++Tour sync: the verb-side control channel (kicad_demo {control}) is cleared at tour start too, so a pause/next/exit queued against a previous tour never steers a new one; completes the stale-press drain of 0.9.302.++## 0.9.302 (2026-09-04 15:13 UTC)++Tour sync fixes (John's run on 0.9.301): stale panel presses from the previous show are drained before a tour starts (his earlier NEXT presses made the new tour jump to step 3 at once); the narration mute is reset from the call's muted flag at thread start and pushed to the panel on the first show; STOP from the remote sets stopping on progress immediately so the dashboard can close its own pill; progress carries stopping.++## 0.9.301 (2026-09-04 15:07 UTC)++Taskbar hygiene (John: frozen progress bars on every KiCad window, no Adom badge): the boot sweep that clears leftover bars ran once at +6 s while ab was still restarting and failed silently; it now retries across the first two minutes until a window answers, and a five-minute idle reaper clears progress and badge on every KiCad window whenever the bridge is driving nothing. Filed adom/adom-bridge#138 for a progress TTL on ab's side.++## 0.9.300 (2026-09-04 15:01 UTC)++Tour 3D motion recalibrated: the orbit drags totalled about a third of the viewer width and turned the board nearly edge-on (KiCad's trackball is roughly 0.1 degree per pixel). Now ~30 degrees of tilt and ~40 of yaw for the isometric, then three gentle orbits with zoom, slower and smoother for the recording.++## 0.9.299 (2026-09-04 14:54 UTC)++Tour board-in-3D fix: KiCad's 3D viewers are unowned top-level windows, so picking 'the viewer owned by the PCB Editor' never matched and the chip's viewer was never closed (0.9.298 test-box run: board3d failed with the viewer actually open). The beats now take the viewer that APPEARS after their open, the chip's viewer is remembered and closed by handle before the board's opens, with the topmost viewer as fallback.++## 0.9.298 (2026-09-04 14:51 UTC)++Fix: the 0.9.297 tour job died on a NameError (_EXIT undefined) before its first progress update, so Play the demo showed the dashboard's transport pill and nothing else (John, lost, 2026-09-03). _EXIT is defined and the job's reset sits inside its try so any early failure lands on progress as an error instead of a silent stall.++## 0.9.297 (2026-09-04 14:08 UTC)++Tour: motion on every beat for the recording (fit, wheel zoom in/out, middle-drag pans in the 2D editors; Home, an isometric left-drag and three orbits with zoom in the 3D viewers), sent only while the beat's window is confirmed in front and aborted at the next gesture on STOP or when the user switches away. The board-in-3D beat now closes the chip's 3D viewer first and picks the viewer owned by the PCB Editor (the last beat was showing the chip's viewer by title match). STOP from the remote's X now clears the caption and ends the tour within the beat, audio already off. Per-beat motion reports ride on the result.++## 0.9.296 (2026-09-04 14:02 UTC)++Tour foreground fix (John: 'why do I not see you foregrounding any kicad window'): the beat raise called SetForegroundWindow from the bridge process and reported true regardless; Windows refuses that from a background process while the user works elsewhere. The bridge now lifts its no-activate mark, asks ab's desktop_bring_to_front to raise the beat's window once, and reports foregrounded only when the z-order afterwards shows that window on top.++## 0.9.295 (2026-09-04 13:55 UTC)++The tour now walks a real Adom board, adom/esc-g431 (149 footprints, 227 symbols): its STM32G431 symbol and LQFP-48 footprint are lifted from the wiki files at tour time and installed into the Adom library, then symbol, footprint, chip in 3D, schematic, board 2D, board 3D in that order. Captions were never painting because ab requires reason on desktop_caption; fixed, medium size, one line per beat. Narration re-rendered to match. Wording no longer claims the background: each beat's window is brought to the user once. Dead local 3D model paths in the board are dropped.++## 0.9.294 (2026-09-04 10:37 UTC)++Demo panel callback URL fix: the bridge read its bound port from a second copy of the server module and sent no eventCallbackUrl to ab (panel presses reached the tour only via poll). The port now travels through the environment, so every press is POSTed to the bridge the instant it is clicked. Verified on the test box: a press to the loopback route paused within the beat, muted at once, and resumed on play.++## 0.9.293 (2026-09-04 10:34 UTC)++kicad_demo progress now reports panel (last show reply, stage, callback URL), muted, muteSource, lastPress and paused; each step in the final result carries foregrounded (whether the beat's window was brought forward once).++## 0.9.292 (2026-09-04 10:30 UTC)++Demo panel: reason is sent on every show (ab marks it required on show; a stage-advance show without it was refused live and the remote stayed at 1/7), and every show's reply is recorded on kicad_demo progress (panel: shown, stage, callbackUrl, error) so a refused show is visible instead of silent.++## 0.9.291 (2026-09-04 10:28 UTC)++kicad_demo adopts the ab 2.1.17 demo-panel contract: the first show passes eventCallbackUrl (loopback POST /demo/event on the bridge's own port) so a press lands the instant it is clicked; mute/unmute silence or replay the beat's narration at once; pause/stop silence at once and apply at the beat boundary; stage-advance shows send only the stage so a user's pause or mute is never undone; the poll stays as backstop and skips presses already delivered. The tour now foregrounds each beat's window exactly once (John: the user must see which window is being demoed, but keeps full control between beats); no persistent focus guardian during the tour. One short ab caption per beat stays.++## 0.9.290 (2026-09-04 09:32 UTC)++kicad_demo drives ab's desktop_demo_panel (Adom Bridge 2.1.0+, wiki adom/adom-bridge#116): the floaty play/pause/next/back/stop remote is shown at stage 0 with the seven stops, polled between beats, stage and state updated per beat, hidden on done/exit/error; the toast transport remains only as the fallback for an older ab. Every beat paints one short ab caption (KiCad tour n/7: ...) for about the beat's length instead of a paragraph for a minute (John, 2026-09-03).++## 0.9.289 (2026-09-03 19:26 UTC)++John, 2026-09-03: no caption on a Symbol Editor launch or rescan reopen (the 0.9.285 one was large, shown too long, and wrong on his laptop: it promised 'behind your window' while KiCad raised itself in front while loading). Launch is silent. _foreground now also reports editorInFrontAtEnd, measured from AD's z-order at return time, so a response never claims background about a window the user can see on top. Dev skill records the rule: one early look, no fighting the user for the foreground, captions only on the opt-in allowFocusSteal path and short.++## 0.9.288 (2026-09-03 19:00 UTC)++Captions before a KiCad window appears now name the calling AI thread (John saw one on his laptop and could not tell which thread was opening the editor). The Symbol Editor close used by rescan goes through ab's desktop_close_window (ab 2.1.12+, graceful, never forced) with the local WM_CLOSE as fallback for older ab; responses say editorCloseVia. Bridge owns KiCad, AD owns Windows (wiki #74 item 3).++## 0.9.287 (2026-09-03 17:58 UTC)++Wiki #71 (Ray, 0.9.286 run): (1) the posted Enter now goes FIRST to the window holding keyboard focus in KiCad's GUI thread (GetGUIThreadInfo), and wxDataView children count as tree candidates regardless of height, so a short Libraries pane no longer yields zero targets; (2) a navigation that ran out of the call budget returns navigation_unconfirmed_within_budget with the trace and _foreground instead of the 'still coming up' contract about an editor that was already driven; (3) an editor the bridge opened after the library file was last written is never judged stale again, so repeated calls stop re-opening it (staleVeto says so); (4) symbol_editor_did_not_open carries pathwaysTried; navigation_failed responses carry errorCode and resolved fields.++## 0.9.286 (2026-09-03 17:05 UTC)++Z-order guard fix: the 0.9.285 guard read AD's window list with the wrong envelope shape and always saw nothing (field test reported 'no window was launched' during a real relaunch). Both the Symbol Editor launch guard and handlers/foreground_guard now read z-order through handlers.kicad_windows, the one parser of AD's desktop_list_windows.++## 0.9.285 (2026-09-03 17:01 UTC)++Foreground etiquette on the Symbol Editor launch (wiki #71 field test): before opening or auto-rescan-relaunching the editor the bridge captions why a KiCad window is about to appear, remembers the user's front window, and the moment the new frame exists looks ONCE at the z-order; if the frame landed above a non-KiCad user window it slides the frame back behind it with SetWindowPos(NOACTIVATE) and then leaves the z-order alone for good (alt-tab and taskbar clicks stay the user's). show_library responses carry _foreground with what happened. kicad_install_symbol success now includes kicadParser (KiCad's verdict on the written file).++## 0.9.284 (2026-09-03 16:44 UTC)++kicad_set_design_rules: response separates fabRuleViolations (DRC items citing one of the fab's rules by name) from otherViolations, and carries drcSummary; the copper layer count is read per layer entry, not per line, so a one-line (layers ...) block no longer infers 1 layer.++## 0.9.283 (2026-09-03 16:38 UTC)++Wiki #73: kicad_list_design_rules and kicad_set_design_rules consume adom/pcb-design-rules at call time (fab, layers, process; layers defaults to the board's copper count; stages <stem>.kicad_dru with backup; runs DRC with it). Wiki #62: kicad_list_symbols (bridge inventory + kicadParser), and kicad_show_library resolves an MPN/file name to the real symbol name (exact, case-insensitive, unique prefix) reporting aliasedFrom/aliasHow. Navigation trace no longer implies the filter was not applied when wx answers WM_GETTEXT with ''. kicad-bridge-dev skill records the #71 lessons.++## 0.9.282 (2026-09-03 16:25 UTC)++Wiki #71 (Ray's remaining case): on a fresh Symbol Editor the filtered tree lists the symbol but nothing is selected, so the posted Enter has nothing to activate and the posted double-click depends on row geometry (DPI). Ray measured select-then-real-Enter loads it. show_library now adds keyboard-only rounds: Down x1..3 posted to the tree candidates, then Enter, title-verified, still PostMessage only (no focus, no foreground). Success responses carry the navigation trace too, so a re-run shows which round landed.++## 0.9.281 (2026-09-03 16:19 UTC)++Wiki #71: kicad_show_library failure responses now carry a per-step navigation trace (search Edit found, parent chain, filter text as read back, tree candidates with class and rect, editor title after Down, after posted Enter, after posted double-click). Ray measured on his box that the tree lists the symbol and a real Enter loads it while the bridge's posted Enter and double-click do not; the trace is how the next build finds which hwnd the Enter must reach.++## 0.9.280 (2026-09-03 16:00 UTC)++kicad_install_symbol now passes errorCode (kicad_refuses_symbol), kicadParser and _hint through to the caller; 0.9.279 set them inside add_symbol but the verb wrapper dropped them, so the wire response had errorCode null.++## 0.9.279 (2026-09-03 15:57 UTC)++Wiki #71 second cause, measured on ConfRoomROG with KiCad's own parser: one malformed symbol makes KiCad refuse a whole .kicad_sym (Symbol Editor lists the library EMPTY) while the bridge's structural reader called it clean. New handlers/kicad_parse_check.py runs kicad-cli sym export svg (read-only) as KiCad's verdict and inventory. kicad_install_symbol and kicad_install_library refuse a file KiCad refuses before writing (kicad_refuses_symbol / kicad_refuses_library); kicad_adom_library_status, kicad_lint_library and the kicad_show_library error carry kicadParser; kicad_adom_library_heal mode:prune drops the symbols KiCad refuses (dryRun names them, original kept as .pruned.bak). Lint's checkmark now says whether KiCad was consulted. Also measured: KiCad's library index is process-wide; a reopened editor frame keeps the old symbol set until kicad.exe fully restarts.++## 0.9.278 (2026-09-03 11:49 UTC)++Wiki #72: kicad_install_library handed a single-part .kicad_sym now returns singlePartFile:true plus a hint naming kicad_install_symbol {filePath}, on both the fresh and already-registered success paths; a real multi-symbol library gets no hint. The verb still does what it did (registers the file as its own library); it just stops being silent about the shared Adom library being untouched.++## 0.9.277 (2026-09-03 02:41 UTC)++kicad_adom_library_status: orphanedLibrary now means the orphan holds symbols the registered file lacks (unique-name compare; new orphanMissingFromRegistered and orphanMissingSample). A count compare kept flagging John's orphan after the 0.9.276 merge because its 122 lenient count included 28 duplicate copies. build_release.py refuses a non-semver first argument (a stray flag was once stamped in as the version).++## 0.9.276 (2026-09-03 02:35 UTC)++heal merge reads a corrupt orphan tolerantly: the orphaned Adom library on a redirected box had 94 symbols inside the library plus 28 duplicate copies appended AFTER the closing paren (the old appender bug's fingerprint), so the strict gate refused the whole file. The heal SOURCE is now read block by block with a quote-aware depth scan, each block balance-checked on its own, duplicates deduped first-copy-wins and reported; the file WRITTEN still goes through the strict gate. Measured on the real file: 94 unique recoverable, 0 unparseable, 28 dupes.++## 0.9.275 (2026-09-03 02:32 UTC)++kicad_adom_library_heal (wiki #71 follow-up, John's call: merge): reconcile a split Adom library by merging every symbol from the orphaned bridge-default file into the registered file KiCad reads, one quote-aware add behind the balance gate with a .bak; dryRun previews; repoint mode kept as the other answer. Never runs on its own. Idempotent, refuses an unbalanced source, never edits or deletes the orphan.+ ## 0.9.274 (2026-09-03 02:06 UTC) Wiki #71 root cause: on a OneDrive-redirected Documents the bridge used %USERPROFILE%/Documents while KiCad resolves the shell Documents folder, so the bridge wrote and reported symbols in a file KiCad never reads. Fix: kicad_detect resolves user_dir like KiCad (handlers/win_paths), the Adom symbol and footprint libraries are ROW-FIRST (install writes to the file sym-/fp-lib-table registers), adom_library_status reports registeredPath/bridgeDefaultPath/split/orphanedLibrary, and the library_cache_stale verdict is built from that measurement instead of a cache story (both prior explanations were disproved by Ray).
SKILL.md+51@@ -146,6 +146,55 @@ Unlabeled nets are auto-named (`Net-(R1-Pad2)`, `unconnected-(C1-Pad1)`); single nets are usually a missed wire but can be intentional — `kicad_run_erc` is the authoritative check and knows about no-connect flags. +## Live routing (from 0.9.340)++Needs KiCad 10.0.1+ with Preferences > Plugins > Enable IPC API server turned on.+The release zip ships the IPC client (`routing_deps/`, from `requirements-routing.txt`),+so nothing has to be pip-installed on the desktop. The live verbs connect to the+exact `filePath` on the selected IPC endpoint (`socketPath` is optional) and edit+the unsaved board in the editor without GUI clicks.++| Verb | Purpose | Required args |+|---|---|---|+| `kicad_routing_state` | Live pads, copper, per-net connected pad groups and revision | `filePath` |+| `kicad_route_net` | Commit one segment, a polyline, or branches on one net as one native Undo step | `filePath`, `expectedRevision`, exact `net`, `points` or `paths` |+| `kicad_remove_route` | Remove specific unlocked copper items as one Undo step | `filePath`, `expectedRevision`, `itemIds` |+| `kicad_routing_validate` | KiCad DRC on the unsaved editor snapshot | `filePath` |++Read routing_state, choose waypoints, then route with its revision. Points may be+`"J1.1"`, `[x,y]`, or `{x,y,layer}`; `{pad:"J2.1",layer:"B.Cu"}` also works.+Coordinates are mm, +y down. A layer marker changes the outgoing layer at that+point using a through via, including at the final waypoint. Width defaults to+0.25 mm, viaSize to 0.6 mm and viaDrill to 0.3 mm. `paths` groups branches on+one net into one Undo step. Only through vias are supported.++`dryRun:true` returns the proposed geometry without editing. By default the+candidate must add no DRC errors relative to the original snapshot; `validate:false`+explicitly skips that preflight. Existing violations and unconnected items may+remain while routing is in progress; routing_validate is the final check.+Sibling on-disk project/rule files are copied for DRC; unsaved project setting+changes and external relative rule resources are not captured. DRC does not+repair clearance errors, choose paths, or shove existing copper. The caller is+the router: there is no autorouter and no electrical sign-off in these verbs.++Success returns itemIds and a fresh revision. `save:true` also saves; the default+is an unsaved live edit. Stale revisions are refused. After a timeout or an unknown+mutation result, inspect the board before deciding what to do; never blindly+replay a mutation. A postCommitError means the copper was committed but the+subsequent read or save failed. Revision checking is optimistic: avoid concurrent+manual edits while committing. Connectivity state that changes mid-read is refused.++For a visible trace-by-trace demo, call route_net once per segment/via; for a+net-by-net demo, send each whole path. See [demo/routing/README.md](demo/routing/README.md).++## Offline copper editing (from 0.9.340)++`kicad_board_pads` reads pads and existing copper from the saved file.+`hasCopper` means some track exists; it does not prove connectivity.+`kicad_add_track`, `kicad_add_via`, and `kicad_route` edit **closed board files**.+They refuse an open board, preserve a `.adom-bak` by default, and never invoke+File > Revert. Use the live verbs above for anything the user should watch.+ ## Exports, linting and settings | CLI form | Purpose | Key args |@@ -165,6 +214,8 @@ authoritative check and knows about no-connect flags. | `kicad_get_settings` | Read the bridge's user-visible settings (overlay badges, ...) | — | | `kicad_set_settings` | Change bridge settings; applies immediately to live windows | settings keys | | `kicad_place_footprint` | Deterministically place a footprint into a board preview | `fileName`, `fileContent`, `footprintName`, `x`, `y`, `open` |+| `kicad_board_pads` | Pads in board coordinates with their nets: the input for routing | `filePath` |+| `kicad_add_track` / `kicad_add_via` / `kicad_route` | Draw copper on a closed board file (see "Offline copper editing") | `filePath`, `net`, points | | `kicad_list_footprints` | List footprints in a library | `libraryName` | | `kicad_make_part_project` | Generate a small real project (pro/sch/pcb) AROUND an installed Adom-library part | part name | | `kicad_install_library_bundle` | Install a wiki Cloud Library bundle zip (symbols + footprints + 3D models) in ONE call | `zipPath`, optional `libraryName`, `capture`, `closeEditor` |
demo/routing/README.mdadded+87@@ -0,0 +1,87 @@+# Live routing development demo++This branch adds explicit copper editing via the official KiCad IPC API. The AI+or caller chooses the route; this is not a general obstacle-avoiding autorouter.+The six-net fixture exercises 13 pads, a branched net and two layer changes.++The [Astra promotion](https://openai.com/index/gpt-6-astra/?video=1223245030)+shows a condensed KiCad placement/routing sequence, but does not document its+routing API. This demo implements the visible copper-editing portion ourselves.++## Container checks++```sh+python3 -m pip install --target /tmp/kicad-routing-deps -r requirements-routing.txt+PYTHONPATH=/tmp/kicad-routing-deps python3 -m unittest discover -s tests -v+python3 tools/build_routing_dev.py /tmp/kicad-routing-overlay.zip+python3 demo/routing/make_fixture.py /tmp/kicad-routing-fixture+```++## Installation++From bridge 0.9.340 the routing verbs and their IPC client (`routing_deps/`, Windows wheels for+CPython 3.11, 3.12 and 3.13) ship inside the normal release zip; nothing is pip-installed on the+desktop. Install the release the canonical way (`bridge_install` with the tier manifest), then:++1. `kicad_ipc_api {"enable":true}` writes KiCad's `api.enable_server` switch (backup kept).+2. `kicad_close`, then open the fixture board again so KiCad starts with the IPC server on.+3. `kicad_routing_state {"filePath":...}` must return a `revision` before any mutation.++The overlay tools (`tools/build_routing_dev.py`, `tools/install_routing_dev.py`) remain for+patching a dev-pinned cache during development; they are not the release path.++Open the generated PCB in KiCad 10.0.1+ with its IPC API enabled. Do not regenerate+or overwrite it while open. Use a separate config for a shared test machine.++```sh+ADOM_API=https://RELAY/proxy/8766 python3 demo/routing/run_demo.py \+ --target arav-rog --board 'C:/Users/arav/Documents/adom-routing-dev/live-routing.kicad_pcb' \+ --plan /tmp/kicad-routing-fixture/plan.json --per-trace --output /tmp/routing-evidence+```++Omit `--per-trace` to commit one whole net per call. The script refuses preexisting+copper, records responses, and requires zero remaining nets, zero DRC errors and+zero unconnected items. Warnings are reported separately. It leaves edits unsaved+unless `--save` is supplied. Each call is a native Undo step. The board need not+be foregrounded for IPC edits; visual evidence requires a window capture.++The current test host is AdomLapper, authorized while arav-rog runs another demo.+Before merging, run both modes on the authorized test host, verify vias/branches and native Undo,+exercise wrong-net/stale-revision/clearance rejection, and confirm the installed+ab fix preserves a pinned cache during refresh and explicit install attempts.++The companion [ab source patch](ab-dev-pin-fix.patch) applies to ab commit+`792b5c0a` (2.1.38). It includes the production guard and its standalone Rust test+harness. It has not been built as a complete Windows application or deployed;+the explicit-install protection must not be claimed until that is done.++All routing and recording run in the background. For UI verification, inspect+`desktop_ui_tree` and invoke `desktop_ui_click`; use exact Win32 menu commands+for Undo (`Ctrl+Z`) and Redo (`Ctrl+Y`). Do not use `Ctrl+Shift+Z` in KiCad (it+activates Draw Filled Zones), or foreground the editor to dismiss a dialog.++## Verified result (2026-09-05)++Both modes passed on AdomLapper with KiCad 10.0.5: six connected nets, 13 pads,+20 segments and two through vias; zero DRC errors and zero unconnected items.+Three warnings identify the deliberately uninstalled RoutingFixture library.+Wrong-net, stale-revision and crossing-route rejection, dry-run immutability,+native Undo/Redo, removal, and Undo restoring all 22 removed items passed.++The final per-trace recording was captured in the background, finalized at 565+frames (1984x1250, 15fps, 37.67 seconds) and decoded without errors. In per-trace+mode the full net is DRC-checked once before its exact segments/vias are committed+against consecutive board revisions. Final DRC checks the complete live board.+The final PCB was saved and pulled back for inspection. Structured results:+[evidence/AdomLapper-2026-09-05.json](evidence/AdomLapper-2026-09-05.json).++Pinned refresh returned `pinned_dev`; the live routing source hash was identical+before and after. The development build also survived the host ab upgrade to+2.1.39. This does not prove protection against explicit bridge_install: the+companion patch remains necessary and has not been deployed.++The companion ab patch also passed `cargo check --locked` against current+2.1.39 source in an isolated Windows checkout on AdomLapper. All four pin guard+tests passed there as well as in the container. The running ab executable was+not replaced; live explicit-install rejection remains a deployment acceptance+check, and no signed/packaged ab release was built.
demo/routing/ab-dev-pin-fix.patchadded+470@@ -0,0 +1,470 @@+From 4da3e46d9627c31115390b26e386690cd0a859bd Mon Sep 17 00:00:00 2001+From: Codex <codex@localhost>+Date: Sat, 5 Sep 2026 12:53:14 -0500+Subject: [PATCH] Respect development pins during explicit installs and cache+ replacement++---+ cli/src/commands.rs | 2 +-+ src-tauri/src/bridge_cache.rs | 47 ++++++++++---+ src-tauri/src/cache_mutation.rs | 111 ++++++++++++++++++++++++++++++++ src-tauri/src/commands.rs | 9 ++-+ src-tauri/src/lib.rs | 3 +-+ tests/dev-pin-harness/Cargo.lock | 72 +++++++++++++++++++++ tests/dev-pin-harness/Cargo.toml | 6 +++ tests/dev-pin-harness/README.md | 23 ++++++++ tests/dev-pin-harness/src/lib.rs | 3 ++ 9 files changed, 259 insertions(+), 17 deletions(-)+ create mode 100644 src-tauri/src/cache_mutation.rs+ create mode 100644 tests/dev-pin-harness/Cargo.lock+ create mode 100644 tests/dev-pin-harness/Cargo.toml+ create mode 100644 tests/dev-pin-harness/README.md+ create mode 100644 tests/dev-pin-harness/src/lib.rs++diff --git a/cli/src/commands.rs b/cli/src/commands.rs+index 6f3c1c32..f62ab8e2 100644+--- a/cli/src/commands.rs+@@ -1534,7 +1534,7 @@ pub fn list_commands() -> Value {+ "returns": "{success, name, paused:true, _hint}"+ },+ "bridge_dev_mode": {+- "description": "Freeze a bridge's cache against ALL automatic updates so you can develop it without ab clobbering your changes. {name, on:true} writes a .dev-pin in the cache dir; while pinned the 4h sweep, on-launch sync, on-use auto-update AND refresh_bridges all skip it (each says outcome:'skipped: dev pin'). {name, on:false} removes the pin and updates resume. The pin survives ab restarts and shows as devPinned:true in bridge_list. bridge_install with force:true is the one deliberate override.",++ "description": "Freeze a bridge's cache against ALL automatic updates so you can develop it without ab clobbering your changes. {name, on:true} writes a .dev-pin in the cache dir; while pinned the 4h sweep, on-launch sync, on-use auto-update AND refresh_bridges all skip it (each says outcome:'skipped: dev pin'). {name, on:false} removes the pin and updates resume. The pin survives ab restarts and shows as devPinned:true in bridge_list. Explicit bridge_install respects the pin, including force:true. Unpin with bridge_dev_mode before installing a release.",+ "args": {"name": "bridge name (from bridge_list)", "on": "true to pin (freeze updates), false to unpin"},+ "returns": "{ok, name, devPinned, pinPath?}",+ },+diff --git a/src-tauri/src/bridge_cache.rs b/src-tauri/src/bridge_cache.rs+index 9665b2ae..06002c3e 100644+--- a/src-tauri/src/bridge_cache.rs+@@ -573,7 +573,7 @@ fn hex_encode(bytes: &[u8]) -> String {+ /// needed (cache invalid AND a bundled seed exists). Returns whether it bootstrapped.+ /// Used by the offline / bundled≥wiki branches of `sync_one` — NOT the update-download+ /// path, which would discard the (huge, for a node bridge) seed copy anyway.+-fn bootstrap_if_invalid(app: &AppHandle, bridge: &str) -> Result<bool, String> {++async fn bootstrap_if_invalid(app: &AppHandle, bridge: &str) -> Result<bool, String> {+ let cache = cache_dir(bridge);+ if cache_is_valid(&cache) {+ return Ok(false);+@@ -584,7 +584,7 @@ fn bootstrap_if_invalid(app: &AppHandle, bridge: &str) -> Result<bool, String> {+ if !has_bundled {+ return Ok(false);+ }+- bootstrap_cache_from_bundled(app, bridge).map_err(|e| format!("bootstrap {bridge} cache: {e}"))?;++ bootstrap_cache_from_bundled(app, bridge).await.map_err(|e| format!("bootstrap {bridge} cache: {e}"))?;+ Ok(true)+ }+ +@@ -634,7 +634,8 @@ pub fn is_dev_pinned(bridge: &str) -> bool {+ /// resulting pinned state; unpinning an already-unpinned bridge is Ok(false).+ /// Every flip is written to the bridge's lifecycle audit so "why did/didn't my+ /// bridge update?" is answerable from one bridge_info read.+-pub fn set_dev_pin(bridge: &str, on: bool, source: &str) -> Result<bool, String> {++pub async fn set_dev_pin(bridge: &str, on: bool, source: &str) -> Result<bool, String> {++ let _mutation = crate::cache_mutation::lock(&cache_dir(bridge)).await;+ let pin = dev_pin_path(bridge);+ if on {+ if let Some(parent) = pin.parent() {+@@ -727,7 +728,7 @@ pub async fn sync_one(app: &AppHandle, bridge: &str) -> Result<(String, String,+ let manifest: BridgeManifest = match crate::with_session_auth(client.get(&manifest_url)).send().await {+ Ok(resp) => {+ if !resp.status().is_success() {+- let bootstrapped = bootstrap_if_invalid(app, bridge)?;++ let bootstrapped = bootstrap_if_invalid(app, bridge).await?;+ let action = if bootstrapped { "bootstrapped" } else { "current_offline" };+ record_sync_detail(bridge, detail(None,+ "failed: manifest not served at the probed tier (bridge not published there, or the page refused)"));+@@ -737,7 +738,7 @@ pub async fn sync_one(app: &AppHandle, bridge: &str) -> Result<(String, String,+ Ok(m) => m,+ Err(e) => {+ log::warn!("[bridge_cache] {bridge} manifest parse failed: {e}");+- let bootstrapped = bootstrap_if_invalid(app, bridge)?;++ let bootstrapped = bootstrap_if_invalid(app, bridge).await?;+ let action = if bootstrapped { "bootstrapped" } else { "current_offline" };+ record_sync_detail(bridge, detail(None, "failed: manifest unparseable"));+ return Ok((from.clone(), from, action.to_string()));+@@ -746,7 +747,7 @@ pub async fn sync_one(app: &AppHandle, bridge: &str) -> Result<(String, String,+ }+ Err(e) => {+ log::warn!("[bridge_cache] {bridge} manifest fetch failed (offline?): {e}");+- let bootstrapped = bootstrap_if_invalid(app, bridge)?;++ let bootstrapped = bootstrap_if_invalid(app, bridge).await?;+ let action = if bootstrapped { "bootstrapped" } else { "current_offline" };+ record_sync_detail(bridge, detail(None, "failed: wiki unreachable (network/timeout)"));+ return Ok((from.clone(), from, action.to_string()));+@@ -771,7 +772,7 @@ pub async fn sync_one(app: &AppHandle, bridge: &str) -> Result<(String, String,+ // "current_bundled_newer"; equal is "current_bundled".+ let bundled = bundled_version(app, bridge).unwrap_or_default();+ if !bundled.is_empty() && !version_gt(&manifest.version, &bundled) {+- let _ = bootstrap_if_invalid(app, bridge);++ let _ = bootstrap_if_invalid(app, bridge).await;+ let action = if version_gt(&bundled, &manifest.version) {+ "current_bundled_newer"+ } else {+@@ -818,7 +819,9 @@ pub async fn sync_one(app: &AppHandle, bridge: &str) -> Result<(String, String,+ + /// Copy the bundled bridge dir into the cache and write a .bridge-version+ /// marker.+-fn bootstrap_cache_from_bundled(app: &AppHandle, bridge: &str) -> Result<(), String> {++async fn bootstrap_cache_from_bundled(app: &AppHandle, bridge: &str) -> Result<(), String> {++ let _mutation = crate::cache_mutation::lock(&cache_dir(bridge)).await;++ crate::cache_mutation::require_unpinned(&cache_dir(bridge))?;+ let bundled = bundled_dir(app, bridge)?;+ if !bundled.is_dir() {+ return Err(format!(+@@ -887,7 +890,8 @@ async fn download_and_install(+ // client-level 20s timeout, and (c) reported truncation as+ // "error decoding response body" — a misleading message. Streaming+ // avoids all three.+- let zip_path = cache_dir(bridge).with_extension("downloading.zip");++ let pending_id = uuid::Uuid::new_v4();++ let zip_path = cache_dir(bridge).with_extension(format!("{pending_id}.downloading.zip"));+ let _ = std::fs::remove_file(&zip_path);+ if let Some(parent) = zip_path.parent() {+ std::fs::create_dir_all(parent).map_err(|e| format!("create cache root: {e}"))?;+@@ -905,7 +909,7 @@ async fn download_and_install(+ }+ + // Extract to a temp dir, then atomically swap with cache.+- let temp = cache_dir(bridge).with_extension("new");++ let temp = cache_dir(bridge).with_extension(format!("{pending_id}.new"));+ if temp.exists() {+ let _ = std::fs::remove_dir_all(&temp);+ }+@@ -965,6 +969,15 @@ async fn download_and_install(+ std::fs::write(temp.join(".bridge-version"), manifest.version.trim())+ .map_err(|e| format!("write .bridge-version: {e}"))?;+ ++ // A pin may have been set while downloading. Serialize the check, reap,++ // and replacement with the Pin/Unpin verb and other cache writers.++ let _mutation = crate::cache_mutation::lock(&cache_dir(bridge)).await;++ if let Err(error) = crate::cache_mutation::require_unpinned(&cache_dir(bridge)) {++ let _ = std::fs::remove_dir_all(&temp);++ let _ = std::fs::remove_file(&zip_path);++ return Err(error);++ }+++ // Atomic-ish swap: rename current cache out, rename temp in, delete old.+ // v1.9.27: if the rename fails with Windows ERROR_SHARING_VIOLATION (os 32)+ // because the bridge is RUNNING and holding the cache dir as its cwd (its+@@ -1822,6 +1835,20 @@ pub async fn install_from_manifest_url(+ });+ }+ ++ // Resolve the actual bridge name from the verified archive, not the URL.++ // Explicit installs obey the same pin as automatic updates. Unpin is a++ // separate deliberate action; force/retries never erase development work.++ let _mutation = crate::cache_mutation::lock(&cache_dir(&canonical_name)).await;++ if let Err(error) = crate::cache_mutation::require_unpinned(&cache_dir(&canonical_name)) {++ let _ = std::fs::remove_dir_all(&temp);++ let _ = std::fs::remove_file(&zip_path);++ return serde_json::json!({++ "ok": false, "errorCode": "dev_pinned", "devPinned": true,++ "bridge": canonical_name, "error": error,++ "_hint": "Development cache preserved. Use bridge_dev_mode {name, on:false} explicitly before installing a release."++ });++ }+++ // Downloaded zip is no longer needed — bytes are now in the temp tree.+ let _ = std::fs::remove_file(&zip_path);+ +diff --git a/src-tauri/src/cache_mutation.rs b/src-tauri/src/cache_mutation.rs+new file mode 100644+index 00000000..80a2e6b2+--- /dev/null+@@ -0,0 +1,111 @@++//! Serialize in-process cache replacement with development pin changes.++//! Manual marker-file writes are observed at the commit boundary; callers that++//! require race-free pinning must use bridge_dev_mode or the GUI pin button.++use std::collections::HashMap;++use std::path::{Path, PathBuf};++use std::sync::{Arc, Mutex, OnceLock};++use tokio::sync::{Mutex as AsyncMutex, OwnedMutexGuard};++++pub async fn lock(cache: &Path) -> OwnedMutexGuard<()> {++ static LOCKS: OnceLock<Mutex<HashMap<PathBuf, Arc<AsyncMutex<()>>>>> = OnceLock::new();++ // Windows cache names are case-insensitive, even before the directory exists.++ let key = if cfg!(windows) {++ PathBuf::from(cache.to_string_lossy().to_lowercase())++ } else {++ cache.to_path_buf()++ };++ let mutex = {++ let mut locks = LOCKS++ .get_or_init(Mutex::default)++ .lock()++ .unwrap_or_else(|e| e.into_inner());++ locks.entry(key).or_default().clone()++ };++ mutex.lock_owned().await++}++++pub fn require_unpinned(cache: &Path) -> Result<(), String> {++ match std::fs::symlink_metadata(cache.join(".dev-pin")) {++ Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),++ Err(e) => Err(format!("Cannot inspect development pin; cache preserved: {e}")),++ Ok(_) => Err(format!(++ "{} is DEV-PINNED; cache and running bridge preserved. Explicitly unpin with bridge_dev_mode before installing a release.",++ cache.display()++ )),++ }++}++++#[cfg(test)]++mod tests {++ use super::*;++ fn fixture() -> PathBuf {++ let path = std::env::temp_dir().join(format!(++ "ab-pin-{}-{}",++ std::process::id(),++ std::time::SystemTime::now()++ .duration_since(std::time::UNIX_EPOCH)++ .unwrap()++ .as_nanos()++ ));++ std::fs::create_dir_all(&path).unwrap();++ path++ }++++ #[tokio::test]++ async fn pin_set_during_download_prevents_commit() {++ let cache = fixture();++ std::fs::write(cache.join("server.py"), "development work").unwrap();++ require_unpinned(&cache).unwrap(); // download starts++ {++ let _pin = lock(&cache).await;++ std::fs::write(cache.join(".dev-pin"), "pinned").unwrap();++ }++ let _commit = lock(&cache).await;++ assert!(require_unpinned(&cache).is_err());++ assert_eq!(++ std::fs::read_to_string(cache.join("server.py")).unwrap(),++ "development work"++ );++ std::fs::remove_dir_all(cache).unwrap();++ }++++ #[tokio::test]++ async fn pin_cannot_acknowledge_during_replacement() {++ let cache = fixture();++ let commit = lock(&cache).await;++ require_unpinned(&cache).unwrap();++ let other = cache.clone();++ let pin = tokio::spawn(async move {++ let _guard = lock(&other).await;++ std::fs::write(other.join(".dev-pin"), "pinned").unwrap();++ });++ tokio::task::yield_now().await;++ assert!(!pin.is_finished());++ drop(commit);++ pin.await.unwrap();++ assert!(require_unpinned(&cache).is_err());++ std::fs::remove_file(cache.join(".dev-pin")).unwrap();++ require_unpinned(&cache).unwrap();++ std::fs::remove_dir_all(cache).unwrap();++ }++++ #[tokio::test]++ async fn independent_bridges_do_not_block_each_other() {++ let a = fixture();++ let b = fixture();++ let _a = lock(&a).await;++ let _b = tokio::time::timeout(std::time::Duration::from_secs(1), lock(&b))++ .await++ .unwrap();++ std::fs::remove_dir_all(a).unwrap();++ std::fs::remove_dir_all(b).unwrap();++ }++++ #[test]++ fn even_a_directory_marker_blocks_install() {++ let cache = fixture();++ std::fs::create_dir(cache.join(".dev-pin")).unwrap();++ assert!(require_unpinned(&cache).is_err());++ std::fs::remove_dir_all(cache).unwrap();++ }++}+diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs+index 1b7a2698..0ddb1c81 100644+--- a/src-tauri/src/commands.rs+@@ -5957,9 +5957,8 @@ async fn handle_restart_bridge(+ /// refresh_bridges). While pinned a developer can modify the cache dir freely+ /// without ab pulling the insiders/public release over their work. It writes a+ /// `.dev-pin` marker in the cache dir, so the pin survives ab restarts and is+-/// visible (bridge_list rows show `devPinned:true`). `bridge_install ... force`+-/// is the one deliberate override — a human explicitly asking to replace the+-/// cache — but the passive sweep never wins.++/// visible (bridge_list rows show `devPinned:true`). Explicit installs also respect the pin;++/// unpin with bridge_dev_mode before installing a release.+ async fn handle_bridge_dev_mode(+ app: &AppHandle,+ ctx: &WsContext,+@@ -5991,11 +5990,11 @@ async fn handle_bridge_dev_mode(+ let caller_tag = crate::caller::Caller::from_args(args, Some(&ctx.server_name))+ .ai_thread+ .unwrap_or_else(|| ctx.server_name.clone());+- let result_json = match crate::bridge_cache::set_dev_pin(&name, on, &format!("bridge_dev_mode verb, {caller_tag}")) {++ let result_json = match crate::bridge_cache::set_dev_pin(&name, on, &format!("bridge_dev_mode verb, {caller_tag}")).await {+ Ok(true) => serde_json::json!({+ "ok": true, "name": name, "devPinned": true,+ "pinPath": crate::bridge_cache::dev_pin_path(&name).to_string_lossy(),+- "_hint": "DEV MODE ON: every automatic update for this bridge is frozen (the 4h sweep, on-launch sync, on-use auto-update, refresh_bridges all skip it). Modify the cache dir freely. Call bridge_dev_mode {\"name\":\"...\",\"on\":false} to resume updates. bridge_install with force:true is the one deliberate override. The bridge's card in the ab window shows an amber 'dev pin' badge while pinned.",++ "_hint": "DEV MODE ON: every automatic update for this bridge is frozen (the 4h sweep, on-launch sync, on-use auto-update, refresh_bridges all skip it). Modify the cache dir freely. Call bridge_dev_mode {\"name\":\"...\",\"on\":false} to resume updates. Explicit bridge_install also respects the pin, including force:true; unpin before installing a release. The bridge's card in the ab window shows an amber 'dev pin' badge while pinned.",+ }),+ Ok(false) => serde_json::json!({+ "ok": true, "name": name, "devPinned": false,+diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs+index 8df61912..713fd96d 100644+--- a/src-tauri/src/lib.rs+@@ -2,6 +2,7 @@ mod ai_thread;+ mod auth;+ mod bridge_autoupdate;+ mod bridge_cache;++mod cache_mutation;+ mod bridge_langs;+ mod bridge_readiness;+ mod bridge_registry;+@@ -3008,7 +3009,7 @@ async fn decline_generic_prompts(app: AppHandle) -> Result<(), String> {+ /// with one click instead of a CLI call. Returns the resulting pinned state.+ #[tauri::command]+ async fn set_bridge_dev_pin(app: AppHandle, name: String, on: bool) -> Result<bool, String> {+- let pinned = bridge_cache::set_dev_pin(&name, on, "ab window button")?;++ let pinned = bridge_cache::set_dev_pin(&name, on, "ab window button").await?;+ let _ = app.emit(+ "bridge-registry-changed",+ serde_json::json!({"reason": "dev-pin", "name": name, "devPinned": pinned}),+diff --git a/tests/dev-pin-harness/Cargo.lock b/tests/dev-pin-harness/Cargo.lock+new file mode 100644+index 00000000..1b950821+--- /dev/null+@@ -0,0 +1,72 @@++# This file is automatically @generated by Cargo.++# It is not intended for manual editing.++version = 4++++[[package]]++name = "ab-dev-pin-tests"++version = "0.1.0"++dependencies = [++ "tokio",++]++++[[package]]++name = "pin-project-lite"++version = "0.2.17"++source = "registry+https://github.com/rust-lang/crates.io-index"++checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"++++[[package]]++name = "proc-macro2"++version = "1.0.107"++source = "registry+https://github.com/rust-lang/crates.io-index"++checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"++dependencies = [++ "unicode-ident",++]++++[[package]]++name = "quote"++version = "1.0.47"++source = "registry+https://github.com/rust-lang/crates.io-index"++checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"++dependencies = [++ "proc-macro2",++]++++[[package]]++name = "syn"++version = "3.0.5"++source = "registry+https://github.com/rust-lang/crates.io-index"++checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"++dependencies = [++ "proc-macro2",++ "quote",++ "unicode-ident",++]++++[[package]]++name = "tokio"++version = "1.53.1"++source = "registry+https://github.com/rust-lang/crates.io-index"++checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"++dependencies = [++ "pin-project-lite",++ "tokio-macros",++]++++[[package]]++name = "tokio-macros"++version = "2.7.2"++source = "registry+https://github.com/rust-lang/crates.io-index"++checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"++dependencies = [++ "proc-macro2",++ "quote",++ "syn",++]++++[[package]]++name = "unicode-ident"++version = "1.0.24"++source = "registry+https://github.com/rust-lang/crates.io-index"++checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"+diff --git a/tests/dev-pin-harness/Cargo.toml b/tests/dev-pin-harness/Cargo.toml+new file mode 100644+index 00000000..9fa1dd7c+--- /dev/null+@@ -0,0 +1,6 @@++[package]++name = "ab-dev-pin-tests"++version = "0.1.0"++edition = "2021"++[dependencies]++tokio = { version = "1", features = ["sync", "rt", "macros", "time"] }+diff --git a/tests/dev-pin-harness/README.md b/tests/dev-pin-harness/README.md+new file mode 100644+index 00000000..16b65d9e+--- /dev/null+@@ -0,0 +1,23 @@++# Development pin regression checks++++Run `cargo test --manifest-path tests/dev-pin-harness/Cargo.toml` on Linux or++Windows. This harness compiles the production cache mutation guard directly,++without the Windows/Tauri GUI dependency tree.++++The fix serializes Pin/Unpin, bundled bootstrap, automatic cache replacement,++and explicit manifest installation. Explicit installation resolves the actual++bridge name from the verified archive before checking the pin, and returns++`dev_pinned` before reaping or changing the cache. `force:true` is not a bypass;++unpinning is a separate deliberate operation. Downloads use independent staging++paths so overlapping automatic downloads cannot overwrite each other's staging.++++Tests cover pins set during download, pin acknowledgement waiting for replacement,++independent bridges, and malformed marker directories. The synchronization covers++one ab process; hand-written marker files are checked at the commit boundary but++cannot participate in the in-process lock. Use the verb or GUI pin button.++++Validation still required before release: full Windows application compilation++and live refresh/install checks, including an install request with force:true.++Verify cache hashes and running PID are preserved for a pinned bridge, then++explicitly unpin and confirm a normal install succeeds. The standalone guard tests++do not substitute for those integration checks.+diff --git a/tests/dev-pin-harness/src/lib.rs b/tests/dev-pin-harness/src/lib.rs+new file mode 100644+index 00000000..cfeb943a+--- /dev/null+@@ -0,0 +1,3 @@++// Compile the production guard directly without Windows/Tauri dependencies.++#[path = "../../../src-tauri/src/cache_mutation.rs"]++pub mod cache_mutation;+-- +2.43.0+
demo/routing/evidence/AdomLapper-2026-09-05.jsonadded+60@@ -0,0 +1,60 @@+{+ "date": "2026-09-05",+ "target": "AdomLapper",+ "kicadVersion": "10.0.5",+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "abVersionAfterUpgrade": "2.1.39",+ "localRoutingTests": 25,+ "abGuardTests": 4,+ "modes": [+ "per-net",+ "per-trace"+ ],+ "segments": 20,+ "vias": 2,+ "nets": 6,+ "pads": 13,+ "drc": {+ "errors": 0,+ "warnings": 3,+ "unconnected": 0,+ "clean": false,+ "stale": false+ },+ "warningExplanation": "Three lib_footprint_issues warnings for the synthetic RoutingFixture library; not installed as a library",+ "checks": {+ "wrongNetRejected": true,+ "staleRevisionRejected": true,+ "dryRunUnchanged": true,+ "crossNetCollisionRejectedWithoutMutation": true,+ "nativeUndoRemovedLastSegment": true,+ "nativeRedoRestoredLastSegment": true,+ "removeAll22ItemsVerified": true,+ "oneUndoRestoredAll22RemovedItems": true,+ "refreshSkippedPinnedCache": true,+ "pinHashBeforeEqualsAfter": true,+ "savedFinalBoard": true+ },+ "recording": {+ "durationSeconds": 37.67,+ "frames": 565,+ "width": 1984,+ "height": 1250,+ "fps": 15,+ "decodedWithoutErrors": true,+ "sha256": "d71543c251948204e49ccefd33ac133a200e528ef27bd3bff31e4709c709247b"+ },+ "limitations": [+ "Caller supplies waypoints; no obstacle search or shove router",+ "Project rules are copied from disk, not unsaved project settings",+ "ab explicit-install pin fix is prepared and unit-tested but not deployed or fully Windows-built"+ ],+ "abWindowsValidation": {+ "sourceVersion": "2.1.39",+ "cargoCheckExitCode": 0,+ "guardTestExitCode": 0,+ "guardTestsPassed": 4,+ "isolatedCheckout": "C:/Users/john/Documents/adom-routing-dev/ab-pin-check-20260905",+ "runningInstallationChanged": false+ }+}
demo/routing/evidence/arav-rog-2026-09-07.jsonadded+56@@ -0,0 +1,56 @@+{+ "host": "arav-rog",+ "date": "2026-09-07",+ "bridge": "0.9.340",+ "zipSha256": "84c6b60b8cc484c502e2cc78274c9016957f0aeb9f5a34ec82b83b89f7dce68f",+ "kicad": "10.0.3",+ "ab": "2.1.66",+ "python": "3.12.10 (vendored routing_deps/cp312)",+ "install": "dev-pinned cache, zip unpacked over a timestamped backup",+ "per-net": {+ "calls": 20,+ "routeNetCalls": 6,+ "segments": 20,+ "vias": 2,+ "netsRemaining": [],+ "drc": {+ "errors": 0,+ "warnings": 3,+ "unconnected": 0,+ "clean": false+ }+ },+ "per-trace": {+ "calls": 36,+ "routeNetCalls": 22,+ "segments": 20,+ "vias": 2,+ "netsRemaining": [],+ "drc": {+ "errors": 0,+ "warnings": 3,+ "unconnected": 0,+ "clean": false+ }+ },+ "rejections": {+ "wrong_net": "net_mismatch",+ "stale": "stale_board",+ "crossing": "drc_rejected",+ "dryRun": [+ true,+ true+ ]+ },+ "remove-all": {+ "success": true,+ "verified": true+ },+ "undo-redo": {+ "before": 22,+ "afterUndo": 21,+ "afterRedo": 22,+ "undoOk": true,+ "redoOk": true+ }+}
demo/routing/evidence/per-net-results.jsonadded+2095@@ -0,0 +1,2095 @@+[+ {+ "verb": "kicad_routing_state",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "netsRemaining": [+ "NET_1",+ "NET_2",+ "NET_3",+ "NET_4",+ "NET_5",+ "NET_6"+ ],+ "revision": "0ffe7515313503db5e6c2fbac3e22eae54ca87f85099b204f657f33924aca35e",+ "segments": [],+ "success": true,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "dryRun": true,+ "netName": "NET_1",+ "revision": "0ffe7515313503db5e6c2fbac3e22eae54ca87f85099b204f657f33924aca35e",+ "segments": [+ {+ "end": [+ 112.0,+ 85.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 85.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 138.0,+ 85.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 85.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 145.0,+ 85.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 85.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "7f462971-23e0-4ce9-b63d-514be8ff4f92",+ "60d205cb-2157-4b1e-9e71-c156d5ab6e1b",+ "f480c34a-cd2d-4d23-9b7a-8ebf6b6fb8ef"+ ],+ "mutated": true,+ "netName": "NET_1",+ "revision": "afd2dd657f31fa440c4fd2a78307fd43e0337276a34ab79c19178af13b92d429",+ "saved": false,+ "segments": [+ {+ "end": [+ 112.0,+ 85.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 85.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 138.0,+ 85.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 85.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 145.0,+ 85.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 85.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_routing_state",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "netsRemaining": [+ "NET_2",+ "NET_3",+ "NET_4",+ "NET_5",+ "NET_6"+ ],+ "revision": "afd2dd657f31fa440c4fd2a78307fd43e0337276a34ab79c19178af13b92d429",+ "segments": [+ {+ "end": {+ "x": 138.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 112.0,+ "y": 85.0+ },+ "uuid": "60d205cb-2157-4b1e-9e71-c156d5ab6e1b",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 105.0,+ "y": 85.0+ },+ "uuid": "7f462971-23e0-4ce9-b63d-514be8ff4f92",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 138.0,+ "y": 85.0+ },+ "uuid": "f480c34a-cd2d-4d23-9b7a-8ebf6b6fb8ef",+ "width": 0.25+ }+ ],+ "success": true,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "dryRun": true,+ "netName": "NET_2",+ "revision": "afd2dd657f31fa440c4fd2a78307fd43e0337276a34ab79c19178af13b92d429",+ "segments": [+ {+ "end": [+ 112.0,+ 90.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 90.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 138.0,+ 90.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 90.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 145.0,+ 90.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 90.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "c290ead2-6ee8-4f4b-ad28-a53982307225",+ "825b4b2a-6284-492e-bbcf-c9eb269ce542",+ "a5a0b151-85e1-40e5-b21e-9cf3bdddcd5b"+ ],+ "mutated": true,+ "netName": "NET_2",+ "revision": "5f92f2bee79ae7d7dbd933e153ec0964ce982b63f2d49b87d61476dd16804616",+ "saved": false,+ "segments": [+ {+ "end": [+ 112.0,+ 90.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 90.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 138.0,+ 90.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 90.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 145.0,+ 90.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 90.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_routing_state",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "netsRemaining": [+ "NET_3",+ "NET_4",+ "NET_5",+ "NET_6"+ ],+ "revision": "5f92f2bee79ae7d7dbd933e153ec0964ce982b63f2d49b87d61476dd16804616",+ "segments": [+ {+ "end": {+ "x": 138.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 112.0,+ "y": 85.0+ },+ "uuid": "60d205cb-2157-4b1e-9e71-c156d5ab6e1b",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 105.0,+ "y": 85.0+ },+ "uuid": "7f462971-23e0-4ce9-b63d-514be8ff4f92",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 138.0,+ "y": 85.0+ },+ "uuid": "f480c34a-cd2d-4d23-9b7a-8ebf6b6fb8ef",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 112.0,+ "y": 90.0+ },+ "uuid": "825b4b2a-6284-492e-bbcf-c9eb269ce542",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 138.0,+ "y": 90.0+ },+ "uuid": "a5a0b151-85e1-40e5-b21e-9cf3bdddcd5b",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 105.0,+ "y": 90.0+ },+ "uuid": "c290ead2-6ee8-4f4b-ad28-a53982307225",+ "width": 0.25+ }+ ],+ "success": true,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "dryRun": true,+ "netName": "NET_3",+ "revision": "5f92f2bee79ae7d7dbd933e153ec0964ce982b63f2d49b87d61476dd16804616",+ "segments": [+ {+ "end": [+ 112.0,+ 95.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 95.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 138.0,+ 95.0+ ],+ "layer": "B.Cu",+ "start": [+ 112.0,+ 95.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 145.0,+ 95.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 95.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "vias": [+ {+ "at": [+ 112.0,+ 95.0+ ],+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "size": 0.6+ },+ {+ "at": [+ 138.0,+ 95.0+ ],+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "size": 0.6+ }+ ]+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "4c072f57-a7d9-4ce1-ad25-51bf8f08aa62",+ "a1cb0b55-242e-4ab4-b735-14540e110574",+ "9ac411ac-cca5-4431-968c-b2d433512e65",+ "1f83a460-caa7-4283-b709-82fb4bdca7ad",+ "6bd8c4fc-3c05-44b9-b216-be6b941ddebe"+ ],+ "mutated": true,+ "netName": "NET_3",+ "revision": "b36e117ec51d01b86a554ba211b687f16718fd308eaf327c2bc2df338f270f61",+ "saved": false,+ "segments": [+ {+ "end": [+ 112.0,+ 95.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 95.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 138.0,+ 95.0+ ],+ "layer": "B.Cu",+ "start": [+ 112.0,+ 95.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 145.0,+ 95.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 95.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": [+ {+ "at": [+ 112.0,+ 95.0+ ],+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "size": 0.6+ },+ {+ "at": [+ 138.0,+ 95.0+ ],+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "size": 0.6+ }+ ]+ }+ },+ {+ "verb": "kicad_routing_state",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "netsRemaining": [+ "NET_4",+ "NET_5",+ "NET_6"+ ],+ "revision": "b36e117ec51d01b86a554ba211b687f16718fd308eaf327c2bc2df338f270f61",+ "segments": [+ {+ "end": {+ "x": 138.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 112.0,+ "y": 85.0+ },+ "uuid": "60d205cb-2157-4b1e-9e71-c156d5ab6e1b",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 105.0,+ "y": 85.0+ },+ "uuid": "7f462971-23e0-4ce9-b63d-514be8ff4f92",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 138.0,+ "y": 85.0+ },+ "uuid": "f480c34a-cd2d-4d23-9b7a-8ebf6b6fb8ef",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 112.0,+ "y": 90.0+ },+ "uuid": "825b4b2a-6284-492e-bbcf-c9eb269ce542",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 138.0,+ "y": 90.0+ },+ "uuid": "a5a0b151-85e1-40e5-b21e-9cf3bdddcd5b",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 105.0,+ "y": 90.0+ },+ "uuid": "c290ead2-6ee8-4f4b-ad28-a53982307225",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 95.0+ },+ "layer": "F.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 105.0,+ "y": 95.0+ },+ "uuid": "4c072f57-a7d9-4ce1-ad25-51bf8f08aa62",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 95.0+ },+ "layer": "F.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 138.0,+ "y": 95.0+ },+ "uuid": "9ac411ac-cca5-4431-968c-b2d433512e65",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 95.0+ },+ "layer": "B.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 112.0,+ "y": 95.0+ },+ "uuid": "a1cb0b55-242e-4ab4-b735-14540e110574",+ "width": 0.25+ }+ ],+ "success": true,+ "vias": [+ {+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net_name": "NET_3",+ "position": {+ "x": 112.0,+ "y": 95.0+ },+ "size": 0.6,+ "uuid": "1f83a460-caa7-4283-b709-82fb4bdca7ad"+ },+ {+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net_name": "NET_3",+ "position": {+ "x": 138.0,+ "y": 95.0+ },+ "size": 0.6,+ "uuid": "6bd8c4fc-3c05-44b9-b216-be6b941ddebe"+ }+ ]+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "dryRun": true,+ "netName": "NET_4",+ "revision": "b36e117ec51d01b86a554ba211b687f16718fd308eaf327c2bc2df338f270f61",+ "segments": [+ {+ "end": [+ 112.0,+ 100.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 100.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 138.0,+ 100.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 100.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 145.0,+ 100.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 100.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "fa807969-4c0d-4f72-8dbd-82f341efd04e",+ "e800fab9-d023-42c4-a1f1-7dc26cb251de",+ "020dd159-1f74-410b-b2f6-5069da44802a"+ ],+ "mutated": true,+ "netName": "NET_4",+ "revision": "8d5300d3268942a6e6d0fcfa9d43bfc0ce14def1d5e5a6743e79f6abc013af6d",+ "saved": false,+ "segments": [+ {+ "end": [+ 112.0,+ 100.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 100.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 138.0,+ 100.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 100.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 145.0,+ 100.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 100.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_routing_state",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "netsRemaining": [+ "NET_5",+ "NET_6"+ ],+ "revision": "8d5300d3268942a6e6d0fcfa9d43bfc0ce14def1d5e5a6743e79f6abc013af6d",+ "segments": [+ {+ "end": {+ "x": 138.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 112.0,+ "y": 85.0+ },+ "uuid": "60d205cb-2157-4b1e-9e71-c156d5ab6e1b",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 105.0,+ "y": 85.0+ },+ "uuid": "7f462971-23e0-4ce9-b63d-514be8ff4f92",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 138.0,+ "y": 85.0+ },+ "uuid": "f480c34a-cd2d-4d23-9b7a-8ebf6b6fb8ef",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 112.0,+ "y": 90.0+ },+ "uuid": "825b4b2a-6284-492e-bbcf-c9eb269ce542",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 138.0,+ "y": 90.0+ },+ "uuid": "a5a0b151-85e1-40e5-b21e-9cf3bdddcd5b",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 105.0,+ "y": 90.0+ },+ "uuid": "c290ead2-6ee8-4f4b-ad28-a53982307225",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 95.0+ },+ "layer": "F.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 105.0,+ "y": 95.0+ },+ "uuid": "4c072f57-a7d9-4ce1-ad25-51bf8f08aa62",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 95.0+ },+ "layer": "F.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 138.0,+ "y": 95.0+ },+ "uuid": "9ac411ac-cca5-4431-968c-b2d433512e65",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 95.0+ },+ "layer": "B.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 112.0,+ "y": 95.0+ },+ "uuid": "a1cb0b55-242e-4ab4-b735-14540e110574",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 138.0,+ "y": 100.0+ },+ "uuid": "020dd159-1f74-410b-b2f6-5069da44802a",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 112.0,+ "y": 100.0+ },+ "uuid": "e800fab9-d023-42c4-a1f1-7dc26cb251de",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 105.0,+ "y": 100.0+ },+ "uuid": "fa807969-4c0d-4f72-8dbd-82f341efd04e",+ "width": 0.25+ }+ ],+ "success": true,+ "vias": [+ {+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net_name": "NET_3",+ "position": {+ "x": 112.0,+ "y": 95.0+ },+ "size": 0.6,+ "uuid": "1f83a460-caa7-4283-b709-82fb4bdca7ad"+ },+ {+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net_name": "NET_3",+ "position": {+ "x": 138.0,+ "y": 95.0+ },+ "size": 0.6,+ "uuid": "6bd8c4fc-3c05-44b9-b216-be6b941ddebe"+ }+ ]+ }+ },+ {+ "verb": "kicad_routing_state",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "netsRemaining": [+ "NET_5",+ "NET_6"+ ],+ "revision": "8d5300d3268942a6e6d0fcfa9d43bfc0ce14def1d5e5a6743e79f6abc013af6d",+ "segments": [+ {+ "end": {+ "x": 138.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 112.0,+ "y": 85.0+ },+ "uuid": "60d205cb-2157-4b1e-9e71-c156d5ab6e1b",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 105.0,+ "y": 85.0+ },+ "uuid": "7f462971-23e0-4ce9-b63d-514be8ff4f92",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 138.0,+ "y": 85.0+ },+ "uuid": "f480c34a-cd2d-4d23-9b7a-8ebf6b6fb8ef",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 112.0,+ "y": 90.0+ },+ "uuid": "825b4b2a-6284-492e-bbcf-c9eb269ce542",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 138.0,+ "y": 90.0+ },+ "uuid": "a5a0b151-85e1-40e5-b21e-9cf3bdddcd5b",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 105.0,+ "y": 90.0+ },+ "uuid": "c290ead2-6ee8-4f4b-ad28-a53982307225",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 95.0+ },+ "layer": "F.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 105.0,+ "y": 95.0+ },+ "uuid": "4c072f57-a7d9-4ce1-ad25-51bf8f08aa62",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 95.0+ },+ "layer": "F.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 138.0,+ "y": 95.0+ },+ "uuid": "9ac411ac-cca5-4431-968c-b2d433512e65",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 95.0+ },+ "layer": "B.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 112.0,+ "y": 95.0+ },+ "uuid": "a1cb0b55-242e-4ab4-b735-14540e110574",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 138.0,+ "y": 100.0+ },+ "uuid": "020dd159-1f74-410b-b2f6-5069da44802a",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 112.0,+ "y": 100.0+ },+ "uuid": "e800fab9-d023-42c4-a1f1-7dc26cb251de",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 105.0,+ "y": 100.0+ },+ "uuid": "fa807969-4c0d-4f72-8dbd-82f341efd04e",+ "width": 0.25+ }+ ],+ "success": true,+ "vias": [+ {+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net_name": "NET_3",+ "position": {+ "x": 112.0,+ "y": 95.0+ },+ "size": 0.6,+ "uuid": "1f83a460-caa7-4283-b709-82fb4bdca7ad"+ },+ {+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net_name": "NET_3",+ "position": {+ "x": 138.0,+ "y": 95.0+ },+ "size": 0.6,+ "uuid": "6bd8c4fc-3c05-44b9-b216-be6b941ddebe"+ }+ ]+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "dryRun": true,+ "netName": "NET_5",+ "revision": "8d5300d3268942a6e6d0fcfa9d43bfc0ce14def1d5e5a6743e79f6abc013af6d",+ "segments": [+ {+ "end": [+ 112.0,+ 105.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 105.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 138.0,+ 105.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 105.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 145.0,+ 105.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 105.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "ac62b492-98d0-49bd-8079-87c140ff71ca",+ "0bd8cf96-5af0-4a37-89c0-48d7f003a103",+ "2da2145a-9bf9-478f-a32d-1235cb0c1346"+ ],+ "mutated": true,+ "netName": "NET_5",+ "revision": "51e658f211d53f071b6894b0b768ce4314e90a6ae06c42092f504786679fca33",+ "saved": false,+ "segments": [+ {+ "end": [+ 112.0,+ 105.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 105.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 138.0,+ 105.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 105.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 145.0,+ 105.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 105.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_routing_state",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "netsRemaining": [+ "NET_6"+ ],+ "revision": "51e658f211d53f071b6894b0b768ce4314e90a6ae06c42092f504786679fca33",+ "segments": [+ {+ "end": {+ "x": 138.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 112.0,+ "y": 85.0+ },+ "uuid": "60d205cb-2157-4b1e-9e71-c156d5ab6e1b",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 105.0,+ "y": 85.0+ },+ "uuid": "7f462971-23e0-4ce9-b63d-514be8ff4f92",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 138.0,+ "y": 85.0+ },+ "uuid": "f480c34a-cd2d-4d23-9b7a-8ebf6b6fb8ef",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 112.0,+ "y": 90.0+ },+ "uuid": "825b4b2a-6284-492e-bbcf-c9eb269ce542",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 138.0,+ "y": 90.0+ },+ "uuid": "a5a0b151-85e1-40e5-b21e-9cf3bdddcd5b",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 105.0,+ "y": 90.0+ },+ "uuid": "c290ead2-6ee8-4f4b-ad28-a53982307225",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 95.0+ },+ "layer": "F.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 105.0,+ "y": 95.0+ },+ "uuid": "4c072f57-a7d9-4ce1-ad25-51bf8f08aa62",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 95.0+ },+ "layer": "F.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 138.0,+ "y": 95.0+ },+ "uuid": "9ac411ac-cca5-4431-968c-b2d433512e65",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 95.0+ },+ "layer": "B.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 112.0,+ "y": 95.0+ },+ "uuid": "a1cb0b55-242e-4ab4-b735-14540e110574",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 138.0,+ "y": 100.0+ },+ "uuid": "020dd159-1f74-410b-b2f6-5069da44802a",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 112.0,+ "y": 100.0+ },+ "uuid": "e800fab9-d023-42c4-a1f1-7dc26cb251de",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 105.0,+ "y": 100.0+ },+ "uuid": "fa807969-4c0d-4f72-8dbd-82f341efd04e",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 105.0+ },+ "layer": "F.Cu",+ "net_name": "NET_5",+ "start": {+ "x": 112.0,+ "y": 105.0+ },+ "uuid": "0bd8cf96-5af0-4a37-89c0-48d7f003a103",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 105.0+ },+ "layer": "F.Cu",+ "net_name": "NET_5",+ "start": {+ "x": 138.0,+ "y": 105.0+ },+ "uuid": "2da2145a-9bf9-478f-a32d-1235cb0c1346",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 105.0+ },+ "layer": "F.Cu",+ "net_name": "NET_5",+ "start": {+ "x": 105.0,+ "y": 105.0+ },+ "uuid": "ac62b492-98d0-49bd-8079-87c140ff71ca",+ "width": 0.25+ }+ ],+ "success": true,+ "vias": [+ {+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net_name": "NET_3",+ "position": {+ "x": 112.0,+ "y": 95.0+ },+ "size": 0.6,+ "uuid": "1f83a460-caa7-4283-b709-82fb4bdca7ad"+ },+ {+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net_name": "NET_3",+ "position": {+ "x": 138.0,+ "y": 95.0+ },+ "size": 0.6,+ "uuid": "6bd8c4fc-3c05-44b9-b216-be6b941ddebe"+ }+ ]+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "dryRun": true,+ "netName": "NET_6",+ "revision": "51e658f211d53f071b6894b0b768ce4314e90a6ae06c42092f504786679fca33",+ "segments": [+ {+ "end": [+ 112.0,+ 110.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 110.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 138.0,+ 110.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 110.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 145.0,+ 110.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 110.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 125.0,+ 113.0+ ],+ "layer": "F.Cu",+ "start": [+ 125.0,+ 110.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 125.0,+ 116.0+ ],+ "layer": "F.Cu",+ "start": [+ 125.0,+ 113.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "ac6f9476-0dec-4e5a-9e53-a20c12f0b5b6",+ "f5e48ca4-74f6-42c3-973d-787986fb4d79",+ "76c20bf0-bd6f-49b0-af9b-6c0c4071ddbd",+ "dc719f86-8b4f-4503-b1cc-cd08646f5b88",+ "bc85f774-cb96-4d85-a425-d43c3a8f01d4"+ ],+ "mutated": true,+ "netName": "NET_6",+ "revision": "c4e968cd87284e5419fb411ae65522b3c41065be2fee967a79812cc93d0cdb74",+ "saved": true,+ "segments": [+ {+ "end": [+ 112.0,+ 110.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 110.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 138.0,+ 110.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 110.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 145.0,+ 110.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 110.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 125.0,+ 113.0+ ],+ "layer": "F.Cu",+ "start": [+ 125.0,+ 110.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 125.0,+ 116.0+ ],+ "layer": "F.Cu",+ "start": [+ 125.0,+ 113.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_routing_state",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "netsRemaining": [],+ "revision": "c4e968cd87284e5419fb411ae65522b3c41065be2fee967a79812cc93d0cdb74",+ "segments": [+ {+ "end": {+ "x": 138.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 112.0,+ "y": 85.0+ },+ "uuid": "60d205cb-2157-4b1e-9e71-c156d5ab6e1b",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 105.0,+ "y": 85.0+ },+ "uuid": "7f462971-23e0-4ce9-b63d-514be8ff4f92",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 138.0,+ "y": 85.0+ },+ "uuid": "f480c34a-cd2d-4d23-9b7a-8ebf6b6fb8ef",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 112.0,+ "y": 90.0+ },+ "uuid": "825b4b2a-6284-492e-bbcf-c9eb269ce542",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 138.0,+ "y": 90.0+ },+ "uuid": "a5a0b151-85e1-40e5-b21e-9cf3bdddcd5b",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 105.0,+ "y": 90.0+ },+ "uuid": "c290ead2-6ee8-4f4b-ad28-a53982307225",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 95.0+ },+ "layer": "F.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 105.0,+ "y": 95.0+ },+ "uuid": "4c072f57-a7d9-4ce1-ad25-51bf8f08aa62",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 95.0+ },+ "layer": "F.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 138.0,+ "y": 95.0+ },+ "uuid": "9ac411ac-cca5-4431-968c-b2d433512e65",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 95.0+ },+ "layer": "B.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 112.0,+ "y": 95.0+ },+ "uuid": "a1cb0b55-242e-4ab4-b735-14540e110574",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 138.0,+ "y": 100.0+ },+ "uuid": "020dd159-1f74-410b-b2f6-5069da44802a",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 112.0,+ "y": 100.0+ },+ "uuid": "e800fab9-d023-42c4-a1f1-7dc26cb251de",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 105.0,+ "y": 100.0+ },+ "uuid": "fa807969-4c0d-4f72-8dbd-82f341efd04e",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 105.0+ },+ "layer": "F.Cu",+ "net_name": "NET_5",+ "start": {+ "x": 112.0,+ "y": 105.0+ },+ "uuid": "0bd8cf96-5af0-4a37-89c0-48d7f003a103",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 105.0+ },+ "layer": "F.Cu",+ "net_name": "NET_5",+ "start": {+ "x": 138.0,+ "y": 105.0+ },+ "uuid": "2da2145a-9bf9-478f-a32d-1235cb0c1346",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 105.0+ },+ "layer": "F.Cu",+ "net_name": "NET_5",+ "start": {+ "x": 105.0,+ "y": 105.0+ },+ "uuid": "ac62b492-98d0-49bd-8079-87c140ff71ca",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 110.0+ },+ "layer": "F.Cu",+ "net_name": "NET_6",+ "start": {+ "x": 138.0,+ "y": 110.0+ },+ "uuid": "76c20bf0-bd6f-49b0-af9b-6c0c4071ddbd",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 110.0+ },+ "layer": "F.Cu",+ "net_name": "NET_6",+ "start": {+ "x": 105.0,+ "y": 110.0+ },+ "uuid": "ac6f9476-0dec-4e5a-9e53-a20c12f0b5b6",+ "width": 0.25+ },+ {+ "end": {+ "x": 125.0,+ "y": 116.0+ },+ "layer": "F.Cu",+ "net_name": "NET_6",+ "start": {+ "x": 125.0,+ "y": 113.0+ },+ "uuid": "bc85f774-cb96-4d85-a425-d43c3a8f01d4",+ "width": 0.25+ },+ {+ "end": {+ "x": 125.0,+ "y": 113.0+ },+ "layer": "F.Cu",+ "net_name": "NET_6",+ "start": {+ "x": 125.0,+ "y": 110.0+ },+ "uuid": "dc719f86-8b4f-4503-b1cc-cd08646f5b88",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 110.0+ },+ "layer": "F.Cu",+ "net_name": "NET_6",+ "start": {+ "x": 112.0,+ "y": 110.0+ },+ "uuid": "f5e48ca4-74f6-42c3-973d-787986fb4d79",+ "width": 0.25+ }+ ],+ "success": true,+ "vias": [+ {+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net_name": "NET_3",+ "position": {+ "x": 112.0,+ "y": 95.0+ },+ "size": 0.6,+ "uuid": "1f83a460-caa7-4283-b709-82fb4bdca7ad"+ },+ {+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net_name": "NET_3",+ "position": {+ "x": 138.0,+ "y": 95.0+ },+ "size": 0.6,+ "uuid": "6bd8c4fc-3c05-44b9-b216-be6b941ddebe"+ }+ ]+ }+ },+ {+ "verb": "kicad_routing_validate",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "clean": false,+ "errors": 0,+ "revision": "c4e968cd87284e5419fb411ae65522b3c41065be2fee967a79812cc93d0cdb74",+ "stale": false,+ "success": true,+ "unconnected": 0,+ "warnings": 3+ }+ }+]
demo/routing/evidence/per-trace-results.jsonadded+2154@@ -0,0 +1,2154 @@+[+ {+ "verb": "kicad_routing_state",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "netsRemaining": [+ "NET_1",+ "NET_2",+ "NET_3",+ "NET_4",+ "NET_5",+ "NET_6"+ ],+ "revision": "0ffe7515313503db5e6c2fbac3e22eae54ca87f85099b204f657f33924aca35e",+ "segments": [],+ "success": true,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "dryRun": true,+ "netName": "NET_1",+ "revision": "0ffe7515313503db5e6c2fbac3e22eae54ca87f85099b204f657f33924aca35e",+ "segments": [+ {+ "end": [+ 112.0,+ 85.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 85.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 138.0,+ 85.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 85.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 145.0,+ 85.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 85.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "204516c1-6cc1-4f13-81dd-97e88d0f6341"+ ],+ "mutated": true,+ "netName": "NET_1",+ "revision": "2c8b91cafe5db4ae0cdced7ec8b8697a2afc78193ede1cc25e8c13af20c23d34",+ "saved": false,+ "segments": [+ {+ "end": [+ 112.0,+ 85.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 85.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "8bf10672-a0ee-4cf3-b847-1731ea4a9625"+ ],+ "mutated": true,+ "netName": "NET_1",+ "revision": "78da52680db7640a9fc70a4d66c00d80c5b603e50d7e398dc48210eea7017d9f",+ "saved": false,+ "segments": [+ {+ "end": [+ 138.0,+ 85.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 85.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "c5729098-056b-416c-9034-382de0363fc9"+ ],+ "mutated": true,+ "netName": "NET_1",+ "revision": "da2986116d728c8751251453795c4d17999f8c1a542684fbc037738b91f44524",+ "saved": false,+ "segments": [+ {+ "end": [+ 145.0,+ 85.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 85.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_routing_state",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "netsRemaining": [+ "NET_2",+ "NET_3",+ "NET_4",+ "NET_5",+ "NET_6"+ ],+ "revision": "da2986116d728c8751251453795c4d17999f8c1a542684fbc037738b91f44524",+ "segments": [+ {+ "end": {+ "x": 112.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 105.0,+ "y": 85.0+ },+ "uuid": "204516c1-6cc1-4f13-81dd-97e88d0f6341",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 112.0,+ "y": 85.0+ },+ "uuid": "8bf10672-a0ee-4cf3-b847-1731ea4a9625",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 138.0,+ "y": 85.0+ },+ "uuid": "c5729098-056b-416c-9034-382de0363fc9",+ "width": 0.25+ }+ ],+ "success": true,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "dryRun": true,+ "netName": "NET_2",+ "revision": "da2986116d728c8751251453795c4d17999f8c1a542684fbc037738b91f44524",+ "segments": [+ {+ "end": [+ 112.0,+ 90.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 90.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 138.0,+ 90.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 90.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 145.0,+ 90.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 90.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "e58f20a2-d5c8-4aea-90f4-2b8062a849d3"+ ],+ "mutated": true,+ "netName": "NET_2",+ "revision": "4d5a5bb12ac3113ad5b1d4c7c727399b844ca56d62faa9fa9ad05da17df20257",+ "saved": false,+ "segments": [+ {+ "end": [+ 112.0,+ 90.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 90.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "20a19c21-7d51-4f0e-b08b-0e7b308a0dfd"+ ],+ "mutated": true,+ "netName": "NET_2",+ "revision": "98ed1b61125c6dfd29f6605348b7c41788c98a9829f61046afc49281e8a550ee",+ "saved": false,+ "segments": [+ {+ "end": [+ 138.0,+ 90.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 90.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "93c32a75-9aa1-4f46-a79b-313d3bc3b9cb"+ ],+ "mutated": true,+ "netName": "NET_2",+ "revision": "4fe70a8d853c9af9ce88c8de9bb0c4f13acfcfa7f1906d5c1a2699682457f147",+ "saved": false,+ "segments": [+ {+ "end": [+ 145.0,+ 90.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 90.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_routing_state",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "netsRemaining": [+ "NET_3",+ "NET_4",+ "NET_5",+ "NET_6"+ ],+ "revision": "4fe70a8d853c9af9ce88c8de9bb0c4f13acfcfa7f1906d5c1a2699682457f147",+ "segments": [+ {+ "end": {+ "x": 112.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 105.0,+ "y": 85.0+ },+ "uuid": "204516c1-6cc1-4f13-81dd-97e88d0f6341",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 112.0,+ "y": 85.0+ },+ "uuid": "8bf10672-a0ee-4cf3-b847-1731ea4a9625",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 138.0,+ "y": 85.0+ },+ "uuid": "c5729098-056b-416c-9034-382de0363fc9",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 112.0,+ "y": 90.0+ },+ "uuid": "20a19c21-7d51-4f0e-b08b-0e7b308a0dfd",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 138.0,+ "y": 90.0+ },+ "uuid": "93c32a75-9aa1-4f46-a79b-313d3bc3b9cb",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 105.0,+ "y": 90.0+ },+ "uuid": "e58f20a2-d5c8-4aea-90f4-2b8062a849d3",+ "width": 0.25+ }+ ],+ "success": true,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "dryRun": true,+ "netName": "NET_3",+ "revision": "4fe70a8d853c9af9ce88c8de9bb0c4f13acfcfa7f1906d5c1a2699682457f147",+ "segments": [+ {+ "end": [+ 112.0,+ 95.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 95.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 138.0,+ 95.0+ ],+ "layer": "B.Cu",+ "start": [+ 112.0,+ 95.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 145.0,+ 95.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 95.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "vias": [+ {+ "at": [+ 112.0,+ 95.0+ ],+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "size": 0.6+ },+ {+ "at": [+ 138.0,+ 95.0+ ],+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "size": 0.6+ }+ ]+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "7b0015e4-2eea-465d-a520-fc57a8905027"+ ],+ "mutated": true,+ "netName": "NET_3",+ "revision": "f95504196f4e404dca3130231a5f035352684a8b51e1adb91990545814cfb4d2",+ "saved": false,+ "segments": [+ {+ "end": [+ 112.0,+ 95.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 95.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "528f8cfc-8c27-4e95-b4b7-4c02f31f6a7e"+ ],+ "mutated": true,+ "netName": "NET_3",+ "revision": "863c731b7d99636373e76b583a415e9c5a60049481087a1f0de2422d55571554",+ "saved": false,+ "segments": [+ {+ "end": [+ 138.0,+ 95.0+ ],+ "layer": "B.Cu",+ "start": [+ 112.0,+ 95.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "38f4bf28-7160-46f5-b177-084997b9b0a7"+ ],+ "mutated": true,+ "netName": "NET_3",+ "revision": "9c7870163719f0b45e341108c488bc9fe4bef70d4b64bfc83c7638baccdbbbf2",+ "saved": false,+ "segments": [+ {+ "end": [+ 145.0,+ 95.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 95.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "a121391f-e430-4bb6-9b13-4d104d5d554c"+ ],+ "mutated": true,+ "netName": "NET_3",+ "revision": "278200e4aef84ef3728eb8001dfb51213d3a6b9ffcd3f6837256dd3268b210da",+ "saved": false,+ "segments": [],+ "success": true,+ "undoSteps": 1,+ "vias": [+ {+ "at": [+ 112.0,+ 95.0+ ],+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "size": 0.6+ }+ ]+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "702da6ac-2582-4507-98de-76a296f6d55d"+ ],+ "mutated": true,+ "netName": "NET_3",+ "revision": "c0dc2136c57759e297fba6576d657d863e0f844450ccbe360c210a67151b6f3d",+ "saved": false,+ "segments": [],+ "success": true,+ "undoSteps": 1,+ "vias": [+ {+ "at": [+ 138.0,+ 95.0+ ],+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "size": 0.6+ }+ ]+ }+ },+ {+ "verb": "kicad_routing_state",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "netsRemaining": [+ "NET_4",+ "NET_5",+ "NET_6"+ ],+ "revision": "c0dc2136c57759e297fba6576d657d863e0f844450ccbe360c210a67151b6f3d",+ "segments": [+ {+ "end": {+ "x": 112.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 105.0,+ "y": 85.0+ },+ "uuid": "204516c1-6cc1-4f13-81dd-97e88d0f6341",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 112.0,+ "y": 85.0+ },+ "uuid": "8bf10672-a0ee-4cf3-b847-1731ea4a9625",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 138.0,+ "y": 85.0+ },+ "uuid": "c5729098-056b-416c-9034-382de0363fc9",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 112.0,+ "y": 90.0+ },+ "uuid": "20a19c21-7d51-4f0e-b08b-0e7b308a0dfd",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 138.0,+ "y": 90.0+ },+ "uuid": "93c32a75-9aa1-4f46-a79b-313d3bc3b9cb",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 105.0,+ "y": 90.0+ },+ "uuid": "e58f20a2-d5c8-4aea-90f4-2b8062a849d3",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 95.0+ },+ "layer": "F.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 138.0,+ "y": 95.0+ },+ "uuid": "38f4bf28-7160-46f5-b177-084997b9b0a7",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 95.0+ },+ "layer": "F.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 105.0,+ "y": 95.0+ },+ "uuid": "7b0015e4-2eea-465d-a520-fc57a8905027",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 95.0+ },+ "layer": "B.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 112.0,+ "y": 95.0+ },+ "uuid": "528f8cfc-8c27-4e95-b4b7-4c02f31f6a7e",+ "width": 0.25+ }+ ],+ "success": true,+ "vias": [+ {+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net_name": "NET_3",+ "position": {+ "x": 138.0,+ "y": 95.0+ },+ "size": 0.6,+ "uuid": "702da6ac-2582-4507-98de-76a296f6d55d"+ },+ {+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net_name": "NET_3",+ "position": {+ "x": 112.0,+ "y": 95.0+ },+ "size": 0.6,+ "uuid": "a121391f-e430-4bb6-9b13-4d104d5d554c"+ }+ ]+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "dryRun": true,+ "netName": "NET_4",+ "revision": "c0dc2136c57759e297fba6576d657d863e0f844450ccbe360c210a67151b6f3d",+ "segments": [+ {+ "end": [+ 112.0,+ 100.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 100.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 138.0,+ 100.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 100.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 145.0,+ 100.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 100.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "953ecdff-1d63-4671-83db-6437c2cff3e3"+ ],+ "mutated": true,+ "netName": "NET_4",+ "revision": "dd65dace30efce842eb9cec62bcb768e448cbc5ae0dfa12c2805a7bf7e96a7fa",+ "saved": false,+ "segments": [+ {+ "end": [+ 112.0,+ 100.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 100.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "bba3aceb-fc4a-4ae5-b50d-9871bdd26bef"+ ],+ "mutated": true,+ "netName": "NET_4",+ "revision": "faa1626a71ce2832f32fcbfcbe21863891ed0283cb1b39fb715df3cd80ee70f5",+ "saved": false,+ "segments": [+ {+ "end": [+ 138.0,+ 100.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 100.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "8ce6ae36-b30a-4774-94e1-b4f2174e2712"+ ],+ "mutated": true,+ "netName": "NET_4",+ "revision": "ab80f4f5a4477cd95ea353f44152aed8a723c883baf3c7f73bdb53bf2cbb47d8",+ "saved": false,+ "segments": [+ {+ "end": [+ 145.0,+ 100.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 100.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_routing_state",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "netsRemaining": [+ "NET_5",+ "NET_6"+ ],+ "revision": "ab80f4f5a4477cd95ea353f44152aed8a723c883baf3c7f73bdb53bf2cbb47d8",+ "segments": [+ {+ "end": {+ "x": 112.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 105.0,+ "y": 85.0+ },+ "uuid": "204516c1-6cc1-4f13-81dd-97e88d0f6341",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 112.0,+ "y": 85.0+ },+ "uuid": "8bf10672-a0ee-4cf3-b847-1731ea4a9625",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 138.0,+ "y": 85.0+ },+ "uuid": "c5729098-056b-416c-9034-382de0363fc9",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 112.0,+ "y": 90.0+ },+ "uuid": "20a19c21-7d51-4f0e-b08b-0e7b308a0dfd",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 138.0,+ "y": 90.0+ },+ "uuid": "93c32a75-9aa1-4f46-a79b-313d3bc3b9cb",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 105.0,+ "y": 90.0+ },+ "uuid": "e58f20a2-d5c8-4aea-90f4-2b8062a849d3",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 95.0+ },+ "layer": "F.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 138.0,+ "y": 95.0+ },+ "uuid": "38f4bf28-7160-46f5-b177-084997b9b0a7",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 95.0+ },+ "layer": "F.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 105.0,+ "y": 95.0+ },+ "uuid": "7b0015e4-2eea-465d-a520-fc57a8905027",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 95.0+ },+ "layer": "B.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 112.0,+ "y": 95.0+ },+ "uuid": "528f8cfc-8c27-4e95-b4b7-4c02f31f6a7e",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 138.0,+ "y": 100.0+ },+ "uuid": "8ce6ae36-b30a-4774-94e1-b4f2174e2712",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 105.0,+ "y": 100.0+ },+ "uuid": "953ecdff-1d63-4671-83db-6437c2cff3e3",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 112.0,+ "y": 100.0+ },+ "uuid": "bba3aceb-fc4a-4ae5-b50d-9871bdd26bef",+ "width": 0.25+ }+ ],+ "success": true,+ "vias": [+ {+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net_name": "NET_3",+ "position": {+ "x": 138.0,+ "y": 95.0+ },+ "size": 0.6,+ "uuid": "702da6ac-2582-4507-98de-76a296f6d55d"+ },+ {+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net_name": "NET_3",+ "position": {+ "x": 112.0,+ "y": 95.0+ },+ "size": 0.6,+ "uuid": "a121391f-e430-4bb6-9b13-4d104d5d554c"+ }+ ]+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "dryRun": true,+ "netName": "NET_5",+ "revision": "ab80f4f5a4477cd95ea353f44152aed8a723c883baf3c7f73bdb53bf2cbb47d8",+ "segments": [+ {+ "end": [+ 112.0,+ 105.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 105.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 138.0,+ 105.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 105.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 145.0,+ 105.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 105.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "5d31746f-f547-4caf-9593-67b195e258d6"+ ],+ "mutated": true,+ "netName": "NET_5",+ "revision": "dc1d00a95899244294471a36365894952705bffbc4ea98173987ae6d5994d2c3",+ "saved": false,+ "segments": [+ {+ "end": [+ 112.0,+ 105.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 105.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "469156d1-1678-4e49-940c-50ff05882b83"+ ],+ "mutated": true,+ "netName": "NET_5",+ "revision": "f93f258ebcbe7c119a9b779ea19eb44eadea38082cc543291745189dd397e491",+ "saved": false,+ "segments": [+ {+ "end": [+ 138.0,+ 105.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 105.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "0179ec36-c803-483f-884f-576bcb0eac51"+ ],+ "mutated": true,+ "netName": "NET_5",+ "revision": "39370a641c6b77708b7c7449a5702049ec9e1d7be88210875cf07f6f2a2130ce",+ "saved": false,+ "segments": [+ {+ "end": [+ 145.0,+ 105.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 105.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_routing_state",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "netsRemaining": [+ "NET_6"+ ],+ "revision": "39370a641c6b77708b7c7449a5702049ec9e1d7be88210875cf07f6f2a2130ce",+ "segments": [+ {+ "end": {+ "x": 112.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 105.0,+ "y": 85.0+ },+ "uuid": "204516c1-6cc1-4f13-81dd-97e88d0f6341",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 112.0,+ "y": 85.0+ },+ "uuid": "8bf10672-a0ee-4cf3-b847-1731ea4a9625",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 138.0,+ "y": 85.0+ },+ "uuid": "c5729098-056b-416c-9034-382de0363fc9",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 112.0,+ "y": 90.0+ },+ "uuid": "20a19c21-7d51-4f0e-b08b-0e7b308a0dfd",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 138.0,+ "y": 90.0+ },+ "uuid": "93c32a75-9aa1-4f46-a79b-313d3bc3b9cb",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 105.0,+ "y": 90.0+ },+ "uuid": "e58f20a2-d5c8-4aea-90f4-2b8062a849d3",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 95.0+ },+ "layer": "F.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 138.0,+ "y": 95.0+ },+ "uuid": "38f4bf28-7160-46f5-b177-084997b9b0a7",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 95.0+ },+ "layer": "F.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 105.0,+ "y": 95.0+ },+ "uuid": "7b0015e4-2eea-465d-a520-fc57a8905027",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 95.0+ },+ "layer": "B.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 112.0,+ "y": 95.0+ },+ "uuid": "528f8cfc-8c27-4e95-b4b7-4c02f31f6a7e",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 138.0,+ "y": 100.0+ },+ "uuid": "8ce6ae36-b30a-4774-94e1-b4f2174e2712",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 105.0,+ "y": 100.0+ },+ "uuid": "953ecdff-1d63-4671-83db-6437c2cff3e3",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 112.0,+ "y": 100.0+ },+ "uuid": "bba3aceb-fc4a-4ae5-b50d-9871bdd26bef",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 105.0+ },+ "layer": "F.Cu",+ "net_name": "NET_5",+ "start": {+ "x": 138.0,+ "y": 105.0+ },+ "uuid": "0179ec36-c803-483f-884f-576bcb0eac51",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 105.0+ },+ "layer": "F.Cu",+ "net_name": "NET_5",+ "start": {+ "x": 112.0,+ "y": 105.0+ },+ "uuid": "469156d1-1678-4e49-940c-50ff05882b83",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 105.0+ },+ "layer": "F.Cu",+ "net_name": "NET_5",+ "start": {+ "x": 105.0,+ "y": 105.0+ },+ "uuid": "5d31746f-f547-4caf-9593-67b195e258d6",+ "width": 0.25+ }+ ],+ "success": true,+ "vias": [+ {+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net_name": "NET_3",+ "position": {+ "x": 138.0,+ "y": 95.0+ },+ "size": 0.6,+ "uuid": "702da6ac-2582-4507-98de-76a296f6d55d"+ },+ {+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net_name": "NET_3",+ "position": {+ "x": 112.0,+ "y": 95.0+ },+ "size": 0.6,+ "uuid": "a121391f-e430-4bb6-9b13-4d104d5d554c"+ }+ ]+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "dryRun": true,+ "netName": "NET_6",+ "revision": "39370a641c6b77708b7c7449a5702049ec9e1d7be88210875cf07f6f2a2130ce",+ "segments": [+ {+ "end": [+ 112.0,+ 110.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 110.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 138.0,+ 110.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 110.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 145.0,+ 110.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 110.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 125.0,+ 113.0+ ],+ "layer": "F.Cu",+ "start": [+ 125.0,+ 110.0+ ],+ "width": 0.25+ },+ {+ "end": [+ 125.0,+ 116.0+ ],+ "layer": "F.Cu",+ "start": [+ 125.0,+ 113.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "d69a2efd-9c3f-43b2-a6ea-d6ebb96fed7f"+ ],+ "mutated": true,+ "netName": "NET_6",+ "revision": "4ffc5ec1d7e00106f0bf944611a247c752e6e7b51ad32b9695d59aa1c98011b4",+ "saved": false,+ "segments": [+ {+ "end": [+ 112.0,+ 110.0+ ],+ "layer": "F.Cu",+ "start": [+ 105.0,+ 110.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "776a005b-2c14-41c4-b8c0-bc6275afd081"+ ],+ "mutated": true,+ "netName": "NET_6",+ "revision": "cfc783531549ad3a8cec5356bd89f406e28e5bf7a14dbde58a473acadde8851e",+ "saved": false,+ "segments": [+ {+ "end": [+ 138.0,+ 110.0+ ],+ "layer": "F.Cu",+ "start": [+ 112.0,+ 110.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "95875e34-9dba-4966-934e-c7c2f76ceca0"+ ],+ "mutated": true,+ "netName": "NET_6",+ "revision": "2ddc0faa3c8a73565730aaae62216c65948258b58097d57f5e77acc71e0dec75",+ "saved": false,+ "segments": [+ {+ "end": [+ 145.0,+ 110.0+ ],+ "layer": "F.Cu",+ "start": [+ 138.0,+ 110.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "974edd43-d2f4-4766-806a-d1143b8be7f7"+ ],+ "mutated": true,+ "netName": "NET_6",+ "revision": "55952ed763dac07788d8d59f2284a13cf5c1f69b3db1f77af25f7901e1eb689a",+ "saved": false,+ "segments": [+ {+ "end": [+ 125.0,+ 113.0+ ],+ "layer": "F.Cu",+ "start": [+ 125.0,+ 110.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_route_net",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "itemIds": [+ "92fe9465-3af5-482c-9136-9869f0ae8452"+ ],+ "mutated": true,+ "netName": "NET_6",+ "revision": "2a370857996036674c8c3ef17eacc87e7ac8c19f48a993ea87bdb69151fa51f4",+ "saved": true,+ "segments": [+ {+ "end": [+ 125.0,+ 116.0+ ],+ "layer": "F.Cu",+ "start": [+ 125.0,+ 113.0+ ],+ "width": 0.25+ }+ ],+ "success": true,+ "undoSteps": 1,+ "vias": []+ }+ },+ {+ "verb": "kicad_routing_state",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "netsRemaining": [],+ "revision": "2a370857996036674c8c3ef17eacc87e7ac8c19f48a993ea87bdb69151fa51f4",+ "segments": [+ {+ "end": {+ "x": 112.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 105.0,+ "y": 85.0+ },+ "uuid": "204516c1-6cc1-4f13-81dd-97e88d0f6341",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 112.0,+ "y": 85.0+ },+ "uuid": "8bf10672-a0ee-4cf3-b847-1731ea4a9625",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 85.0+ },+ "layer": "F.Cu",+ "net_name": "NET_1",+ "start": {+ "x": 138.0,+ "y": 85.0+ },+ "uuid": "c5729098-056b-416c-9034-382de0363fc9",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 112.0,+ "y": 90.0+ },+ "uuid": "20a19c21-7d51-4f0e-b08b-0e7b308a0dfd",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 138.0,+ "y": 90.0+ },+ "uuid": "93c32a75-9aa1-4f46-a79b-313d3bc3b9cb",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 90.0+ },+ "layer": "F.Cu",+ "net_name": "NET_2",+ "start": {+ "x": 105.0,+ "y": 90.0+ },+ "uuid": "e58f20a2-d5c8-4aea-90f4-2b8062a849d3",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 95.0+ },+ "layer": "F.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 138.0,+ "y": 95.0+ },+ "uuid": "38f4bf28-7160-46f5-b177-084997b9b0a7",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 95.0+ },+ "layer": "F.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 105.0,+ "y": 95.0+ },+ "uuid": "7b0015e4-2eea-465d-a520-fc57a8905027",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 95.0+ },+ "layer": "B.Cu",+ "net_name": "NET_3",+ "start": {+ "x": 112.0,+ "y": 95.0+ },+ "uuid": "528f8cfc-8c27-4e95-b4b7-4c02f31f6a7e",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 138.0,+ "y": 100.0+ },+ "uuid": "8ce6ae36-b30a-4774-94e1-b4f2174e2712",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 105.0,+ "y": 100.0+ },+ "uuid": "953ecdff-1d63-4671-83db-6437c2cff3e3",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 100.0+ },+ "layer": "F.Cu",+ "net_name": "NET_4",+ "start": {+ "x": 112.0,+ "y": 100.0+ },+ "uuid": "bba3aceb-fc4a-4ae5-b50d-9871bdd26bef",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 105.0+ },+ "layer": "F.Cu",+ "net_name": "NET_5",+ "start": {+ "x": 138.0,+ "y": 105.0+ },+ "uuid": "0179ec36-c803-483f-884f-576bcb0eac51",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 105.0+ },+ "layer": "F.Cu",+ "net_name": "NET_5",+ "start": {+ "x": 112.0,+ "y": 105.0+ },+ "uuid": "469156d1-1678-4e49-940c-50ff05882b83",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 105.0+ },+ "layer": "F.Cu",+ "net_name": "NET_5",+ "start": {+ "x": 105.0,+ "y": 105.0+ },+ "uuid": "5d31746f-f547-4caf-9593-67b195e258d6",+ "width": 0.25+ },+ {+ "end": {+ "x": 138.0,+ "y": 110.0+ },+ "layer": "F.Cu",+ "net_name": "NET_6",+ "start": {+ "x": 112.0,+ "y": 110.0+ },+ "uuid": "776a005b-2c14-41c4-b8c0-bc6275afd081",+ "width": 0.25+ },+ {+ "end": {+ "x": 125.0,+ "y": 116.0+ },+ "layer": "F.Cu",+ "net_name": "NET_6",+ "start": {+ "x": 125.0,+ "y": 113.0+ },+ "uuid": "92fe9465-3af5-482c-9136-9869f0ae8452",+ "width": 0.25+ },+ {+ "end": {+ "x": 145.0,+ "y": 110.0+ },+ "layer": "F.Cu",+ "net_name": "NET_6",+ "start": {+ "x": 138.0,+ "y": 110.0+ },+ "uuid": "95875e34-9dba-4966-934e-c7c2f76ceca0",+ "width": 0.25+ },+ {+ "end": {+ "x": 125.0,+ "y": 113.0+ },+ "layer": "F.Cu",+ "net_name": "NET_6",+ "start": {+ "x": 125.0,+ "y": 110.0+ },+ "uuid": "974edd43-d2f4-4766-806a-d1143b8be7f7",+ "width": 0.25+ },+ {+ "end": {+ "x": 112.0,+ "y": 110.0+ },+ "layer": "F.Cu",+ "net_name": "NET_6",+ "start": {+ "x": 105.0,+ "y": 110.0+ },+ "uuid": "d69a2efd-9c3f-43b2-a6ea-d6ebb96fed7f",+ "width": 0.25+ }+ ],+ "success": true,+ "vias": [+ {+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net_name": "NET_3",+ "position": {+ "x": 138.0,+ "y": 95.0+ },+ "size": 0.6,+ "uuid": "702da6ac-2582-4507-98de-76a296f6d55d"+ },+ {+ "drill": 0.3,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net_name": "NET_3",+ "position": {+ "x": 112.0,+ "y": 95.0+ },+ "size": 0.6,+ "uuid": "a121391f-e430-4bb6-9b13-4d104d5d554c"+ }+ ]+ }+ },+ {+ "verb": "kicad_routing_validate",+ "result": {+ "bridgeVersion": "0.10.0-dev.per-net.1",+ "clean": false,+ "errors": 0,+ "revision": "2a370857996036674c8c3ef17eacc87e7ac8c19f48a993ea87bdb69151fa51f4",+ "stale": false,+ "success": true,+ "unconnected": 0,+ "warnings": 3+ }+ }+]
demo/routing/make_fixture.pyadded+42@@ -0,0 +1,42 @@+"""Create a small, entirely unrouted two-layer routing regression board."""+from pathlib import Path+import json+import uuid+++def make(directory):+ directory = Path(directory)+ directory.mkdir(parents=True, exist_ok=True)+ base = Path(__file__).resolve().parents[2] / 'templates/blank-board.kicad_pcb'+ text = base.read_text()+ blocks=[]+ def footprint(ref,x,y,pads):+ body=f'(footprint "RoutingFixture:{ref}" (layer "F.Cu") (uuid "{uuid.uuid4()}") (at {x} {y})\n'+ body+=f'(property "Reference" "{ref}" (at 0 -3) (layer "F.SilkS") (effects (font (size 1 1) (thickness 0.15))))\n'+ body+=f'(property "Value" "ROUTING_TEST" (at 0 -5) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))))\n'+ body+='(attr through_hole exclude_from_pos_files exclude_from_bom)\n'+ for number, px, py, net in pads:+ body+=f'(pad "{number}" thru_hole circle (at {px} {py}) (size 2 2) (drill 1) (layers "*.Cu" "*.Mask") (net "{net}") (uuid "{uuid.uuid4()}"))\n'+ blocks.append(body+')\n')+ footprint('J1',105,85,[(str(i),0,(i-1)*5,f'NET_{i}') for i in range(1,7)])+ footprint('J2',145,85,[(str(i),0,(i-1)*5,f'NET_{i}') for i in range(1,7)])+ footprint('J3',125,116,[('1',0,0,'NET_6')])+ blocks.append(f'(gr_rect (start 98 78) (end 152 122) (stroke (width 0.05) (type default)) (fill none) (layer "Edge.Cuts") (uuid "{uuid.uuid4()}"))\n')+ output=directory/'live-routing.kicad_pcb'+ output.write_text(text[:text.rfind(')')]+''.join(blocks)+')\n')+ plan=[]+ for i in range(1,7):+ y=85+(i-1)*5+ points=[f'J1.{i}',[112,y],[138,y],f'J2.{i}']+ if i==3:+ points=[f'J1.{i}',{'x':112,'y':y,'layer':'B.Cu'},{'x':138,'y':y,'layer':'F.Cu'},f'J2.{i}']+ row={'net':f'NET_{i}','width':0.25,'points':points}+ if i==6:+ row.pop('points'); row['paths']=[points,[[125,y], [125,113], 'J3.1']]+ plan.append(row)+ (directory/'plan.json').write_text(json.dumps(plan,indent=2)+'\n')+ return output++if __name__=='__main__':+ import sys+ print(make(sys.argv[1] if len(sys.argv)>1 else Path(__file__).parent/'fixture'))
demo/routing/run_demo.pyadded+79@@ -0,0 +1,79 @@+"""Replay explicit net waypoints through ab in a live KiCad editor.++Require --target and ADOM_API for a remote test. Never mutates anything unless+routing_state proves the correct board and the expected dev build is installed.+"""+import argparse+import json+import os+from pathlib import Path+import subprocess+import time+++def call(target, verb, args):+ proc=subprocess.run(['adom-bridge','--ai-thread','KiCad per-net routing','--target',target,+ verb,json.dumps(args)],capture_output=True,text=True,timeout=240)+ result=json.loads(proc.stdout)+ if result.get('success') is False or result.get('status') == 'error':+ raise RuntimeError(f'{verb}: {result.get("errorCode")}: {result.get("error")}')+ return result+++def main():+ p=argparse.ArgumentParser(description=__doc__)+ p.add_argument('--target',required=True)+ p.add_argument('--board',required=True)+ p.add_argument('--plan',required=True)+ p.add_argument('--output',default='routing-evidence')+ p.add_argument('--per-trace',action='store_true')+ p.add_argument('--socket')+ p.add_argument('--save',action='store_true')+ p.add_argument('--expect-version',default='',help='refuse unless bridgeVersion starts with this (default: any release that reports one)')+ a=p.parse_args()+ out=Path(a.output);out.mkdir(parents=True,exist_ok=True)+ common={'filePath':a.board}+ if a.socket:common['socketPath']=a.socket+ records=[]+ def invoke(verb,args):+ r=call(a.target,verb,{**common,**args})+ records.append({'verb':verb,'result':r})+ (out/'report.json').write_text(json.dumps(records,indent=2))+ return r+ state=invoke('kicad_routing_state',{})+ if not state.get('revision') or not state.get('bridgeVersion','').startswith(a.expect_version):+ raise RuntimeError('Bridge reported no revision or an unexpected bridgeVersion; refusing to continue')+ if state['segments'] or state['vias']:+ raise RuntimeError('Demo requires an unrouted scratch board; will not remove existing copper')+ (out/'before.json').write_text(json.dumps(state,indent=2))+ plans=json.loads(Path(a.plan).read_text())+ for index,plan in enumerate(plans):+ revision=state['revision']+ preview=invoke('kicad_route_net',{**plan,'expectedRevision':revision,'dryRun':True})+ work=[plan]+ if a.per_trace:+ work=[{'net':plan['net'],'width':s['width'],'layer':s['layer'],'points':[s['start'],s['end']]}+ for s in preview['segments']]+ work.extend({'net':plan['net'],'layer':'F.Cu','viaSize':v['size'],'viaDrill':v['drill'],+ 'points':[v['at'],{'x':v['at'][0],'y':v['at'][1],'layer':'B.Cu'}]}+ for v in preview['vias'])+ for item_index,item in enumerate(work):+ # The complete net was DRC-checked above. Replay its exact steps+ # against consecutive revisions, then check final board DRC.+ result=invoke('kicad_route_net',{**item,'expectedRevision':revision,+ 'validate':not a.per_trace,+ 'save':a.save and index==len(plans)-1 and item_index==len(work)-1})+ if not result.get('mutated') or not result.get('revision'):+ raise RuntimeError('Mutation outcome incomplete; inspect the board before retrying')+ revision=result['revision']+ print(f"{plan['net']}: step {item_index+1}/{len(work)}, {len(result['itemIds'])} copper items committed",flush=True)+ time.sleep(0.4)+ state=invoke('kicad_routing_state',{})+ print('Nets remaining:',len(state['netsRemaining']),flush=True)+ final=invoke('kicad_routing_validate',{})+ (out/'after.json').write_text(json.dumps(state,indent=2))+ if state['netsRemaining'] or final['unconnected'] or final['errors'] or final['stale']:+ raise RuntimeError('Final connectivity/DRC failed: '+str({k:final[k] for k in ['errors','warnings','unconnected','stale']}))+ print(json.dumps({k:final[k] for k in ['errors','warnings','unconnected','clean']}),flush=True)++if __name__=='__main__':main()
tests/test_ipc_api.pyadded+30@@ -0,0 +1,30 @@+import json, os, sys, tempfile, unittest+from pathlib import Path+from unittest.mock import patch+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))+from handlers import ipc_api++class IpcApi(unittest.TestCase):+ def test_read_flip_and_backup(self):+ with tempfile.TemporaryDirectory() as d:+ root = Path(d) / "kicad"+ for ver, on in (("9.0", True), ("10.0", False)):+ (root / ver).mkdir(parents=True)+ (root / ver / "kicad_common.json").write_text(json.dumps({"api": {"enable_server": on}, "other": 1}))+ with patch.object(ipc_api, "_settings_roots", return_value=[root]):+ r = ipc_api.handle_ipc_api({}, {})+ self.assertTrue(r["success"]); self.assertFalse(r["enabled"]); self.assertEqual(r["kicadSettingsVersion"], "10.0")+ r = ipc_api.handle_ipc_api({}, {"enable": True})+ self.assertTrue(r["changed"] and r["needsRestart"] and r["enabled"])+ data = json.loads((root / "10.0" / "kicad_common.json").read_text())+ self.assertTrue(data["api"]["enable_server"]); self.assertEqual(data["other"], 1)+ self.assertTrue(Path(r["backup"]).is_file())+ self.assertFalse(ipc_api.handle_ipc_api({}, {"enable": True})["changed"])+ self.assertEqual(ipc_api.handle_ipc_api({}, {"enable": "yes"})["errorCode"], "invalid_arg")++ def test_missing_settings(self):+ with patch.object(ipc_api, "_settings_roots", return_value=[Path("/nonexistent-adom")]):+ self.assertEqual(ipc_api.handle_ipc_api({}, {})["errorCode"], "settings_not_found")++if __name__ == "__main__":+ unittest.main()
tests/test_live_routing.pyadded+109@@ -0,0 +1,109 @@+import copy+from pathlib import Path+import sys+import tempfile+import unittest+from unittest.mock import patch++sys.path.insert(0, str(Path(__file__).resolve().parents[1]))+from handlers import live_routing as live, route+++def board():+ return {'net_format':'name', 'nets':[{'name':'GND'}, {'name':'VCC'}, {'name':'123'}],+ 'layers':[{'name':s} for s in ['F.Cu','In1.Cu','B.Cu']],+ 'footprints':[{'reference':'J1','layer':'F.Cu','position':{'x':10,'y':20,'rotation':90},+ 'pads':[{'number':'1','x':2,'y':0,'net_name':'GND','layers':['*.Cu'],'type':'thru_hole'},+ {'number':'2','x':0,'y':0,'net_name':'VCC','layers':['F.Cu'],'type':'smd'}]},+ {'reference':'J2','layer':'B.Cu','position':{'x':30,'y':20},+ 'pads':[{'number':'1','x':0,'y':0,'net_name':'GND','layers':['B.Cu'],'type':'smd'}]}]}+++class Planning(unittest.TestCase):+ def test_rotated_pad_coordinates_and_final_layer_transition(self):+ p=live._plan(board(), {'net':'GND','points':['J1.1',{'pad':'J2.1','layer':'B.Cu'}]})+ self.assertEqual(p['segments'][0]['start'],[10,18])+ self.assertEqual(p['vias'][0]['at'],[30,20])+ self.assertEqual(p['vias'][0]['layers'],['F.Cu','B.Cu'])++ def test_branches_on_one_net(self):+ p=live._plan(board(), {'net':'GND','paths':[['J1.1',[20,18]],[[20,18],[20,25]]]})+ self.assertEqual(len(p['segments']),2)++ def test_reject_wrong_net_and_layer(self):+ for args,code in [({'net':'GND','points':['J1.1','J1.2']},'net_mismatch'),+ ({'net':'GND','points':['J1.1','J2.1']},'pad_layer_mismatch'),+ ({'net':'GND','points':['J1.1',{'x':20,'y':20,'layer':'F.SilkS'}]},'unknown_layer')]:+ with self.subTest(code=code),self.assertRaises(live.RoutingError) as cm: live._plan(board(),args)+ self.assertEqual(cm.exception.code,code)++ def test_invalid_geometry_is_rejected_before_mutation(self):+ for args in [{'width':0},{'width':-1},{'width':float('nan')},{'width':True},{'points':[[True,0],[1,1]]},{'points':[[0,0,9],[1,1]]},+ {'viaDrill':0.8,'viaSize':0.6},{'points':[[0,0],[float('inf'),0]]},+ {'points':[[0,0],[0,0]]}]:+ with self.subTest(args=args),self.assertRaises(live.RoutingError):+ live._plan(board(),{'net':'GND','points':[[0,0],[1,1]],**args})++ def test_numeric_net_name_in_kicad_10_file(self):+ from parsers.pcb import parse_pcb_text+ data=parse_pcb_text('(kicad_pcb (version 20260206) (footprint "X" (layer "F.Cu") (pad "1" thru_hole circle (net "123"))))')+ self.assertEqual(data["net_format"],"name")+ self.assertEqual(data["nets"],[{"number":None,"name":"123"}])++ def test_exact_numeric_net_name(self):+ self.assertEqual(live._plan(board(),{'net':'123','points':[[0,0],[1,1]]})['netName'],'123')++ def test_quoted_net_serialization(self):+ self.assertIn('"a\\"b\\\\c"',route._net_sexpr({'format':'name','name':'a"b\\c'}))++ def test_stale_revision(self):+ for rev in [None,'old']:+ with self.assertRaises(live.RoutingError): live._revision({'expectedRevision':rev},'new')++ def test_existing_error_does_not_mask_additional_same_error(self):+ v={'type':'clearance','severity':'error','description':'collision','items':[]}+ self.assertEqual(live._new_errors({'violations':[v]},{'violations':[v,v]}),[v])++ def test_open_file_refused_even_with_private_arg(self):+ with patch.object(route,'_lock_file',return_value=Path('lock')):+ self.assertEqual(route._load({'filePath':'test.kicad_pcb','_readOnly':True})[-1]['errorCode'],'board_open')+++class Transactions(unittest.TestCase):+ class Board:+ def __init__(self): self.ops=[]+ def begin_commit(self): self.ops.append('begin'); return 'commit'+ def push_commit(self,c,m): self.ops.append('push')+ def drop_commit(self,c): self.ops.append('drop')++ def test_success_is_one_undo_step(self):+ b=self.Board()+ self.assertEqual(live._transaction(b,lambda:42,'route'),42)+ self.assertEqual(b.ops,['begin','push'])++ def test_partial_create_failure_rolls_back(self):+ b=self.Board()+ def fail(): raise RuntimeError('partial create')+ with self.assertRaises(live.RoutingError) as cm: live._transaction(b,fail,'route')+ self.assertEqual(cm.exception.code,'mutation_rolled_back')+ self.assertEqual(b.ops,['begin','drop'])++ def test_push_timeout_is_unknown_even_if_drop_acknowledged(self):+ b = self.Board()+ def push(*args): raise TimeoutError('commit response lost')+ b.push_commit = push+ with self.assertRaises(live.RoutingError) as cm:+ live._transaction(b, lambda: 42, 'route')+ self.assertEqual(cm.exception.code, 'mutation_outcome_unknown')+ self.assertIsNone(cm.exception.detail['mutated'])++ def test_failed_rollback_is_unknown_not_safe_to_retry(self):+ b=self.Board()+ def fail(*a): raise RuntimeError('lost IPC')+ b.drop_commit=fail+ with self.assertRaises(live.RoutingError) as cm: live._transaction(b,fail,'route')+ self.assertEqual(cm.exception.code,'mutation_outcome_unknown')+ self.assertIsNone(cm.exception.detail['mutated'])+++if __name__=='__main__': unittest.main()
tests/test_routing_ipc.pyadded+151@@ -0,0 +1,151 @@+"""Exercise public handlers with real kipy copper types and a simulated editor.++IPC transport and DRC execution are mocked here; Windows live tests remain required.+"""+from contextlib import contextmanager+from pathlib import Path+from types import SimpleNamespace+import tempfile+import unittest+from unittest.mock import patch+import sys+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))+from handlers import live_routing as live, route+from demo.routing.make_fixture import make+try:+ from kipy.board_types import Net+except ImportError:+ Net = None++[email protected](Net is None, 'Install requirements-routing.txt for protocol tests')+class IPCHandlers(unittest.TestCase):+ def setUp(self):+ self.tmp = tempfile.TemporaryDirectory()+ self.path = make(self.tmp.name)+ text = self.path.read_text()+ self.args = {'filePath': str(self.path), 'net': 'NET_1', 'points': ['J1.1', 'J2.1'], 'validate': False}+ class Board:+ def __init__(b):+ b.text = text; b.items = []; b.ops = []; b.partial = False; b.fail_read = False; b.fail_save = False; b.keep_removed = False+ def get_as_string(b):+ if b.fail_read: raise RuntimeError('read failed after commit')+ return b.text+ def get_nets(b):+ n = Net(); n.name = 'NET_1'; return [n]+ def begin_commit(b): b.ops.append('begin'); return 'commit'+ def create_items(b, items):+ for i, item in enumerate(items): item.id.value = 'created-'+str(i)+ b.items += items+ return [] if b.partial else items+ def push_commit(b, *_): b.ops.append('push'); b.text += '\n'+ def drop_commit(b, *_): b.ops.append('drop'); b.items = []+ def save(b):+ if b.fail_save: raise RuntimeError('save failed')+ b.ops.append('save')+ def get_tracks(b): return b.items+ def get_vias(b): return []+ def remove_items(b, items):+ if not b.keep_removed: b.items = []+ self.board = Board()+ @contextmanager+ def connect(args): yield self.board+ self.patch = patch.object(live, '_connect', connect); self.patch.start()+ self.args['expectedRevision'] = live._snapshot(self.board, self.args)[2]++ def tearDown(self): self.patch.stop(); self.tmp.cleanup()++ def test_copper_protocol_units_net_and_layer(self):+ result = live.handle_route_net({}, self.args)+ self.assertTrue(result['success'], result)+ self.assertEqual(self.board.items[0].width, 250000)+ self.assertEqual(self.board.items[0].start.x, 105000000)+ self.assertEqual(self.board.items[0].net.name, 'NET_1')+ self.assertEqual(self.board.ops, ['begin', 'push'])+ self.assertNotEqual(result['revision'], self.args['expectedRevision'])+ self.assertFalse(result['saved'])++ def test_dry_run_does_not_start_transaction(self):+ result = live.handle_route_net({}, {**self.args, 'dryRun': True})+ self.assertTrue(result['success'], result)+ self.assertEqual(self.board.ops, [])++ def test_drc_rejection_and_edit_during_preflight_do_not_mutate(self):+ ok = {'violations': []}+ bad = {'violations': [{'severity': 'error', 'type': 'clearance'}]}+ with patch.object(live, '_drc', side_effect=[ok, bad]):+ r = live.handle_route_net({}, {**self.args, 'validate': True})+ self.assertEqual(r['errorCode'], 'drc_rejected')+ def drc(*a): self.board.text += '\n'; return ok+ with patch.object(live, '_drc', side_effect=drc):+ r = live.handle_route_net({}, {**self.args, 'validate': True})+ self.assertEqual(r['errorCode'], 'stale_board')+ self.assertEqual(self.board.ops, [])++ def test_partial_create_is_rolled_back(self):+ self.board.partial = True+ r = live.handle_route_net({}, self.args)+ self.assertEqual(r['errorCode'], 'mutation_rolled_back')+ self.assertEqual(self.board.ops, ['begin', 'drop'])+ self.assertFalse(self.board.items)++ def test_post_commit_save_failure_is_not_failed_mutation(self):+ self.board.fail_save = True+ r = live.handle_route_net({}, {**self.args, 'save': True})+ self.assertTrue(r['success'], r); self.assertTrue(r['mutated'])+ self.assertFalse(r['saved']); self.assertIn('save failed', r['postCommitError'])+ self.assertEqual(self.board.ops, ['begin', 'push'])++ def test_replay_rejected(self):+ self.assertTrue(live.handle_route_net({}, self.args)['success'])+ self.assertEqual(live.handle_route_net({}, self.args)['errorCode'], 'stale_board')+ self.assertEqual(self.board.ops, ['begin', 'push'])++ def test_remove_readback_detects_ignored_deletion(self):+ r = live.handle_route_net({}, self.args)+ self.board.keep_removed = True+ with patch.object(live, '_delete_items', side_effect=RuntimeError('deletion rejected')):+ r = live.handle_remove_route({}, {**self.args, 'expectedRevision': r['revision'], 'itemIds': r['itemIds']})+ self.assertEqual(r['errorCode'], 'mutation_rolled_back')+ self.assertEqual(self.board.ops[-2:], ['begin', 'drop'])++ def test_deletion_protocol_requires_each_id_accepted(self):+ from kipy.proto.common.commands.editor_commands_pb2 import DeleteItemsResponse, IDS_OK, IDS_IMMUTABLE+ from kipy.proto.common.types.base_types_pb2 import DocumentSpecifier, IRS_OK+ live.handle_route_net({}, self.args)+ response = DeleteItemsResponse(status=IRS_OK)+ row = response.deleted_items.add(); row.id.CopyFrom(self.board.items[0].id); row.status = IDS_OK+ self.board._doc = DocumentSpecifier()+ self.board._kicad = SimpleNamespace(send=lambda *a: response)+ live._delete_items(self.board, self.board.items)+ row.status = IDS_IMMUTABLE+ with self.assertRaises(RuntimeError): live._delete_items(self.board, self.board.items)+ del response.deleted_items[:]+ live._delete_items(self.board, self.board.items) # KiCad 10.0.5 compatibility+ response.status = 0+ with self.assertRaises(RuntimeError): live._delete_items(self.board, self.board.items)++ def test_wrong_or_locked_item_never_deleted(self):+ r = live.handle_remove_route({}, {**self.args, 'itemIds': ['not-copper']})+ self.assertEqual(r['errorCode'], 'invalid_item_ids')+ self.assertEqual(self.board.ops, [])+++class OfflineTests(unittest.TestCase):+ def test_numeric_net_name_and_fractional_code(self):+ b = {'nets': [{'number': 1, 'name': '123'}], 'net_format': 'number'}+ self.assertEqual(route._resolve_net(b, '123')[0]['number'], 1)+ for spec in [1.5, float('nan'), True, '123 '.strip()+'x']:+ self.assertIsNotNone(route._resolve_net(b, spec)[1])++ def test_atomic_file_edit_and_open_board_refusal(self):+ with tempfile.TemporaryDirectory() as d:+ path = make(d); before = path.read_bytes()+ r = route.handle_route({}, {'filePath':str(path),'net':'NET_1','points':['J1.1','J2.1']})+ self.assertTrue(r['success'], r)+ self.assertEqual(Path(str(path)+'.adom-bak').read_bytes(), before)+ path.with_name('~'+path.name+'.lck').write_text('open')+ after = path.read_bytes()+ r = route.handle_add_track({}, {'filePath':str(path),'net':'NET_1','start':[1,1],'end':[2,2]})+ self.assertEqual(r['errorCode'], 'board_open')+ self.assertEqual(path.read_bytes(), after)
tools/build_release.py+29−4@@ -15,6 +15,7 @@ import hashlib import json import os import subprocess+import re import sys import zipfile from datetime import datetime, timezone@@ -55,6 +56,11 @@ def main() -> int: print(__doc__) return 2 version = sys.argv[1].lstrip("v")+ # The version is positional. A flag in slot 1 ("--notes ...") once got+ # stamped into BRIDGE_VERSION and bridge.json as the version string.+ if not re.fullmatch(r"\d+\.\d+\.\d+", version):+ print(f"REFUSING: first argument must be the version (got {version!r}); usage: build_release.py <x.y.z> [--publish] --notes \"...\"")+ return 2 publish = "--publish" in sys.argv # WHY NOTES ARE MANDATORY (John, 2026-09-02). #@@ -114,6 +120,16 @@ def main() -> int: return 1 files = json.load(open("tools/release_files.json"))["files"]+ # Whole directories (vendored wheels for the routing verbs, tools/release_files.json+ # "dirs"). Walked at build time because the file names carry wheel versions.+ for d in json.load(open("tools/release_files.json")).get("dirs", []):+ if not os.path.isdir(d):+ print("REFUSING TO BUILD - listed directory is missing:", d)+ return 1+ for root, _dirs, names in os.walk(d):+ if "__pycache__" in root:+ continue+ files.extend(os.path.join(root, n).replace(os.sep, "/") for n in sorted(names)) missing = [f for f in files if not os.path.isfile(f)] if missing: print("REFUSING TO BUILD - listed files are missing:")@@ -129,9 +145,14 @@ def main() -> int: json.dump(bj, open("bridge.json", "w"), indent=2) out = f"adom-bridge-kicad-v{version}.zip"- with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:- for f in files:- z.write(f, f)+ # --reuse-zip: publish the exact bytes that were staged and tested on a box+ # (a rebuild changes zip timestamps, hence the sha256 the test evidence names).+ if "--reuse-zip" in sys.argv and os.path.isfile(out):+ print(f"reusing existing {out} (tested bytes; not rebuilt)")+ else:+ with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:+ for f in files:+ z.write(f, f) raw = open(out, "rb").read() sha, size = hashlib.sha256(raw).hexdigest(), len(raw)@@ -179,8 +200,12 @@ def main() -> int: # (insiders-tier-verify) caught public == insiders == 0.9.273 immediately. # The public manifest is written by tools/promote.py, or by --public. tier_public = "--public" in sys.argv+ # Vendored third-party trees (release_files.json "dirs") ship in the zip but+ # are NOT source: tools/vendor_routing_deps.py rebuilds them from+ # requirements-routing.txt, so they stay out of the page repo.+ vendored = tuple(d.rstrip("/") + "/" for d in json.load(open("tools/release_files.json")).get("dirs", [])) push_files = ["BRIDGE_VERSION", "bridge.json"] + ([MANIFEST] if tier_public else []) + [- f for f in files if os.path.isfile(f) and f != MANIFEST]+ f for f in files if os.path.isfile(f) and f != MANIFEST and not f.startswith(vendored)] print(f"pushing {len(push_files)} source file(s) to the page repo") # chunked: a single push of the whole file list can exceed the request cap CHUNK = 40
tools/build_routing_dev.pyadded+17@@ -0,0 +1,17 @@+"""Build only the routing overlay; does not publish or alter release manifests."""+import ast+import json+from pathlib import Path+import sys+import zipfile+from install_routing_dev import FILES, NAMES++root=Path(__file__).resolve().parents[1]+tree=ast.parse((root/'server.py').read_text())+node=next(n for n in tree.body if isinstance(n,ast.Assign) and any(getattr(t,'id','')=='_VERB_CATALOG' for t in n.targets))+catalog=ast.literal_eval(node.value)+output=Path(sys.argv[1] if len(sys.argv)>1 else root/'kicad-routing-overlay.zip')+with zipfile.ZipFile(output,'w',zipfile.ZIP_DEFLATED) as z:+ for name in FILES: z.write(root/name,name)+ z.writestr('routing-catalog.json',json.dumps({n:catalog[n] for n in NAMES}))+print(output)
tools/install_routing_dev.pyadded+92@@ -0,0 +1,92 @@+"""Apply the routing development overlay to an existing PINNED bridge cache.++Run with the bridge stopped: python install_routing_dev.py CACHE OVERLAY.zip.+Only the routing files and registration entries change; unrelated newer bridge+fixes stay installed. A timestamped backup beside the cache permits restoration.+"""+import ast+from datetime import datetime, timezone+import json+from pathlib import Path+import shutil+import sys+import zipfile++FILES = ['handlers/live_routing.py', 'handlers/route.py', 'parsers/pcb.py', 'requirements-routing.txt']+NAMES = ['routing_state', 'route_net', 'remove_route', 'routing_validate', 'board_pads', 'add_track', 'add_via', 'route']+++def after_assignment(source, name, addition):+ node = next(n for n in ast.walk(ast.parse(source)) if isinstance(n, ast.Assign)+ and any(isinstance(t, ast.Name) and t.id == name for t in n.targets))+ lines = source.splitlines(keepends=True)+ indentation = ' ' * node.col_offset+ lines.insert(node.end_lineno, '\n' + '\n'.join(indentation + line for line in addition.splitlines()) + '\n')+ return ''.join(lines)+++def install(cache, overlay):+ cache = Path(cache)+ if not (cache / '.dev-pin').is_file():+ raise ValueError('Refusing: use ab bridge_dev_mode {name:kicad,on:true} first')+ # Read and compile the entire overlay before touching the running cache.+ with zipfile.ZipFile(overlay) as z:+ contents = {name: z.read(name) for name in FILES}+ catalog = json.loads(z.read('routing-catalog.json'))+ if set(catalog) != set(NAMES):+ raise ValueError('Incomplete routing verb catalog')+ for name, data in contents.items():+ if name.endswith('.py'):+ compile(data, name, 'exec')+ source = (cache / 'server.py').read_text(encoding='utf-8')+ # Remove only our delimited registrations so reinstallation updates help too.+ import re+ source = re.sub(r'\n[ \t]*# ADOM_LIVE_ROUTING_DEV_BEGIN.*?# ADOM_LIVE_ROUTING_DEV_END\n', '\n', source, flags=re.S)+ if '# ADOM_LIVE_ROUTING_DEV' in source:+ raise ValueError('Old development overlay found; restore server.py from its backup before upgrading')+ def marked(text):+ return '# ADOM_LIVE_ROUTING_DEV_BEGIN\n' + text + '\n# ADOM_LIVE_ROUTING_DEV_END'+ source = after_assignment(source, '_VERB_CATALOG', marked('_VERB_CATALOG.update(' + repr(catalog) + ')'))+ addition = ('from handlers.live_routing import handle_routing_state, handle_route_net, handle_remove_route, handle_routing_validate\n'+ 'from handlers.route import handle_board_pads, handle_add_track, handle_add_via, handle_route\n'+ 'COMMAND_HANDLERS.update({' + ', '.join(repr(n) + ': handle_' + n for n in NAMES) + '})')+ source = after_assignment(source, 'COMMAND_HANDLERS', marked(addition))+ source = after_assignment(source, '_NO_GUI', marked('_NO_GUI.update(' + repr(set(NAMES)) + ')'))+ compile(source, str(cache / 'server.py'), 'exec')+ contents['server.py'] = source.encode('utf-8')+ manifest = json.loads((cache / 'bridge.json').read_text())+ for name in NAMES:+ if 'kicad_' + name not in manifest['verbs']:+ manifest['verbs'].append('kicad_' + name)+ manifest['version'] = '0.10.0-dev.per-net.1'+ contents['bridge.json'] = (json.dumps(manifest, indent=2) + '\n').encode()+ contents['BRIDGE_VERSION'] = manifest['version'].encode()+ contents['.bridge-version'] = manifest['version'].encode()+ backup = cache.parent / ('kicad-routing-backup-' + datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S%f'))+ backup.mkdir()+ originals = {}+ for name in contents:+ original = cache / name+ originals[name] = original.read_bytes() if original.is_file() else None+ if originals[name] is not None:+ target = backup / name+ target.parent.mkdir(parents=True, exist_ok=True)+ target.write_bytes(originals[name])+ try:+ for name, data in contents.items():+ target = cache / name+ target.parent.mkdir(parents=True, exist_ok=True)+ target.write_bytes(data)+ except Exception:+ for name, data in originals.items():+ if data is None:+ (cache / name).unlink(missing_ok=True)+ else:+ (cache / name).write_bytes(data)+ raise+ result = {'installed': str(cache), 'backup': str(backup), 'version': manifest['version']}+ print(json.dumps(result))+ return result++if __name__ == '__main__':+ install(*sys.argv[1:])
tools/release_files.json+39−2@@ -31,6 +31,12 @@ "handlers/library_browser.py", "handlers/make_part_project.py", "handlers/netlist.py",+ "handlers/win_paths.py",+ "handlers/pcm.py",+ "handlers/foreground_guard.py",+ "handlers/kicad_log.py",+ "handlers/kicad_parse_check.py",+ "handlers/kicad_windows.py", "handlers/open_files.py", "handlers/open_footprint_editor.py", "handlers/open_symbol_editor.py",@@ -71,6 +77,37 @@ "start.bat", "start.sh", "templates/blank-board.kicad_pcb",- "uninstall.sh"+ "uninstall.sh",+ "resources/demo-audio/prepare.mp3",+ "resources/demo-audio/schematic.mp3",+ "resources/demo-audio/board.mp3",+ "resources/demo-audio/board3d.mp3",+ "resources/demo-audio/symbol.mp3",+ "resources/demo-audio/footprint.mp3",+ "resources/demo-audio/footprint3d.mp3",+ "resources/demo-audio/done.mp3",+ "resources/demo-audio/manifest.json",+ "handlers/design_rules.py",+ "handlers/list_symbols.py",+ "handlers/demo_motion.py",+ "resources/demo-audio/symbol-a.mp3",+ "resources/demo-audio/symbol-b.mp3",+ "resources/demo-audio/footprint-a.mp3",+ "resources/demo-audio/footprint-b.mp3",+ "resources/demo-audio/footprint3d-a.mp3",+ "resources/demo-audio/footprint3d-b.mp3",+ "resources/demo-audio/schematic-a.mp3",+ "resources/demo-audio/schematic-b.mp3",+ "resources/demo-audio/board-a.mp3",+ "resources/demo-audio/board-b.mp3",+ "resources/demo-audio/board3d-a.mp3",+ "resources/demo-audio/board3d-b.mp3",+ "handlers/live_routing.py",+ "handlers/route.py",+ "requirements-routing.txt",+ "handlers/ipc_api.py"+ ],+ "dirs": [+ "routing_deps" ]-}+}
tools/vendor_routing_deps.pyadded+51@@ -0,0 +1,51 @@+"""Rebuild routing_deps/ (the vendored IPC client for the live routing verbs).++Downloads Windows x64 wheels for requirements-routing.txt, one tree per CPython+ABI the bridge may run under (start.bat picks system Python, else KiCad's), and+unpacks them under routing_deps/cp<maj><min>. The tree ships in the release zip+(tools/release_files.json "dirs") and is deliberately not pushed to the page repo.++ python3 tools/vendor_routing_deps.py # cp311 cp312 cp313+ python3 tools/vendor_routing_deps.py 3.12 # one ABI+"""+import shutil+import subprocess+import sys+import tempfile+import zipfile+from pathlib import Path++ROOT = Path(__file__).resolve().parents[1]+OUT = ROOT / "routing_deps"+README = """Vendored Python dependencies for the live routing verbs (requirements-routing.txt),+unpacked from Windows x64 wheels, one tree per CPython ABI: cp311, cp312, cp313.+handlers/live_routing.py appends routing_deps/cp<major><minor> to sys.path when it exists.+Other platforms: pip install -r requirements-routing.txt into the bridge's Python.+Rebuild with tools/vendor_routing_deps.py; never edit by hand.+"""+++def main():+ versions = sys.argv[1:] or ["3.11", "3.12", "3.13"]+ if OUT.exists():+ shutil.rmtree(OUT)+ OUT.mkdir()+ (OUT / "README.txt").write_text(README)+ for ver in versions:+ tag = "cp" + ver.replace(".", "")+ target = OUT / tag+ target.mkdir()+ with tempfile.TemporaryDirectory() as tmp:+ subprocess.run([sys.executable, "-m", "pip", "download", "-r", str(ROOT / "requirements-routing.txt"),+ "--platform", "win_amd64", "--python-version", ver, "--implementation", "cp",+ "--only-binary=:all:", "-d", tmp, "-q"], check=True)+ wheels = sorted(Path(tmp).glob("*.whl"))+ for wheel in wheels:+ with zipfile.ZipFile(wheel) as z:+ z.extractall(target)+ print(f"{tag}: {len(wheels)} wheels -> {target}")+ print("done:", OUT)+++if __name__ == "__main__":+ main()