feat: add find_orphaned_wires schematic analysis tool

Detects wire segments with at least one dangling endpoint — an endpoint
not connected to a component pin, net label, or another wire.  These
cause ERC 'wire end unconnected' violations and are a common symptom of
incomplete routing or stray stub wires.

Algorithm uses exact KiCad IU (10 000 IU/mm) coordinate matching,
consistent with wire_connectivity.py:
  1. Build an endpoint-frequency map for all wires (IU precision)
  2. Collect anchored IU points: component pins (via PinLocator),
     net labels / global_labels, power symbol pins
     (via _parse_virtual_connections)
  3. An endpoint is dangling when it is touched by exactly one wire AND
     is not an anchored point; the containing wire is reported

Does not require the KiCad UI to be running.

Changes:
  python/commands/schematic_analysis.py — find_orphaned_wires() function
  python/kicad_interface.py             — handler + route registration
  python/schemas/tool_schemas.py        — MCP schema entry
  src/tools/schematic.ts                — TypeScript server.tool() call
  tests/test_schematic_analysis.py      — 7 integration tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Eugene Mikhantyev
2026-04-12 14:44:12 +01:00
parent 94b234a36e
commit 58bb08a252
5 changed files with 311 additions and 0 deletions

View File

@@ -1360,4 +1360,36 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
};
},
);
// Find orphaned wires
server.tool(
"find_orphaned_wires",
"Find wire segments with at least one dangling endpoint — not connected to a component pin, " +
"net label, or another wire. Orphaned wires cause ERC 'wire end unconnected' errors. " +
"Does not require the KiCad UI to be running.",
{
schematicPath: z.string().describe("Path to the .kicad_sch schematic file"),
},
async (args: { schematicPath: string }) => {
const result = await callKicadScript("find_orphaned_wires", args);
if (result.success) {
const wires: any[] = result.orphaned_wires || [];
if (wires.length === 0) {
return { content: [{ type: "text", text: "No orphaned wires found." }] };
}
const lines: string[] = [`Found ${wires.length} orphaned wire(s):\n`];
wires.slice(0, 50).forEach((w: any) => {
const dangling = w.dangling_ends.map((e: any) => `(${e.x}, ${e.y})`).join(", ");
lines.push(
` wire (${w.start.x}, ${w.start.y})→(${w.end.x}, ${w.end.y}) dangling end(s): ${dangling}`,
);
});
if (wires.length > 50) lines.push(` ... and ${wires.length - 50} more`);
return { content: [{ type: "text", text: lines.join("\n") }] };
}
return {
content: [{ type: "text", text: `Failed: ${result.message || "Unknown error"}` }],
};
},
);
}