search_symbols, list_symbol_libraries, list_library_symbols, and
get_symbol_info previously only consulted the global sym-lib-table. A
library registered with project scope (an entry in
<project>/sym-lib-table) was therefore invisible — even right after
open_project succeeded — making add_schematic_component the only tool
that could see it.
Fix has two parts:
1. Wrap project_commands.open_project and project_commands.create_project
in handlers that rebuild SymbolLibraryCommands.library_manager against
the project directory. After open_project, project-scope libraries are
automatically visible to subsequent search/list/info calls.
2. Add an optional projectPath parameter to the four discovery tools
(accepts a project directory, .kicad_pro, .kicad_pcb, or .kicad_sch
path). Stateless callers can resolve project libraries without first
calling open_project. SymbolLibraryCommands._derive_project_path also
walks up from schematicPath/boardPath to find the directory that owns
the project, mirroring the logic in _handle_add_schematic_component.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the handler used kicad-skip to apply rotation and mirror.
kicad-skip has no API for (mirror x/y) on placed symbols, causing:
'NoneType' object has no attribute 'value'
Fix:
- Rewrote _handle_rotate_schematic_component to use sexpdata (same
approach as move_schematic_component) for both rotation and mirror
- Added WireDragger.compute_pin_positions_for_rotation: computes old
and new pin world positions when rotation/mirror changes at fixed (x,y)
- Added WireDragger.update_symbol_rotation_mirror: updates (at) rotation
and adds/removes/replaces the (mirror x/y) sexpdata token cleanly
- Connected wires now follow pin positions after rotate/mirror via the
existing WireDragger.drag_wires infrastructure
Tests: 10 unit tests in tests/test_rotate_schematic_mirror.py covering
update_symbol_rotation_mirror, compute_pin_positions_for_rotation, and
a handler smoke test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Refactor _handle_rotate_schematic_component to use raw sexp throughout
and write the schematic once instead of three times
- Hoist sexpdata and WireManager imports to module scope; drop the
unnecessary underscore aliases in move/rotate handlers
- Move WireManager._SUB_UNIT_RE to the top of the class body
- Promote the per-call Symbol("symbol")/Symbol("unit") allocations in
_parse_lib_pins / _collect_pin_positions to module-level _SYM_*
constants
- Document the assumption behind _SUB_UNIT_RE's greedy match
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the manual add_schematic_junction tool with automatic junction
management. WireManager.sync_junctions inserts/removes junction dots
based on wire endpoints plus component pin positions and is invoked
after add_wire, add_polyline_wire, delete_wire, move, and rotate.
- Pin-aware: parses lib_symbols and applies KiCad's mirror/rotate/
translate transform to compute world pin coordinates
- Multi-unit safe: filters lib_symbols sub-units by the placed
symbol's (unit N) field plus the unit-0 common body
- Removes the now-unused WireManager.add_junction static method
- Updates CHANGELOG [Unreleased] with the tool removal notice
- Adds .mcp.json to .gitignore (machine-local paths)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Configuring logging at module import time hard-failed in environments
without write access to the user's home directory (sandboxed pytest
runners, restricted CI containers, read-only filesystems). The
unhandled OSError/PermissionError aborted module import and cascaded
into ~100 test failures during collection.
Wrap the FileHandler setup in try/except and fall back to console-only
logging when the log directory cannot be created. Production behavior
is unchanged - file logging in ~/.kicad-mcp/logs is still used
whenever the directory is writable.
Also picks up an isort-driven import reorder applied by the project's
pre-commit hook.
connect_to_net and connect_passthrough previously only touched the
schematic: a wire stub plus a net label were added via
ConnectionManager, but the corresponding pad on the .kicad_pcb was
never updated.
pcbnew.SaveBoard() silently drops every net that is not referenced by
at least one board element (pad/track/via/zone). A net that exists
only in the schematic therefore disappears on save, and in KiCad the
board's ratsnest never shows anything.
Fix:
- new _assign_net_to_pad(component_ref, pin_name, net_name):
ensures the net exists on the board (creates NETINFO_ITEM if not)
and sets it on the matching pad of the matching footprint.
- _handle_connect_to_net calls it after the schematic op when
self.board is loaded.
- _handle_connect_passthrough parses ConnectionManager's 'connected'
entries ('J1/1 <-> J2/1 [PIN_1]') and assigns nets to both pads.
- register 'connect_to_net' in _BOARD_MUTATING_COMMANDS so the
existing auto-save path persists the pad assignment to disk.
KiCad serialises rescued or locally-customised library entries with an
extra (lib_name "...") child before (lib_id "..."):
(symbol
(lib_name "RESISTOR_0603_4")
(lib_id "MF_Passives:RESISTOR_0603")
(at 132.08 44.45 90)
...)
The block-matching regex in _handle_get_schematic_component,
_handle_edit_schematic_component, and _handle_delete_schematic_component
required (lib_id IMMEDIATELY after (symbol, so any placed component
using this form was silently invisible to lookup. The user-visible
symptom is "Component '<ref>' not found in schematic" even though the
component is plainly present (and reachable through list / IPC paths).
This bug also affected set/remove_schematic_component_property and the
existing footprint/value/reference rewriting paths in edit, since they
all share the same lookup code.
The parent-position lookup used a similarly-strict regex
((symbol (lib_id "...") (at ...))), which silently fell back to (0,0)
on (lib_name)-first symbols and caused new properties added through
the custom-properties path to anchor at the schematic origin instead
of the parent symbol.
Fix: relax the symbol-block opening pattern to (symbol\s+\( — matching
any opening paren after (symbol — and read the symbol's origin from
the first (at ...) inside the block. Library-definition entries inside
(lib_symbols ...) are still excluded by the existing range check
(they use the (symbol "name" ...) form with a quoted string, not a
paren).
Adds 7 regression tests in TestLibNameBeforeLibIdOrdering using a
real-world (lib_name)-first resistor block, covering get / edit /
set-property / remove-property / delete and verifying that newly
added properties anchor to the symbol origin instead of (0, 0).
Promotes BOM / sourcing fields (MPN, Manufacturer, DigiKey_PN, LCSC,
JLCPCB_PN, Voltage, Tolerance, Dielectric, ...) to first-class citizens
on placed schematic symbols.
New MCP tools:
- set_schematic_component_property: add or update one custom property
on a component (convenience wrapper around edit_schematic_component).
- remove_schematic_component_property: delete one custom property.
The four built-in fields (Reference, Value, Footprint, Datasheet) are
protected and rejected.
edit_schematic_component enhancements:
- New `properties` parameter: map of property name to either a string
value or a full spec object { value, x?, y?, angle?, hide?, fontSize? }.
Adds the property when missing, otherwise updates the existing field
(and optionally its label position / visibility). Lets a single tool
call attach an entire BOM payload to a component.
- New `removeProperties` parameter: list of custom property names to
delete in the same call.
- Property values are now backslash-escaped so descriptions containing
a double-quote or a backslash no longer corrupt the .kicad_sch file.
- New properties default to (hide yes) so they appear in BOM exports
without cluttering the schematic canvas.
get_schematic_component description clarified to highlight that it
already returns every field on the symbol, including custom ones.
New MCP prompt component_sourcing_properties guides agents through the
conventional property names recognised by downstream BOM tooling and
the recommended call sequence.
Implementation (python/kicad_interface.py):
- _PROTECTED_PROPERTY_FIELDS frozenset
- _escape_sexpr_string / _find_matching_paren static helpers
- _set_property_in_block / _set_hide_on_property /
_remove_property_from_block surgical text-level edits that preserve
formatting and the property's UUID
- _handle_edit_schematic_component rewritten to orchestrate
add/update/remove and return a per-property summary
- New handlers _handle_set_schematic_component_property and
_handle_remove_schematic_component_property registered in the
command dispatch table
Tests (tests/test_schematic_component_properties.py):
32 tests covering escape helper, paren matcher, add/update/remove
(single + batched), full spec dicts, default position, default
(hide yes), special-character escaping, UUID preservation, protected
built-in field rejection, no-op removal, both new convenience tools,
and input validation. All 590 tests in the project still pass.
Docs: README, SCHEMATIC_TOOLS_REFERENCE, TOOL_INVENTORY, CHANGELOG.
Adds robust multi-sheet (hierarchical) net connectivity for KiCad
schematics and switches the wire/label parsing to a direct sexpdata
pipeline that bypasses kicad-skip's collection iteration, which was
silently dropping wires, labels, and symbol instances on real-world
schematics.
python/commands/wire_connectivity.py
- New sexpdata helpers: _load_sexp, _parse_wires_sexp,
_parse_labels_sexp, _parse_symbol_instances_sexp,
_parse_hierarchical_labels_sexp, _discover_sub_sheets.
- _build_adjacency now detects T-junctions (endpoint landing on
another wire's interior segment) so adjacency captures connections
KiCad doesn't represent as separate wire segments.
- _find_connected_wires gains an interior-segment fallback so labels
placed mid-wire still seed BFS correctly.
- _parse_virtual_connections gathers label / global_label /
hierarchical_label and power-symbol pin positions, with a
kicad-skip fallback for unit tests that mock the schematic.
- _find_pins_on_net rebuilds pin positions from sexpdata symbol
instances (with mirror_x/mirror_y/rotation handling) and uses a
plus/minus 1 IU tolerance for floating-point edge cases.
- get_connections_for_net walks the top sheet plus every recursively
discovered sub-sheet, deduping pins across sheets.
python/commands/pin_locator.py
- lib_id matching now falls back to a bare-name + unit-suffix match
so instances like "stat-tis-custom:BAT_18650" resolve to
lib_symbols entries like "BAT_18650_3".
- Pin position math now y-negates lib_symbols coords, applies
mirror_x/mirror_y in local coords before rotation, and propagates
the same transform into get_pin_orientation so downstream callers
get a correct outward angle for mirrored symbols.
python/commands/connection_schematic.py
- generate_netlist now collects nets from both label and
global_label and routes them through get_connections_for_net so
netlists reflect cross-sheet connectivity instead of single-sheet
label-only nets.
python/kicad_interface.py
- list_schematic_nets aggregates net names across the top sheet and
all sub-sheets via the sexp helpers, then resolves connections
using get_connections_for_net.
- get_net_connections delegates to get_connections_for_net for
consistent multi-sheet results.
Adds two new MCP tools for working with free-form text annotations
(SCH_TEXT elements) in KiCad schematics:
- add_schematic_text: place a text note with optional angle, font size,
bold/italic, and justification
- list_schematic_texts: list all text annotations with optional
case-insensitive substring filter
Includes WireManager.add_text / list_texts using _text_insert + sexpdata,
handler dispatch in KiCADInterface, TypeScript tool definitions, registry
entry, reference doc updates, and 30 unit tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hierarchical KiCad schematics store all components in sub-sheets;
the top-level .kicad_sch only contains sheet references. The previous
sync_schematic_to_board implementation called generate_netlist on the
top-level file only, which has no components, so it always returned
0 pads assigned.
Replace with _build_hierarchical_pad_net_map which:
- rglobs all .kicad_sch files in the project directory
- For each sheet, collects label positions from label, global_label,
and hierarchical_label via skip.Schematic
- Adds power symbol (#PWR/#FLG) positions using their Value as net name
- Builds a wire adjacency graph and BFS-propagates net names through
wire segments to reach pins not directly under a label
- Calls PinLocator.get_all_symbol_pins to get absolute pin positions,
then matches to the propagated net map within 0.5 mm tolerance
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds python/annotations/loader.py (AnnotationLoader) which resolves MCP
tool names to KiCad IPC proto message annotations using three layers:
1. Explicit TOOL_TO_PROTO mapping for non-obvious name pairs
2. Automatic snake_case → PascalCase conversion
3. Suffix-stripped variants (get_nets_list → GetNets)
Integrates into kicad_interface.py tools/list handler:
- Tools with existing schemas: enrich_schema() adds blocking/interactive
ToolAnnotations hints and fills description gaps
- Tools without schemas (fallback): gets a real description from the
IPC annotation instead of the generic "KiCAD command: <name>" string
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When add_schematic_component is called with a schematic that lives in a
sub-folder (e.g. sheets/rs485.kicad_sch), the handler derived the project
path as schematic.parent — which in a hierarchical project is the sheets/
directory, not the project root. Any project-local symbol library declared
in sym-lib-table (using ${KIPRJMOD}) is invisible to DynamicSymbolLoader
from that directory, causing "Symbol 'X' not found in library 'Y'" even
though the library file and sym-lib-table entry both exist.
Fix: walk the ancestor chain from the schematic file upward until a
directory is found that contains a sym-lib-table file or a .kicad_pro
file, then use that as the project root for library resolution.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Allows placing a specific unit (A=1, B=2, C=3, …) of a multi-unit KiCad
symbol rather than always defaulting to unit 1. Required for quad
optocouplers, dual op-amps, and other multi-unit parts where each channel
must be placed independently.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves a net label (local, global, or hierarchical) to a new position in
place, avoiding the error-prone delete-then-re-add workflow. Supports an
optional currentPosition disambiguator and labelType filter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add optional netName (exact case-sensitive match) and labelType
(net/global/power enum) parameters. Both are optional and AND
together when combined. Omitting both preserves current behaviour.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two new tools for managing hierarchical schematic connections:
- add_schematic_hierarchical_label: create sheet interface ports on
sub-sheet schematics. These are the sub-sheet side of hierarchical
connections, linking to sheet pins on the parent.
- add_sheet_pin: add pins to sheet symbol blocks on the parent
schematic. Targets the correct sheet by matching the Sheetname
property. The pinName must match a hierarchical_label in the
sub-sheet.
Both tools use text-based S-expression insertion (not sexpdata
round-trip) to preserve KiCad's native file formatting. Labels
include proper justification based on orientation: left-justify for
rightward labels (0°), right-justify for leftward labels (180°).
Wire manager additions:
- _find_insertion_point(): locates sheet_instances block or final paren
- _text_insert(): inserts formatted S-expression text at the right position
- _make_hierarchical_label_text(): generates hierarchical_label S-expression
- _make_sheet_pin_text(): generates sheet pin S-expression
- WireManager.add_hierarchical_label(): static method for label insertion
- WireManager.add_sheet_pin(): static method for pin insertion into
named sheet blocks
12 unit tests covering insertion, orientation/justification mapping,
parameter validation, multi-sheet targeting, and error handling.
Two issues with run_erc on KiCad 9:
1. kicad-cli returns non-zero exit code when ERC violations exist.
The handler treated this as a command failure and returned early
with success=false, even though valid JSON output was produced.
Fix: check for output file existence instead of exit code.
2. KiCad 9 nests violations under sheets[].violations instead of
(or in addition to) the top-level violations[] array used by
KiCad 8. The handler only read the top-level array, reporting
0 violations on schematics with sub-sheets.
Fix: iterate sheets[] and collect all nested violations.
Both fixes are backward-compatible with KiCad 8.
6 unit tests added covering non-zero exit codes, KiCad 8 top-level
violations, KiCad 9 sheets[] nesting, mixed structures, zero
violations, and missing output files.
export_netlist was returning "Unknown command" because no Python handler
existed. generate_netlist was timing out (30s) due to an O(nets × components
× pins) wire-graph algorithm with a new PinLocator instantiated per net.
Both handlers now delegate to `kicad-cli sch export netlist`:
- export_netlist: new handler; writes KiCad XML / Spice / Cadstar / OrcadPCB2
to the caller-supplied outputPath. Added schematicPath parameter to the TS
tool definition (was absent, making file export impossible).
- generate_netlist: replaces the slow wire-graph with kicad-cli + XML parse;
returns the same {components, nets} JSON the TS handler already expected.
Also adds _find_kicad_cli_static() so both handlers share CLI discovery
without depending on ExportCommands (which requires a loaded pcbnew board).
Cleaned up generate_netlist schema in tool_schemas.py (removed outputPath and
format fields the handler never used), updated MCP tool descriptions and
SCHEMATIC_TOOLS_REFERENCE.md to clearly distinguish the two tools.
26 unit tests added covering parameter validation, subprocess mocking,
format mapping, XML→JSON parsing, and error/timeout propagation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Half of all valid KiCAD schematic pin positions are on the 50-mil
(1.27mm) grid but not the 100-mil (2.54mm) grid — e.g. 26.67mm = 21 ×
1.27mm. Snapping to 2.54mm displaced those coordinates by 1.27mm,
moving labels off their pins and increasing floating-label count.
KiCAD source confirms: DEFAULT_CONNECTION_GRID_MILS = 50 and the ERC
off-grid check uses exact integer modulo against this value, so any
displacement breaks connectivity unconditionally.
Also update the kicad-source absolute path in CLAUDE.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
get_pin_net was a superset of get_wire_connections with the same
coordinate-based flood-fill but two extra response fields (net, query_point)
and a reference+pin input mode. Having both tools confused LLM tool selection.
get_wire_connections now:
- Returns net (label name or null) and query_point in all response paths
- Accepts reference+pin input in addition to x/y coordinates,
resolving the pin endpoint via PinLocator internally
get_pin_net tool, handler, schema, TS registration, and tests removed.
test_wire_connectivity.py updated with coverage for all new behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Answers "what net is pin X of component Y on?" without requiring
callers to triangulate from list_schematic_nets or know a wire
coordinate first.
Accepts either {reference, pin} (resolved via PinLocator) or {x, y}
coordinate. Returns net label name (or null for unnamed nets), all
connected pins, wire segments, and the resolved query point.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a new MCP tool that snaps wire endpoints, junction positions, and
net label coordinates to the nearest grid point (default 2.54 mm). Off-grid
coordinates cause wires that appear visually connected to fail ERC checks
because KiCAD uses exact IU integer matching internally; this tool eliminates
that class of error before running ERC.
- python/commands/schematic_snap.py: core snap logic with in-place sexp mutation
- python/kicad_interface.py: route + handler
- python/schemas/tool_schemas.py: JSON schema (gridSize, elements params)
- src/tools/schematic.ts: TypeScript MCP tool registration
- tests/test_snap_to_grid.py: 20 unit + integration tests (all passing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
add_schematic_net_label now accepts optional componentRef + pinNumber to
snap the label directly to the exact pin endpoint via PinLocator, removing
all approximation risk. The response always includes actual_position and,
when snapping was used, snapped_to_pin — so the caller gets confirmation
of exactly where the label landed.
connect_to_net return type changed from bool to Dict, returning
pin_location, label_location, and wire_stub on success so agents no
longer need a separate verification call to confirm placement.
connect_passthrough updated to check result.get("success") against the
new dict return. tool_schemas.py and schematic.ts updated to match
(position is now optional, componentRef/pinNumber/labelType/orientation
added, connect_to_net schema field names corrected).
17 new unit tests in tests/test_net_label_pin_snapping.py.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously passing mirror='x' or mirror='y' to rotate_schematic_component
always raised:
'NoneType' object has no attribute 'value'
Root cause: kicad-skip has no API for setting (mirror x/y) on a placed
symbol instance. The handler tried to use a non-existent kicad-skip
attribute, returning None, then calling .value on it.
Fix: add _apply_mirror_to_symbol_sexp() which directly patches the
(mirror x/y) S-expression token in the .kicad_sch file. The rotate
handler now applies rotation via kicad-skip (which works fine) and
delegates mirror to this helper.
The helper:
- Inserts (mirror x/y) immediately after the (at x y rot) token
- Removes any existing mirror token before inserting the new one
(prevents duplicate tokens when toggling axis)
- Returns False gracefully when the reference is not found
Tests: 6 unit tests in tests/test_rotate_schematic_mirror.py
covering: add x, add y, remove, replace, unknown ref, no-mirror smoke test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the path argument to create_schematic() was accepted by the
tool schema but silently ignored in SchematicManager.create_schematic()
(python/commands/schematic.py). The schematic file was always written
using only the bare filename, resolving to the MCP server's working
directory.
On systems where that directory is not writable (e.g. Program Files,
OneDrive-synced folders) this caused:
[Errno 13] Permission denied: 'myproject.kicad_sch'
Fix:
- Add path: Optional[str] = None parameter to SchematicManager.create_schematic()
- Compute output_path as os.path.join(path, base_name) when path is given
- Forward the resolved path from _handle_create_schematic() into create_schematic()
Tests: added tests/test_create_schematic_path.py with three unit tests
covering: path respected, no-path fallback, and no double-suffix on .kicad_sch names.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add type annotations to all previously untyped functions and remove 9
suppressed error codes (call-arg, assignment, return-value, operator,
has-type, dict-item, misc, list-item, annotation-unchecked) by fixing
the underlying type issues.
Add [[tool.mypy.overrides]] with ignore_missing_imports for KiCAD-specific
modules (pcbnew, sexpdata, skip, cairosvg, kipy, PIL) so the pre-commit
mypy hook passes in its isolated venv. Add types-requests and pytest to
additional_dependencies in .pre-commit-config.yaml.
Also fixes several real bugs uncovered by stricter checks: incorrect static
calls to instance methods in swig_backend, wrong return type on get_size,
missing value param in BoardAPI.place_component, variable shadowing in
kicad_process.py, unqualified LibraryManager reference in kicad_interface,
and missing top-level Path import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The TS<->Python communication channel uses stdout for JSON responses.
pcbnew's C++ SWIG layer can write warnings and diagnostics directly to
C-level stdout (fd 1), corrupting the JSON framing. The TS parser then
never sees valid JSON and the command times out after 30 seconds.
Three changes fix this:
1. Python stdout redirect: In main(), save the original stdout fd for
exclusive JSON response use, then redirect fd 1 to stderr so all
pcbnew C++ output goes to logs instead of the response pipe.
2. Robust TS JSON parser: tryParseResponse() now uses newline-delimited
parsing as a fallback. The Python side writes single-line JSON
terminated by \n; the parser uses this as the completion signal
instead of brace-matching, which prevents premature resolution of
truncated chunked responses. Non-JSON preamble lines are logged
and stripped.
3. Fix stray print() calls: Converted print() to logger in
component_schematic.py and library_schematic.py so they don't
leak to stdout during normal operations.
Also adds sync_schematic_to_board to the longRunningCommands list for
an appropriate timeout value.
When moving a schematic component whose pins directly touch pins of
stationary components (no wire segment, just pin-to-pin contact),
synthesize bridge wires to preserve the electrical connection after
the move. Also fixes duplicate-pin-position collision in old_to_new
map and updates symbol reference assignment to use setAllReferences.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>