Files Branches

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)