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.
Comparing master ← fix/live-routing-drc-completeness
Changes on fix/live-routing-drc-completeness that are not yet on master (three-dot, from the merge base).
5 files changed, 828 insertions(+), 5 deletions(-)
demo/routing/README.md+6−2@@ -23,8 +23,8 @@ From bridge 0.9.340 the routing verbs and their IPC client (`routing_deps/`, Win 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.+1. Save existing work and close KiCad before changing its preferences. A running KiCad can overwrite the IPC setting when it exits.+2. `kicad_ipc_api {"enable":true}` writes `api.enable_server` (backup kept). Open the fixture board 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@@ -85,3 +85,7 @@ The companion ab patch also passed `cargo check --locked` against current 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.++## Snapshot validation limits++The DRC-completeness follow-up refills zones on the disposable snapshot, preserving the live board and source file. Validation reports `zonesRefilled:true`. Project and custom rules are copied alongside the snapshot. If an unconnected list may be capped, its count is labeled a lower bound. If a violation list may be capped, route validation rejects the comparison with `drc_incomplete` before changing copper. These checks do not provide electrical sign-off.
demo/routing/evidence/winvm-2026-09-07.jsonadded+749@@ -0,0 +1,749 @@+{+ "date": "2026-09-07",+ "host": "winvm",+ "abVersion": "2.1.68",+ "kicadVersion": "10.0.6",+ "bridgeVersion": "0.9.340",+ "releaseSha256": "84c6b60b8cc484c502e2cc78274c9016957f0aeb9f5a34ec82b83b89f7dce68f",+ "scope": "One-net live IPC acceptance on disposable six-net fixture, not whole-board routing or electrical sign-off",+ "checks": {+ "dryRun": true,+ "staleRevisionRejected": true,+ "wrongNetRejected": true,+ "liveCommit": true,+ "nativeUndo": true,+ "nativeRedo": true,+ "removeRoute": true,+ "returnedToUnrouted": true+ },+ "records": [+ {+ "verb": "kicad_routing_state",+ "args": {+ "filePath": "C:/Users/azureuser/Documents/adom-kicad-verbtest/live/release-check-20260907.kicad_pcb"+ },+ "response": {+ "success": true,+ "status": "ok",+ "error": "",+ "bridgeVersion": "0.9.340",+ "boardPath": "C:/Users/azureuser/Documents/adom-kicad-verbtest/live/release-check-20260907.kicad_pcb",+ "revision": "d04187168f99a61592bba5f5bd17a4eaf3ab1a2e05670f068e1870789760c54f",+ "source": "live-editor",+ "netsRemaining": [+ "NET_1",+ "NET_2",+ "NET_3",+ "NET_4",+ "NET_5",+ "NET_6"+ ],+ "segmentCount": 0,+ "viaCount": 0+ }+ },+ {+ "verb": "kicad_route_net",+ "args": {+ "filePath": "C:/Users/azureuser/Documents/adom-kicad-verbtest/live/release-check-20260907.kicad_pcb",+ "expectedRevision": "d04187168f99a61592bba5f5bd17a4eaf3ab1a2e05670f068e1870789760c54f",+ "net": "NET_1",+ "width": 0.25,+ "points": [+ "J1.1",+ [+ 112,+ 85+ ],+ [+ 138,+ 85+ ],+ "J2.1"+ ],+ "dryRun": true+ },+ "response": {+ "success": true,+ "status": "ok",+ "error": "",+ "bridgeVersion": "0.9.340",+ "revision": "d04187168f99a61592bba5f5bd17a4eaf3ab1a2e05670f068e1870789760c54f",+ "dryRun": true,+ "drc": {+ "clean": false,+ "errors": 0,+ "projectRulesCopied": [],+ "source": "live-editor-snapshot",+ "unconnected": 6,+ "unconnectedItems": [+ {+ "description": "Missing connection between items",+ "items": [+ {+ "description": "PTH pad 6 [NET_6] of J1",+ "pos": {+ "x": 105.0,+ "y": 110.0+ },+ "uuid": "3d334398-b224-4f80-b176-09d31bce8458"+ },+ {+ "description": "PTH pad 1 [NET_6] of J3",+ "pos": {+ "x": 125.0,+ "y": 116.0+ },+ "uuid": "e6ff0f60-ddff-4083-9082-849ebf06f759"+ }+ ],+ "severity": "error",+ "type": "unconnected_items"+ },+ {+ "description": "Missing connection between items",+ "items": [+ {+ "description": "PTH pad 1 [NET_6] of J3",+ "pos": {+ "x": 125.0,+ "y": 116.0+ },+ "uuid": "e6ff0f60-ddff-4083-9082-849ebf06f759"+ },+ {+ "description": "PTH pad 6 [NET_6] of J2",+ "pos": {+ "x": 145.0,+ "y": 110.0+ },+ "uuid": "522a7227-4339-48c3-b477-dc9f2bd2d1a1"+ }+ ],+ "severity": "error",+ "type": "unconnected_items"+ },+ {+ "description": "Missing connection between items",+ "items": [+ {+ "description": "PTH pad 2 [NET_2] of J1",+ "pos": {+ "x": 105.0,+ "y": 90.0+ },+ "uuid": "e7dd8e25-b62f-44df-aaf8-2aedee11818a"+ },+ {+ "description": "PTH pad 2 [NET_2] of J2",+ "pos": {+ "x": 145.0,+ "y": 90.0+ },+ "uuid": "1907c87e-19d3-4e00-b9d2-cd8b568cec43"+ }+ ],+ "severity": "error",+ "type": "unconnected_items"+ },+ {+ "description": "Missing connection between items",+ "items": [+ {+ "description": "PTH pad 3 [NET_3] of J1",+ "pos": {+ "x": 105.0,+ "y": 95.0+ },+ "uuid": "8504368c-eb14-4118-962a-346d30cf1ba2"+ },+ {+ "description": "PTH pad 3 [NET_3] of J2",+ "pos": {+ "x": 145.0,+ "y": 95.0+ },+ "uuid": "bdc606a6-5234-4d68-9b97-af444f1f0616"+ }+ ],+ "severity": "error",+ "type": "unconnected_items"+ },+ {+ "description": "Missing connection between items",+ "items": [+ {+ "description": "PTH pad 4 [NET_4] of J1",+ "pos": {+ "x": 105.0,+ "y": 100.0+ },+ "uuid": "cf8bf00a-91a0-43db-bd10-2186c3479dce"+ },+ {+ "description": "PTH pad 4 [NET_4] of J2",+ "pos": {+ "x": 145.0,+ "y": 100.0+ },+ "uuid": "c588684d-e2e9-455a-8b41-7f9a6c0a4a66"+ }+ ],+ "severity": "error",+ "type": "unconnected_items"+ },+ {+ "description": "Missing connection between items",+ "items": [+ {+ "description": "PTH pad 5 [NET_5] of J1",+ "pos": {+ "x": 105.0,+ "y": 105.0+ },+ "uuid": "3b59115d-c3e1-4cb8-bbca-93967a13150d"+ },+ {+ "description": "PTH pad 5 [NET_5] of J2",+ "pos": {+ "x": 145.0,+ "y": 105.0+ },+ "uuid": "e38c53dc-7dd6-4461-a573-266d7c76033f"+ }+ ],+ "severity": "error",+ "type": "unconnected_items"+ }+ ],+ "violations": [+ {+ "description": "The current configuration does not include the footprint library 'RoutingFixture'",+ "items": [+ {+ "description": "Footprint J3",+ "pos": {+ "x": 125.0,+ "y": 116.0+ },+ "uuid": "1835b937-b88a-47d8-8f8b-34ab9b51ec3b"+ }+ ],+ "severity": "warning",+ "type": "lib_footprint_issues"+ },+ {+ "description": "The current configuration does not include the footprint library 'RoutingFixture'",+ "items": [+ {+ "description": "Footprint J1",+ "pos": {+ "x": 105.0,+ "y": 85.0+ },+ "uuid": "237d23c0-9604-4f92-ae39-b797e8c7a3a0"+ }+ ],+ "severity": "warning",+ "type": "lib_footprint_issues"+ },+ {+ "description": "The current configuration does not include the footprint library 'RoutingFixture'",+ "items": [+ {+ "description": "Footprint J2",+ "pos": {+ "x": 145.0,+ "y": 85.0+ },+ "uuid": "c6a7f067-f59d-410c-aac9-86933040c91b"+ }+ ],+ "severity": "warning",+ "type": "lib_footprint_issues"+ }+ ],+ "warnings": 3+ },+ "segmentCount": 3,+ "viaCount": 0+ }+ },+ {+ "verb": "kicad_route_net",+ "args": {+ "filePath": "C:/Users/azureuser/Documents/adom-kicad-verbtest/live/release-check-20260907.kicad_pcb",+ "expectedRevision": "wrong-revision",+ "net": "NET_1",+ "width": 0.25,+ "points": [+ "J1.1",+ [+ 112,+ 85+ ],+ [+ 138,+ 85+ ],+ "J2.1"+ ]+ },+ "response": {+ "success": false,+ "status": "error",+ "error": "expectedRevision must match the current live board",+ "errorCode": "stale_board",+ "bridgeVersion": "0.9.340",+ "mutated": false+ }+ },+ {+ "verb": "kicad_route_net",+ "args": {+ "filePath": "C:/Users/azureuser/Documents/adom-kicad-verbtest/live/release-check-20260907.kicad_pcb",+ "expectedRevision": "d04187168f99a61592bba5f5bd17a4eaf3ab1a2e05670f068e1870789760c54f",+ "net": "NET_2",+ "width": 0.25,+ "points": [+ "J1.1",+ [+ 112,+ 85+ ],+ [+ 138,+ 85+ ],+ "J2.1"+ ],+ "dryRun": true+ },+ "response": {+ "success": false,+ "status": "error",+ "error": "Pad J1.1 is not on NET_2",+ "errorCode": "net_mismatch",+ "bridgeVersion": "0.9.340"+ }+ },+ {+ "verb": "kicad_route_net",+ "args": {+ "filePath": "C:/Users/azureuser/Documents/adom-kicad-verbtest/live/release-check-20260907.kicad_pcb",+ "expectedRevision": "d04187168f99a61592bba5f5bd17a4eaf3ab1a2e05670f068e1870789760c54f",+ "net": "NET_1",+ "width": 0.25,+ "points": [+ "J1.1",+ [+ 112,+ 85+ ],+ [+ 138,+ 85+ ],+ "J2.1"+ ]+ },+ "response": {+ "success": true,+ "status": "ok",+ "error": "",+ "bridgeVersion": "0.9.340",+ "revision": "5d6e32e2b17cf9bad0c180370d3bf9e1984e0f1a615ac949af335e8d160ace39",+ "mutated": true,+ "itemIds": [+ "cbdb2efa-0490-4d42-8c3b-f3e250953ed5",+ "db30ecbb-8f85-4098-b92c-a77d40e4f597",+ "0e891439-017e-4553-9a10-619187179101"+ ],+ "undoSteps": 1,+ "source": "live-editor",+ "drc": {+ "clean": false,+ "errors": 0,+ "projectRulesCopied": [],+ "source": "live-editor-snapshot",+ "unconnected": 6,+ "unconnectedItems": [+ {+ "description": "Missing connection between items",+ "items": [+ {+ "description": "PTH pad 6 [NET_6] of J1",+ "pos": {+ "x": 105.0,+ "y": 110.0+ },+ "uuid": "3d334398-b224-4f80-b176-09d31bce8458"+ },+ {+ "description": "PTH pad 1 [NET_6] of J3",+ "pos": {+ "x": 125.0,+ "y": 116.0+ },+ "uuid": "e6ff0f60-ddff-4083-9082-849ebf06f759"+ }+ ],+ "severity": "error",+ "type": "unconnected_items"+ },+ {+ "description": "Missing connection between items",+ "items": [+ {+ "description": "PTH pad 1 [NET_6] of J3",+ "pos": {+ "x": 125.0,+ "y": 116.0+ },+ "uuid": "e6ff0f60-ddff-4083-9082-849ebf06f759"+ },+ {+ "description": "PTH pad 6 [NET_6] of J2",+ "pos": {+ "x": 145.0,+ "y": 110.0+ },+ "uuid": "522a7227-4339-48c3-b477-dc9f2bd2d1a1"+ }+ ],+ "severity": "error",+ "type": "unconnected_items"+ },+ {+ "description": "Missing connection between items",+ "items": [+ {+ "description": "PTH pad 2 [NET_2] of J1",+ "pos": {+ "x": 105.0,+ "y": 90.0+ },+ "uuid": "e7dd8e25-b62f-44df-aaf8-2aedee11818a"+ },+ {+ "description": "PTH pad 2 [NET_2] of J2",+ "pos": {+ "x": 145.0,+ "y": 90.0+ },+ "uuid": "1907c87e-19d3-4e00-b9d2-cd8b568cec43"+ }+ ],+ "severity": "error",+ "type": "unconnected_items"+ },+ {+ "description": "Missing connection between items",+ "items": [+ {+ "description": "PTH pad 3 [NET_3] of J1",+ "pos": {+ "x": 105.0,+ "y": 95.0+ },+ "uuid": "8504368c-eb14-4118-962a-346d30cf1ba2"+ },+ {+ "description": "PTH pad 3 [NET_3] of J2",+ "pos": {+ "x": 145.0,+ "y": 95.0+ },+ "uuid": "bdc606a6-5234-4d68-9b97-af444f1f0616"+ }+ ],+ "severity": "error",+ "type": "unconnected_items"+ },+ {+ "description": "Missing connection between items",+ "items": [+ {+ "description": "PTH pad 4 [NET_4] of J1",+ "pos": {+ "x": 105.0,+ "y": 100.0+ },+ "uuid": "cf8bf00a-91a0-43db-bd10-2186c3479dce"+ },+ {+ "description": "PTH pad 4 [NET_4] of J2",+ "pos": {+ "x": 145.0,+ "y": 100.0+ },+ "uuid": "c588684d-e2e9-455a-8b41-7f9a6c0a4a66"+ }+ ],+ "severity": "error",+ "type": "unconnected_items"+ },+ {+ "description": "Missing connection between items",+ "items": [+ {+ "description": "PTH pad 5 [NET_5] of J1",+ "pos": {+ "x": 105.0,+ "y": 105.0+ },+ "uuid": "3b59115d-c3e1-4cb8-bbca-93967a13150d"+ },+ {+ "description": "PTH pad 5 [NET_5] of J2",+ "pos": {+ "x": 145.0,+ "y": 105.0+ },+ "uuid": "e38c53dc-7dd6-4461-a573-266d7c76033f"+ }+ ],+ "severity": "error",+ "type": "unconnected_items"+ }+ ],+ "violations": [+ {+ "description": "The current configuration does not include the footprint library 'RoutingFixture'",+ "items": [+ {+ "description": "Footprint J3",+ "pos": {+ "x": 125.0,+ "y": 116.0+ },+ "uuid": "1835b937-b88a-47d8-8f8b-34ab9b51ec3b"+ }+ ],+ "severity": "warning",+ "type": "lib_footprint_issues"+ },+ {+ "description": "The current configuration does not include the footprint library 'RoutingFixture'",+ "items": [+ {+ "description": "Footprint J1",+ "pos": {+ "x": 105.0,+ "y": 85.0+ },+ "uuid": "237d23c0-9604-4f92-ae39-b797e8c7a3a0"+ }+ ],+ "severity": "warning",+ "type": "lib_footprint_issues"+ },+ {+ "description": "The current configuration does not include the footprint library 'RoutingFixture'",+ "items": [+ {+ "description": "Footprint J2",+ "pos": {+ "x": 145.0,+ "y": 85.0+ },+ "uuid": "c6a7f067-f59d-410c-aac9-86933040c91b"+ }+ ],+ "severity": "warning",+ "type": "lib_footprint_issues"+ }+ ],+ "warnings": 3+ },+ "segmentCount": 3,+ "viaCount": 0+ }+ },+ {+ "verb": "kicad_routing_state",+ "args": {+ "filePath": "C:/Users/azureuser/Documents/adom-kicad-verbtest/live/release-check-20260907.kicad_pcb"+ },+ "response": {+ "success": true,+ "status": "ok",+ "error": "",+ "bridgeVersion": "0.9.340",+ "boardPath": "C:/Users/azureuser/Documents/adom-kicad-verbtest/live/release-check-20260907.kicad_pcb",+ "revision": "5d6e32e2b17cf9bad0c180370d3bf9e1984e0f1a615ac949af335e8d160ace39",+ "source": "live-editor",+ "netsRemaining": [+ "NET_2",+ "NET_3",+ "NET_4",+ "NET_5",+ "NET_6"+ ],+ "segmentCount": 3,+ "viaCount": 0+ }+ },+ {+ "verb": "kicad_routing_validate",+ "args": {+ "filePath": "C:/Users/azureuser/Documents/adom-kicad-verbtest/live/release-check-20260907.kicad_pcb"+ },+ "response": {+ "success": true,+ "status": "ok",+ "error": "",+ "bridgeVersion": "0.9.340",+ "revision": "5d6e32e2b17cf9bad0c180370d3bf9e1984e0f1a615ac949af335e8d160ace39",+ "errors": 0,+ "warnings": 3,+ "unconnected": 6,+ "stale": false,+ "source": "live-editor-snapshot"+ }+ },+ {+ "verb": "kicad_send_key",+ "args": {+ "hwnd": 591366,+ "key": "ctrl+z"+ },+ "response": {+ "success": true,+ "status": "ok",+ "error": "",+ "bridgeVersion": "0.9.340",+ "data": {+ "cmdId": 20444,+ "foreground": false,+ "hwnd": 591366,+ "menuItem": "Undo",+ "method": "menu-command",+ "title": "*release-check-20260907 \u2014 PCB Editor"+ }+ }+ },+ {+ "verb": "kicad_routing_state",+ "args": {+ "filePath": "C:/Users/azureuser/Documents/adom-kicad-verbtest/live/release-check-20260907.kicad_pcb"+ },+ "response": {+ "success": true,+ "status": "ok",+ "error": "",+ "bridgeVersion": "0.9.340",+ "boardPath": "C:/Users/azureuser/Documents/adom-kicad-verbtest/live/release-check-20260907.kicad_pcb",+ "revision": "d04187168f99a61592bba5f5bd17a4eaf3ab1a2e05670f068e1870789760c54f",+ "source": "live-editor",+ "netsRemaining": [+ "NET_1",+ "NET_2",+ "NET_3",+ "NET_4",+ "NET_5",+ "NET_6"+ ],+ "segmentCount": 0,+ "viaCount": 0+ }+ },+ {+ "verb": "kicad_send_key",+ "args": {+ "hwnd": 591366,+ "key": "ctrl+y"+ },+ "response": {+ "success": true,+ "status": "ok",+ "error": "",+ "bridgeVersion": "0.9.340",+ "data": {+ "cmdId": 20445,+ "foreground": false,+ "hwnd": 591366,+ "menuItem": "Redo",+ "method": "menu-command",+ "title": "*release-check-20260907 \u2014 PCB Editor"+ }+ }+ },+ {+ "verb": "kicad_routing_state",+ "args": {+ "filePath": "C:/Users/azureuser/Documents/adom-kicad-verbtest/live/release-check-20260907.kicad_pcb"+ },+ "response": {+ "success": true,+ "status": "ok",+ "error": "",+ "bridgeVersion": "0.9.340",+ "boardPath": "C:/Users/azureuser/Documents/adom-kicad-verbtest/live/release-check-20260907.kicad_pcb",+ "revision": "5d6e32e2b17cf9bad0c180370d3bf9e1984e0f1a615ac949af335e8d160ace39",+ "source": "live-editor",+ "netsRemaining": [+ "NET_2",+ "NET_3",+ "NET_4",+ "NET_5",+ "NET_6"+ ],+ "segmentCount": 3,+ "viaCount": 0+ }+ },+ {+ "verb": "kicad_remove_route",+ "args": {+ "filePath": "C:/Users/azureuser/Documents/adom-kicad-verbtest/live/release-check-20260907.kicad_pcb",+ "expectedRevision": "5d6e32e2b17cf9bad0c180370d3bf9e1984e0f1a615ac949af335e8d160ace39",+ "itemIds": [+ "cbdb2efa-0490-4d42-8c3b-f3e250953ed5",+ "db30ecbb-8f85-4098-b92c-a77d40e4f597",+ "0e891439-017e-4553-9a10-619187179101"+ ]+ },+ "response": {+ "success": true,+ "status": "ok",+ "error": "",+ "bridgeVersion": "0.9.340",+ "revision": "d04187168f99a61592bba5f5bd17a4eaf3ab1a2e05670f068e1870789760c54f",+ "mutated": true,+ "removedIds": [+ "cbdb2efa-0490-4d42-8c3b-f3e250953ed5",+ "db30ecbb-8f85-4098-b92c-a77d40e4f597",+ "0e891439-017e-4553-9a10-619187179101"+ ],+ "verified": true,+ "undoSteps": 1+ }+ },+ {+ "verb": "kicad_routing_state",+ "args": {+ "filePath": "C:/Users/azureuser/Documents/adom-kicad-verbtest/live/release-check-20260907.kicad_pcb"+ },+ "response": {+ "success": true,+ "status": "ok",+ "error": "",+ "bridgeVersion": "0.9.340",+ "boardPath": "C:/Users/azureuser/Documents/adom-kicad-verbtest/live/release-check-20260907.kicad_pcb",+ "revision": "d04187168f99a61592bba5f5bd17a4eaf3ab1a2e05670f068e1870789760c54f",+ "source": "live-editor",+ "netsRemaining": [+ "NET_1",+ "NET_2",+ "NET_3",+ "NET_4",+ "NET_5",+ "NET_6"+ ],+ "segmentCount": 0,+ "viaCount": 0+ }+ }+ ]+}
handlers/live_routing.py+20−2@@ -174,6 +174,18 @@ def _plan(data, args): return {"netName": netname, "segments": segments, "vias": vias, "blocks": blocks} +def _report_limits(violations, unconnected):+ # KiCad 10.0.3 DRC_ENGINE: 199 per type, extended to 499 for+ # clearance and unconnected items. At the boundary completeness is unknown.+ counts = Counter(v.get("type") for v in violations)+ capped = sorted(str(kind) for kind, count in counts.items()+ if count >= (499 if kind == "clearance" else 199))+ return {"unconnectedReportMayBeTruncated": len(unconnected) >= 499,+ "unconnectedCountIsLowerBound": len(unconnected) >= 499,+ "violationReportMayBeTruncated": bool(capped),+ "possiblyTruncatedViolationTypes": capped}++ def _drc(info, args, text): exe = info.get("kicad_cli_exe") if not exe or not Path(exe).is_file():@@ -191,8 +203,10 @@ def _drc(info, args, text): shutil.copy2(sibling, candidate.with_suffix(suffix)) copied.append(str(sibling)) report = Path(tmp) / "drc.json"+ # Copper edits invalidate cached zone fills. Refill the disposable+ # snapshot so connectivity reflects the proposed copper, not old pours. proc = subprocess.run([exe, "pcb", "drc", "--format", "json", "--severity-all",- "--all-track-errors", "--output", str(report), str(candidate)],+ "--all-track-errors", "--refill-zones", "--output", str(report), str(candidate)], capture_output=True, text=True, timeout=90) if proc.returncode or not report.is_file(): raise RoutingError("drc_failed", proc.stderr or proc.stdout or "No DRC report")@@ -204,7 +218,8 @@ def _drc(info, args, text): "unconnected": len(unconnected), "violations": violations, "unconnectedItems": unconnected, "clean": not violations and not unconnected,- "source": "live-editor-snapshot", "projectRulesCopied": copied}+ "source": "live-editor-snapshot", "projectRulesCopied": copied,+ "zonesRefilled": True, **_report_limits(violations, unconnected)} def _fingerprint(v):@@ -213,6 +228,9 @@ def _fingerprint(v): def _new_errors(before, after):+ if any(r.get("violationReportMayBeTruncated") for r in (before, after)):+ raise RoutingError("drc_incomplete", "DRC violation lists may be capped; cannot certify the error delta. No live copper was changed",+ mutated=False) existing = Counter(_fingerprint(v) for v in before["violations"] if v.get("severity") == "error") added = [] for v in after["violations"]:
tests/test_live_routing.py+46@@ -19,6 +19,52 @@ def board(): 'pads':[{'number':'1','x':0,'y':0,'net_name':'GND','layers':['B.Cu'],'type':'smd'}]}]} +class DrcSnapshot(unittest.TestCase):+ def test_capped_opens_are_lower_bound_without_blocking_error_comparison(self):+ limits = live._report_limits([], [{}] * 499)+ self.assertTrue(limits['unconnectedCountIsLowerBound'])+ self.assertFalse(limits['violationReportMayBeTruncated'])+ self.assertEqual(live._new_errors({'violations': [], **limits}, {'violations': [], **limits}), [])+ self.assertFalse(live._report_limits([], [{}] * 498)['unconnectedCountIsLowerBound'])++ def test_per_type_caps_block_error_delta_even_when_reports_match(self):+ for kind, cap in [('clearance', 499), ('shorting_items', 199)]:+ v = {'type': kind, 'severity': 'error', 'items': []}+ self.assertFalse(live._report_limits([v] * (cap - 1), [])['violationReportMayBeTruncated'])+ report = {'violations': [v] * cap, **live._report_limits([v] * cap, [])}+ with self.assertRaises(live.RoutingError) as cm:+ live._new_errors(report, report)+ self.assertEqual(cm.exception.code, 'drc_incomplete')+ self.assertFalse(cm.exception.detail['mutated'])++ def test_refills_snapshot_with_project_rules_without_changing_source(self):+ import json+ from types import SimpleNamespace+ with tempfile.TemporaryDirectory() as tmp:+ source = Path(tmp) / 'board.kicad_pcb'+ source.write_text('original board')+ source.with_suffix('.kicad_pro').write_text('project rules')+ source.with_suffix('.kicad_dru').write_text('custom rules')+ exe = Path(tmp) / 'kicad-cli'+ exe.touch()+ def run(command, **kwargs):+ candidate = Path(command[-1])+ self.assertNotEqual(candidate, source)+ self.assertEqual(candidate.read_text(), 'live snapshot')+ self.assertEqual(candidate.with_suffix('.kicad_pro').read_text(), 'project rules')+ self.assertEqual(candidate.with_suffix('.kicad_dru').read_text(), 'custom rules')+ self.assertIn('--refill-zones', command)+ Path(command[command.index('--output') + 1]).write_text(json.dumps({+ 'violations': [], 'unconnected_items': [{'items': []}]}))+ return SimpleNamespace(returncode=0, stderr='', stdout='')+ with patch.object(live.subprocess, 'run', side_effect=run):+ result = live._drc({'kicad_cli_exe': str(exe)}, {'filePath': str(source)}, 'live snapshot')+ self.assertTrue(result['zonesRefilled'])+ self.assertEqual(result['unconnected'], 1)+ self.assertFalse(result['clean'])+ self.assertEqual(source.read_text(), 'original board')++ 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'}]})
tests/test_routing_ipc.py+7−1@@ -140,7 +140,13 @@ class OfflineTests(unittest.TestCase): def test_atomic_file_edit_and_open_board_refusal(self): with tempfile.TemporaryDirectory() as d:- path = make(d); before = path.read_bytes()+ # A user can have the demonstration's default "live-routing" board+ # open on the Windows test host. Give this disposable fixture a+ # unique title so the real open-editor guard does not match it.+ fixture = make(d)+ path = fixture.with_name(Path(d).name + '-offline-test.kicad_pcb')+ fixture.rename(path)+ 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)