From 39af32e08a7e6536f2da039458541491dfdcf033 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Thu, 28 May 2026 18:03:59 +0800 Subject: [PATCH] docs: plan freecad electrical auto routing --- ...6-05-28-freecad-electrical-auto-routing.md | 787 ++++++++++++++++++ 1 file changed, 787 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-28-freecad-electrical-auto-routing.md diff --git a/docs/superpowers/plans/2026-05-28-freecad-electrical-auto-routing.md b/docs/superpowers/plans/2026-05-28-freecad-electrical-auto-routing.md new file mode 100644 index 0000000..1d5a140 --- /dev/null +++ b/docs/superpowers/plans/2026-05-28-freecad-electrical-auto-routing.md @@ -0,0 +1,787 @@ +# FreeCAD Electrical Auto Routing Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Finish the first production-ready FreeCAD electrical auto-routing loop from QET `wires[]` tasks to saved `AutoSuggested` 3D wires with actionable diagnostics. + +**Architecture:** Keep FreeCAD as the 3D truth source and QET as the electrical truth source. QET exports richer `wires[]` task metadata; FreeCAD consumes that task list, prepares a route-carrier graph, runs Dijkstra, stores routed wires and diagnostics in `scene.FCStd`, and never writes 3D wire geometry into the database. + +**Tech Stack:** FreeCAD Python API, Qt/C++ QET export service, JSON exchange files, `unittest`, fake FreeCAD test harness, FreeCAD smoke scripts. + +--- + +## Scope Check + +The approved spec touches two repos, but the work is still one coherent feature because QET only supplies task metadata and FreeCAD owns all routing behavior. Treat the QET change as a small upstream contract task. Do not run QET build commands in `D:\code\zwl`; that repo's `AGENTS.md` forbids build, cmake, make, and ninja. + +## File Structure + +- Modify: `D:\code\zwl\sources\FreeCAD\FreeCADExchangeExportService.cpp` + - Responsibility: include `start_instance_id` and `end_instance_id` in exported `wires[]` entries so FreeCAD can bind local template terminals more reliably. +- Modify: `src/Mod/FreeCADExchange/WiringImport.py` + - Responsibility: preserve `wire_style_id`, start/end instance IDs, and endpoint display metadata on `QETWiring_01_Tasks`. +- Modify: `src/Mod/FreeCADExchange/WiringObjects.py` + - Responsibility: provide shared helpers for wire task/routed wire metadata, including route length and diagnostic payload fields. +- Modify: `src/Mod/FreeCADExchange/AutoRouting.py` + - Responsibility: compute route length, carry `wire_style_id`, create diagnostic summaries, and enrich batch reports. +- Modify: `src/Mod/FreeCADExchange/AutoRoutingPanel.py` + - Responsibility: show the improved batch summary without adding extra workflow steps. +- Modify: `tests/python/freecad_exchange_wiring_import_test.py` + - Responsibility: lock `wires[]` task import metadata. +- Modify: `tests/python/freecad_exchange_auto_routing_test.py` + - Responsibility: lock auto-route metadata, diagnostic group output, and one-click reporting. +- Modify: `tests/manual/freecad_auto_routing_smoke.py` + - Responsibility: verify the end-to-end FreeCAD document can save, reopen, and retain auto routes. + +## Task 1: Add QET wire endpoint instance IDs to `wires[]` + +**Files:** +- Modify: `D:\code\zwl\sources\FreeCAD\FreeCADExchangeExportService.cpp:412-477` +- Modify: `D:\code\zwl\sources\FreeCAD\FreeCADExchangeExportService.cpp:928` + +- [ ] **Step 1: Confirm the current export gap** + +Run: + +```powershell +rg -n "start_instance_id|end_instance_id|buildWireObjectsForDiagram" D:\code\zwl\sources\FreeCAD\FreeCADExchangeExportService.cpp +``` + +Expected before this task: `buildWireObjectsForDiagram` exists, but `start_instance_id` and `end_instance_id` are not inserted into `wireObject`. + +- [ ] **Step 2: Change the function signature** + +In `D:\code\zwl\sources\FreeCAD\FreeCADExchangeExportService.cpp`, change the function header from: + +```cpp +QJsonArray buildWireObjectsForDiagram(Diagram *diagram) +``` + +to: + +```cpp +QJsonArray buildWireObjectsForDiagram( + Diagram *diagram, + const QHash &terminalInstanceIds) +``` + +- [ ] **Step 3: Insert endpoint instance IDs** + +Inside the loop that builds `wireObject`, immediately after the existing terminal UUID inserts, add: + +```cpp +const QString startInstanceId = terminalInstanceIds.value(startTerminalUuid).trimmed(); +const QString endInstanceId = terminalInstanceIds.value(endTerminalUuid).trimmed(); +wireObject.insert(QStringLiteral("start_instance_id"), startInstanceId); +wireObject.insert(QStringLiteral("end_instance_id"), endInstanceId); +``` + +The surrounding block should read: + +```cpp +wireObject.insert(QStringLiteral("start_element_uuid"), uuidText(startElementUuid)); +wireObject.insert(QStringLiteral("start_terminal_uuid"), uuidText(startTerminalUuid)); +wireObject.insert(QStringLiteral("start_instance_id"), startInstanceId); +wireObject.insert(QStringLiteral("end_element_uuid"), uuidText(endElementUuid)); +wireObject.insert(QStringLiteral("end_terminal_uuid"), uuidText(endTerminalUuid)); +wireObject.insert(QStringLiteral("end_instance_id"), endInstanceId); +``` + +- [ ] **Step 4: Pass the binding map from the exporter** + +Replace: + +```cpp +wiresArray = buildWireObjectsForDiagram(diagram); +``` + +with: + +```cpp +wiresArray = buildWireObjectsForDiagram(diagram, terminalInstanceIds); +``` + +- [ ] **Step 5: Run static verification** + +Run: + +```powershell +rg -n "start_instance_id|end_instance_id|buildWireObjectsForDiagram\\(" D:\code\zwl\sources\FreeCAD\FreeCADExchangeExportService.cpp +``` + +Expected: one function definition with the new parameter, one call that passes `terminalInstanceIds`, and two JSON inserts for `start_instance_id` / `end_instance_id`. + +- [ ] **Step 6: Commit the QET contract change** + +Run in `D:\code\zwl`: + +```powershell +git status --short +git add -- sources/FreeCAD/FreeCADExchangeExportService.cpp +git commit -m "feat: export freecad wire endpoint instances" +``` + +Expected: the commit contains only `sources/FreeCAD/FreeCADExchangeExportService.cpp`. + +## Task 2: Preserve wire style and endpoint metadata on FreeCAD tasks + +**Files:** +- Modify: `src/Mod/FreeCADExchange/WiringImport.py:47-109` +- Modify: `src/Mod/FreeCADExchange/WiringImport.py:145-190` +- Modify: `src/Mod/FreeCADExchange/WiringObjects.py:181-213` +- Test: `tests/python/freecad_exchange_wiring_import_test.py` + +- [ ] **Step 1: Write the failing import test** + +Add this test to `tests/python/freecad_exchange_wiring_import_test.py` inside `WiringImportTest`: + +```python + def test_import_wire_tasks_preserves_auto_routing_metadata(self): + wiring_import = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "wire_label": "W1", + "wire_mark": "N4111", + "wire_mark_is_manual": True, + "wire_style_id": 42, + "net_uuid": "net-1", + "group_uuid": "group-1", + "start_element_uuid": "device-a", + "start_instance_id": "instance-a", + "start_terminal_uuid": "terminal-a", + "start_terminal_display": "A1", + "end_element_uuid": "device-b", + "end_instance_id": "instance-b", + "end_terminal_uuid": "terminal-b", + "end_terminal_display": "B1", + } + ], + } + + report = wiring_import.import_wire_tasks_from_payload(payload, doc) + task = doc.getObject("QETWiring_01_Tasks").Group[0] + + self.assertEqual(1, report["imported_tasks"]) + self.assertEqual("42", task.QetWireStyleId) + self.assertEqual("instance-a", task.QetStartInstanceId) + self.assertEqual("instance-b", task.QetEndInstanceId) + self.assertEqual("A1", task.QetStartTerminalDisplay) + self.assertEqual("B1", task.QetEndTerminalDisplay) +``` + +- [ ] **Step 2: Run the test and verify it fails** + +Run: + +```powershell +D:\FreeCAD-1.1.1\bin\python.exe -m unittest tests.python.freecad_exchange_wiring_import_test.WiringImportTest.test_import_wire_tasks_preserves_auto_routing_metadata +``` + +Expected: FAIL because `QetWireStyleId` is not set. + +- [ ] **Step 3: Extend normalized wire entries** + +In `src/Mod/FreeCADExchange/WiringImport.py`, add this helper near `_bool_value`: + +```python +def _int_text_value(item, field_name): + value = item.get(field_name, "") + if value is None: + return "" + try: + return str(int(value)).strip() + except Exception: + return str(value).strip() +``` + +Then add this field in `_normalize_wire_entry`: + +```python +"wire_style_id": _int_text_value(item, "wire_style_id"), +``` + +- [ ] **Step 4: Persist the style ID on tasks** + +In `_set_task_extra_properties`, add: + +```python +_ensure_string_property(task, "QetWireStyleId", entry["wire_style_id"]) +``` + +- [ ] **Step 5: Run the import test again** + +Run: + +```powershell +D:\FreeCAD-1.1.1\bin\python.exe -m unittest tests.python.freecad_exchange_wiring_import_test.WiringImportTest.test_import_wire_tasks_preserves_auto_routing_metadata +``` + +Expected: PASS. + +- [ ] **Step 6: Run the wiring import test file** + +Run: + +```powershell +D:\FreeCAD-1.1.1\bin\python.exe -m unittest tests.python.freecad_exchange_wiring_import_test +``` + +Expected: all tests in the file pass. + +- [ ] **Step 7: Commit** + +Run: + +```powershell +git add src/Mod/FreeCADExchange/WiringImport.py tests/python/freecad_exchange_wiring_import_test.py +git commit -m "feat: preserve freecad wire task metadata" +``` + +Expected: the commit contains the import code and its test. + +## Task 3: Store auto-route length and wire style diagnostics + +**Files:** +- Modify: `src/Mod/FreeCADExchange/AutoRouting.py:260-300` +- Modify: `src/Mod/FreeCADExchange/AutoRouting.py:860-970` +- Modify: `src/Mod/FreeCADExchange/AutoRouting.py:1169-1190` +- Test: `tests/python/freecad_exchange_auto_routing_test.py` + +- [ ] **Step 1: Write the failing routed-wire metadata test** + +Add this test to `tests/python/freecad_exchange_auto_routing_test.py` inside `AutoRoutingTest`: + +```python + def test_auto_route_stores_length_and_wire_style_diagnostics(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + result = auto_routing.route_between_terminals( + doc, + start, + end, + wire_uuid="wire-1", + wire_label="N4111", + options={"wire_style_id": "42"}, + ) + wire = result["wire"] + payload = json.loads(wire.QetAutoRouteDiagnosticsJson) + + self.assertGreater(float(wire.QetAutoRouteLengthMm), 0.0) + self.assertEqual("42", wire.QetWireStyleId) + self.assertEqual("42", payload["wire_style_id"]) + self.assertGreater(payload["length_mm"], 0.0) +``` + +If the test file does not already import `json`, add: + +```python +import json +``` + +- [ ] **Step 2: Run the new test and verify it fails** + +Run: + +```powershell +D:\FreeCAD-1.1.1\bin\python.exe -m unittest tests.python.freecad_exchange_auto_routing_test.AutoRoutingTest.test_auto_route_stores_length_and_wire_style_diagnostics +``` + +Expected: FAIL because `QetAutoRouteLengthMm` and `QetWireStyleId` are not set on routed wires. + +- [ ] **Step 3: Add route length helpers** + +In `src/Mod/FreeCADExchange/AutoRouting.py`, add near `_point_payload`: + +```python +def _route_length(points): + total = 0.0 + normalized = [_vector(point) for point in points or []] + for index in range(len(normalized) - 1): + total += _distance(normalized[index], normalized[index + 1]) + return total +``` + +- [ ] **Step 4: Add wire style and length to the diagnostic payload** + +Change `_route_payload` to accept `wire_style_id`: + +```python +def _route_payload(route_data, collisions, wire_style_id=""): + points = route_data.get("points", []) + return { + "algorithm": route_data.get("algorithm", ""), + "length_mm": _route_length(points), + "wire_style_id": str(wire_style_id or "").strip(), + "points": [_point_payload(point) for point in points], + "collision_count": len(collisions), + "collisions": collisions, + "network": route_data.get("network", {}), + } +``` + +Change `_set_auto_metadata` to accept and store `wire_style_id`: + +```python +def _set_auto_metadata(wire, route_data, collisions, wire_style_id=""): + length_mm = _route_length(route_data.get("points", [])) + _set_string(wire, "QetAutoRouteAlgorithm", route_data.get("algorithm", ""), "Auto-routing algorithm used for this wire") + _set_string(wire, "QetAutoRouteLengthMm", "{0:.3f}".format(length_mm), "Auto route length in millimeters") + _set_string(wire, "QetWireStyleId", str(wire_style_id or "").strip(), "QET wire style ID") + _set_string( + wire, + "QetAutoRouteDiagnosticsJson", + json.dumps(_route_payload(route_data, collisions, wire_style_id=wire_style_id), ensure_ascii=False), + "Auto-routing diagnostics for this wire", + ) + if route_data.get("network"): + _set_string( + wire, + "QetAutoRouteNetworkJson", + json.dumps(route_data.get("network", {}), ensure_ascii=False), + "Route network metadata used by this wire", + ) +``` + +- [ ] **Step 5: Thread `wire_style_id` through routing** + +Add a parameter to `route_between_terminals`: + +```python +wire_style_id="", +``` + +Before metadata is written, merge explicit parameter and options: + +```python +effective_wire_style_id = str(wire_style_id or opts.get("wire_style_id", "") or "").strip() +``` + +Then replace: + +```python +_set_auto_metadata(wire, route_data, collisions) +``` + +with: + +```python +_set_auto_metadata(wire, route_data, collisions, wire_style_id=effective_wire_style_id) +``` + +- [ ] **Step 6: Pass task style from payload and task objects** + +In `route_all_from_payload`, pass: + +```python +wire_style_id=_wire_item_value(item, "wire_style_id"), +``` + +In `_wire_tasks_payload`, add: + +```python +"wire_style_id": (getattr(task, "QetWireStyleId", "") or "").strip(), +``` + +- [ ] **Step 7: Run the focused test** + +Run: + +```powershell +D:\FreeCAD-1.1.1\bin\python.exe -m unittest tests.python.freecad_exchange_auto_routing_test.AutoRoutingTest.test_auto_route_stores_length_and_wire_style_diagnostics +``` + +Expected: PASS. + +- [ ] **Step 8: Run the auto-routing test file** + +Run: + +```powershell +D:\FreeCAD-1.1.1\bin\python.exe -m unittest tests.python.freecad_exchange_auto_routing_test +``` + +Expected: all tests in the file pass. + +- [ ] **Step 9: Commit** + +Run: + +```powershell +git add src/Mod/FreeCADExchange/AutoRouting.py tests/python/freecad_exchange_auto_routing_test.py +git commit -m "feat: record freecad auto route diagnostics" +``` + +Expected: the commit contains route metadata and tests. + +## Task 4: Write batch diagnostics into `QETWiring_05_Diagnostics` + +**Files:** +- Modify: `src/Mod/FreeCADExchange/AutoRouting.py:1022-1152` +- Modify: `src/Mod/FreeCADExchange/WiringObjects.py:153-156` +- Test: `tests/python/freecad_exchange_auto_routing_test.py` + +- [ ] **Step 1: Write the failing diagnostics group test** + +Add this test to `tests/python/freecad_exchange_auto_routing_test.py` inside `AutoRoutingTest`: + +```python + def test_route_all_writes_diagnostic_object_for_missing_terminal(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-missing", + } + ], + } + + report = auto_routing.route_all_from_payload(doc, payload) + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + + self.assertEqual(1, report["skipped_missing_terminal"]) + self.assertIsNotNone(diagnostic_group) + self.assertEqual(1, len(diagnostic_group.Group)) + diagnostic = diagnostic_group.Group[0] + self.assertEqual("AutoRouteBatch", diagnostic.QetDiagnosticKind) + self.assertIn("terminal-missing", diagnostic.QetDiagnosticJson) +``` + +- [ ] **Step 2: Run the new test and verify it fails** + +Run: + +```powershell +D:\FreeCAD-1.1.1\bin\python.exe -m unittest tests.python.freecad_exchange_auto_routing_test.AutoRoutingTest.test_route_all_writes_diagnostic_object_for_missing_terminal +``` + +Expected: FAIL because no diagnostic object is created. + +- [ ] **Step 3: Add diagnostic helper functions** + +In `src/Mod/FreeCADExchange/AutoRouting.py`, add after `format_route_all_report`: + +```python +def _clear_auto_route_batch_diagnostics(doc): + group = WiringObjects.ensure_diagnostic_group(doc, _project_uuid(doc)) + removed = 0 + for obj in list(getattr(group, "Group", []) or []): + if (getattr(obj, "QetDiagnosticKind", "") or "").strip() != "AutoRouteBatch": + continue + try: + group.removeObject(obj) + except Exception: + pass + try: + if doc.getObject(getattr(obj, "Name", "")) is not None: + doc.removeObject(obj.Name) + removed += 1 + except Exception: + pass + return removed + + +def _write_auto_route_batch_diagnostic(doc, report): + if doc is None or not isinstance(report, dict): + return None + if not report.get("errors") and not report.get("missing_endpoint_uuids") and report.get("collision_warnings", 0) <= 0: + return None + project_uuid = _project_uuid(doc) + group = WiringObjects.ensure_diagnostic_group(doc, project_uuid) + _clear_auto_route_batch_diagnostics(doc) + diagnostic = doc.addObject("App::DocumentObjectGroup", _unique_name(doc, "QETAutoRouteDiagnostic")) + diagnostic.Label = "QET Auto Route Diagnostic" + _set_string(diagnostic, "QetDiagnosticKind", "AutoRouteBatch", "QET diagnostic kind") + _set_string( + diagnostic, + "QetDiagnosticJson", + json.dumps(report, ensure_ascii=False), + "QET auto-routing batch diagnostic payload", + ) + group.addObject(diagnostic) + return diagnostic +``` + +- [ ] **Step 4: Call the diagnostic writer** + +At the end of `route_all_from_payload`, immediately before `return report`, add: + +```python +_write_auto_route_batch_diagnostic(doc, report) +``` + +- [ ] **Step 5: Run the focused test** + +Run: + +```powershell +D:\FreeCAD-1.1.1\bin\python.exe -m unittest tests.python.freecad_exchange_auto_routing_test.AutoRoutingTest.test_route_all_writes_diagnostic_object_for_missing_terminal +``` + +Expected: PASS. + +- [ ] **Step 6: Run auto-routing tests** + +Run: + +```powershell +D:\FreeCAD-1.1.1\bin\python.exe -m unittest tests.python.freecad_exchange_auto_routing_test +``` + +Expected: all tests pass. + +- [ ] **Step 7: Commit** + +Run: + +```powershell +git add src/Mod/FreeCADExchange/AutoRouting.py tests/python/freecad_exchange_auto_routing_test.py +git commit -m "feat: write freecad auto routing diagnostics" +``` + +Expected: the commit contains diagnostic object behavior and tests. + +## Task 5: Improve one-click panel summary + +**Files:** +- Modify: `src/Mod/FreeCADExchange/AutoRouting.py:1116-1152` +- Modify: `src/Mod/FreeCADExchange/AutoRoutingPanel.py:137-160` +- Test: `tests/python/freecad_exchange_auto_routing_test.py` + +- [ ] **Step 1: Write the failing report text test** + +Add this test to `tests/python/freecad_exchange_auto_routing_test.py` inside `AutoRoutingTest`: + +```python + def test_route_all_report_includes_network_and_first_error(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "total_wires": 2, + "routed": 1, + "collision_warnings": 1, + "skipped_missing_terminal": 1, + "prepared_layout": { + "wire_duct_carriers": 2, + "surface_carriers": 4, + "terminal_access_carriers": 6, + }, + "missing_endpoint_samples": [ + { + "start_terminal_uuid": "terminal-a", + "end_terminal_uuid": "terminal-b", + } + ], + "errors": ["没有可用的线槽/路由路径网络"], + } + + message = auto_routing.format_route_all_report(report) + + self.assertIn("routed=1", message) + self.assertIn("线槽路径 2 条", message) + self.assertIn("首个错误:没有可用的线槽/路由路径网络", message) + self.assertIn("缺失示例:terminal-a -> terminal-b", message) +``` + +- [ ] **Step 2: Run the new report test** + +Run: + +```powershell +D:\FreeCAD-1.1.1\bin\python.exe -m unittest tests.python.freecad_exchange_auto_routing_test.AutoRoutingTest.test_route_all_report_includes_network_and_first_error +``` + +Expected: FAIL because the first error is not included. + +- [ ] **Step 3: Extend `format_route_all_report`** + +In `src/Mod/FreeCADExchange/AutoRouting.py`, inside `format_route_all_report`, after the prepared layout block, add: + +```python +errors = report.get("errors", []) or [] +if errors: + message += "\n首个错误:{0}".format(str(errors[0])) +``` + +Then ensure the missing endpoint sample block runs whenever a sample exists: + +```python +sample = (report.get("missing_endpoint_samples") or [None])[0] +if sample: + message += "\n缺失示例:{0} -> {1}".format( + sample.get("start_terminal_uuid", ""), + sample.get("end_terminal_uuid", ""), + ) +``` + +- [ ] **Step 4: Run the focused test** + +Run: + +```powershell +D:\FreeCAD-1.1.1\bin\python.exe -m unittest tests.python.freecad_exchange_auto_routing_test.AutoRoutingTest.test_route_all_report_includes_network_and_first_error +``` + +Expected: PASS. + +- [ ] **Step 5: Run auto-routing tests** + +Run: + +```powershell +D:\FreeCAD-1.1.1\bin\python.exe -m unittest tests.python.freecad_exchange_auto_routing_test +``` + +Expected: all tests pass. + +- [ ] **Step 6: Commit** + +Run: + +```powershell +git add src/Mod/FreeCADExchange/AutoRouting.py tests/python/freecad_exchange_auto_routing_test.py +git commit -m "fix: clarify freecad auto routing report" +``` + +Expected: the commit contains only report-formatting code and tests. + +## Task 6: Add save and reopen coverage to the FreeCAD smoke + +**Files:** +- Modify: `tests/manual/freecad_auto_routing_smoke.py` + +- [ ] **Step 1: Extend smoke output assertions** + +In `tests/manual/freecad_auto_routing_smoke.py`, after the current document save path is created, ensure the script saves and reopens the FCStd: + +```python + doc.saveAs(OUT_FCSTD) + App.closeDocument(doc.Name) + reopened = App.openDocument(OUT_FCSTD) + routed_group = reopened.getObject("QETWiring_04_Routed") + reopened_wires = list(getattr(routed_group, "Group", []) or []) if routed_group else [] + result_payload["reopened_routed_wire_count"] = len(reopened_wires) + result_payload["reopened_has_auto_route"] = any( + (getattr(wire, "RouteType", "") or "").strip() == "AutoSuggested" + for wire in reopened_wires + ) +``` + +If the script currently writes JSON before save/reopen, move the JSON write after the new payload fields are set. + +- [ ] **Step 2: Run the manual smoke** + +Run: + +```powershell +D:\FreeCAD-1.1.1\bin\python.exe tests\manual\freecad_auto_routing_smoke.py +``` + +Expected: exit code 0 and JSON output contains: + +```json +"reopened_has_auto_route": true +``` + +- [ ] **Step 3: Commit** + +Run: + +```powershell +git add tests/manual/freecad_auto_routing_smoke.py +git commit -m "test: verify freecad auto routes survive reopen" +``` + +Expected: the commit contains only the manual smoke update. + +## Task 7: Final verification + +**Files:** +- Read: `docs/superpowers/specs/2026-05-28-freecad-electrical-auto-routing-design.md` +- Read: changed files from Tasks 1-6 + +- [ ] **Step 1: Run FreeCADExchange unit tests** + +Run: + +```powershell +D:\FreeCAD-1.1.1\bin\python.exe -m unittest discover -s tests\python -p "freecad_exchange*_test.py" +``` + +Expected: all discovered tests pass. + +- [ ] **Step 2: Run FreeCADExchange syntax check** + +Run: + +```powershell +D:\FreeCAD-1.1.1\bin\python.exe -m py_compile src\Mod\FreeCADExchange\AutoRouting.py src\Mod\FreeCADExchange\RoutingNetwork.py src\Mod\FreeCADExchange\AutoRoutingPanel.py src\Mod\FreeCADExchange\WiringImport.py src\Mod\FreeCADExchange\WiringObjects.py +``` + +Expected: exit code 0 with no syntax errors. + +- [ ] **Step 3: Run manual smoke** + +Run: + +```powershell +D:\FreeCAD-1.1.1\bin\python.exe tests\manual\freecad_auto_routing_smoke.py +``` + +Expected: exit code 0, output JSON exists, and reopened auto route is true. + +- [ ] **Step 4: Verify no forbidden first-version database dependency was introduced** + +Run: + +```powershell +rg -n "project_3d_scene_instance|project_3d_space_object|project_2d3d_link|start_end_terminal_matches|connection_point_key|terminal_key" src\Mod\FreeCADExchange docs\superpowers\plans\2026-05-28-freecad-electrical-auto-routing.md +``` + +Expected: no matches in `src/Mod/FreeCADExchange`; matches in this plan are allowed only because this verification command names forbidden strings. + +- [ ] **Step 5: Verify staged and unstaged diffs** + +Run: + +```powershell +git status --short +git diff --check +``` + +Expected: no whitespace errors. Untracked screenshots and diagnostic JSON files may remain; do not stage them. + +- [ ] **Step 6: Commit final docs if the plan was adjusted during execution** + +Run only if this plan file changed during execution: + +```powershell +git add docs/superpowers/plans/2026-05-28-freecad-electrical-auto-routing.md +git commit -m "docs: update freecad auto routing plan" +``` + +Expected: commit contains only the plan document.