Blanket exclude_points in clamp_stub_length() skipped same-component
obstacles regardless of direction, allowing stubs to bridge through
adjacent pins (R2 +3V3/SDA). Moved exclusion to batch.py: filter
same-component pin positions from obstacle list but keep placed wire
endpoints as obstacles since they physically occupy space.
clamp_stub_length() was treating all pins as potential obstacles,
including pins on the same component. On vertical caps like C7 with
5.08mm pin spacing, pin 1 clamped pin 2's stub to near-zero. Added
exclude_points parameter so callers can skip same-component pins
that cannot cause external net bridges.
resolve_wire_collision() was shifting both stub endpoints when avoiding
collinear overlaps, detaching the wire from the pin. Now only the label
end shifts perpendicular to the axis while the start stays anchored.
A4 is too small for real schematics. A3 matches US engineering
convention (close to ANSI B/Tabloid) and is KiCad's standard
template size. Batch JSON also supports paper_size field.
Autowire guide covers the decision tree, threshold tuning, workflow
patterns, and troubleshooting. CLAUDE.md updated to reflect the new
autowire/ package and tools/autowire.py.
The obstacle pre-scan ran before component placement, caching None
for every pin on components being created in the same batch. Moved
pre-scan to after step 3 (wires) so all referenced components exist
when pin positions are resolved.
7.62mm default stubs caused shorts on small passives and stacked
labels where pin-to-pin distance was less than the stub length.
clamp_stub_length() now auto-shortens stubs when obstacles (adjacent
pins, placed wire endpoints) are detected in the stub's path, with
a 1.27mm clearance margin and 2.54mm minimum floor.
Default stub_length increased from 2.54mm to 7.62mm (3 grid units)
so labels clear component bodies. Per-connection stub_length and
direction overrides added for edge cases where auto-placement puts
labels in bad spots.
007: Pin-ref fix not active after /mcp reconnect (transport
reconnect doesn't reload Python modules). 008: Reply confirming
server restart required to load new sexp_parser.py code.
Classifies unconnected nets into optimal wiring methods (direct wire,
local/global label, power symbol, no-connect) based on pin distance,
crossing count, fanout, and net name patterns. Delegates all file
manipulation to apply_batch, inheriting collision detection and label
serialization for free.
Strategy concepts informed by KICAD-autowire (MIT, arashmparsa).
sch.components.get() returns only the first unit instance for a given
reference. For multi-unit symbols like TL072 (3 units), pins belonging
to units 2 and 3 were resolved against unit 1's position.
Added pin-to-unit mapping via KiCad sub-symbol naming convention
(e.g. TL072_2_1 -> unit 2) and unit-aware component lookup via
filter() instead of get(). Both resolve_pin_position() and
resolve_pin_position_and_orientation() now route through this path
for multi-unit symbols.
validate_schematic now exports a netlist via kicad-cli to traverse the
full sheet hierarchy for connectivity analysis, replacing the single-file
kicad-sch-api approach that returned zeros on hierarchical projects.
Unconnected pins derived from ERC pin_not_connected count.
Pass unit field through to kicad-sch-api's native multi-unit validation
instead of custom bypass. Removes _add_multi_unit() that used incompatible
internal API (_add_item vs _add_item_to_collection across API versions).
Combines hierarchy-aware ERC (via root schematic resolution) with
connectivity analysis and optional baseline comparison into one atomic
call. Supports fail_on parameter to gate pass/fail on specific ERC
violation types (default: multiple_net_names). Baseline comparison
detects connection count decreases and unconnected pin increases as
regressions.
Replaces the 20+ tool call workflow of running ERC + connectivity on
each sub-sheet individually.
280/280 tests pass, ruff + mypy clean.
Wire collision detection: apply_batch now tracks placed wire segments and
detects collinear stubs on the same axis with overlapping ranges belonging
to different nets. Colliding wires shift perpendicular to their axis by
1.27mm, preventing KiCad from merging wire segments into mega-nets.
Project-local library resolution: apply_batch now scans batch component
lib_ids for unknown libraries and registers them with kicad-sch-api's
SymbolLibraryCache via sym-lib-table parsing before component placement.
Unblocks projects using Samacsys and other non-standard symbol libraries.
Root ERC: run_schematic_erc accepts root=True to resolve to the project
root schematic before running kicad-cli, enabling hierarchy-aware ERC
that eliminates ~180 false-positive global_label_dangling warnings from
sub-sheet isolation.
270/270 tests pass, ruff + mypy clean.
KiCad 9 nests violations under sheets[].violations instead of
a top-level violations key. The parser now iterates all sheets
and aggregates violations, with a fallback for flat format.
no_connects now accept {pin_ref, pin_number} as an alternative to
{x, y} coordinates, matching the pattern used by labels. Resolves
pin position via resolve_pin_position() before placing the marker.
snap_to_grid() used 2.54mm default grid for symbol position,
silently rounding sub-2.54mm stub lengths up and causing shorts
on tightly-spaced connectors. Now uses 1.27mm fine grid.
add_hierarchical_sheet now returns sheet_uuid and parent_uuid.
apply_batch accepts these as optional params to call
set_hierarchy_context() before placing components, fixing
kicad-cli netlist export for hierarchical designs.
330 power symbols placed across 10 sheets but zero power nets appear in
kicad-cli export. Affects both mckicad and generate_schematic.py output.
Instance path mismatch identified as secondary issue. Workaround: use
verify_connectivity which reads power symbols via kicad-sch-api natively.
Label collision detection: resolve_label_collision() shifts different-net
labels that share the same (x,y) coordinate by 1.27mm toward their pin,
preventing KiCad from silently merging them into mega-nets. Integrated
at both label placement points in apply_batch.
Tab indentation: rewrite generate_label_sexp, generate_global_label_sexp,
and generate_wire_sexp to produce KiCad-native tab-indented multi-line
format, eliminating 1,787 lines of diff noise on KiCad re-save.
Intersheetrefs property now uses (at 0 0 0) placeholder.
Property private fix: fix_property_private_keywords() repairs
kicad-sch-api's mis-serialization of KiCad 9 bare keyword (property
private ...) as quoted (property "private" ...), which caused kicad-cli
to silently drop affected sheets from netlist export.
243 tests pass, ruff + mypy clean.
Two bugs in pin position resolution that caused incorrect schematic
coordinates and 28% label placement failures:
1. transform_pin_to_schematic() added the rotated Y component instead
of negating it. lib_symbol pins use Y-up; schematics use Y-down.
Fix: comp_y + ry -> comp_y - ry.
2. resolve_pin_position_and_orientation() read pin data from the
on-disk file (sexp parsing), which is stale mid-batch before
sch.save(). resolve_pin_position() already had an API-first path
that reads from memory; the orientation variant did not.
Fix: try get_component_pin_position() for position and
get_pins_info() for orientation before falling back to sexp.
Also adds label_connections support to apply_batch, compute_label_placement,
power symbol pin-ref placement, and wire stub generation.
Netlists exceeding INLINE_RESULT_THRESHOLD (20 nets) now return a
compact summary with the top nets by connection count as a preview.
Full data is always written to the sidecar JSON file. Small netlists
still return everything inline.
KiCad 9 defaults to s-expression netlist export, not XML. Add
_parse_kicad_sexp() parser with pinfunction/pintype metadata, update
auto-detection to distinguish (export from <export by content.
Fix export_netlist: use kicad-cli's actual format names (kicadsexpr,
kicadxml) instead of invalid 'kicad', use --format instead of -f, and
treat non-zero exit with valid output as success with warnings.
Adds KiCad XML (.net) and CSV/TSV netlist import with auto-format
detection. Output nets dict is directly compatible with
verify_connectivity's expected parameter.
Parses external netlist files into the component-pin-net graph that
verify_connectivity and apply_batch consume. Supports KiCad XML (.net)
exported by kicad-cli, and CSV/TSV with flexible column name matching.
Auto-detects format from file extension and content. Output is directly
compatible with verify_connectivity's expected parameter, closing the
loop between "I have a design" and "I can build it in KiCad."
Requested by ESP32-P4 project (agent thread message 028).
Hierarchical KiCad projects store sub-sheets in subdirectories (e.g.
sheets/). The flat os.listdir scan missed all of them. Use recursive
glob to find .kicad_sch files at any depth under the project directory.
Reported by ESP32-P4 project (agent thread message 025) — their 8
malformed property-private entries were all in sheets/ subdirectory.
validate_project now scans all .kicad_sch files for two classes of
silent rendering failure: quoted (property "private" ...) that should
be a bare keyword in KiCad 9, and lib_id references with no matching
lib_symbols entry. Both cause kicad-cli to blank entire pages during
export without any error message.
Also pass working_dir to run_kicad_command in export_pdf for robust
library path resolution.
Addresses feedback from ESP32-P4 project (agent thread message 021).
Power symbols at a pin coordinate create a direct electrical connection
without a wire. Note this in audit_wiring (explains wire_count: 0 for
connected pins) and add_power_symbol (explains why it draws a stub).
Group results by net name instead of per-pin, keeping the summary
compact enough to stay inline even for 100+ pin components. Add
anomaly detection (unconnected pins, high-fanout nets, auto-named
nets) and optional pin/net filters. Wire coordinates are now opt-in
via include_wires flag to avoid flooding the calling LLM with
coordinate noise.
Refactors _build_connectivity() into a two-layer state builder so the
union-find internals (pin_at, label_at, wire_segments) are accessible
to new analysis tools without duplicating the 200-line connectivity engine.
New tools:
- audit_wiring: trace all wires connected to a component, report per-pin
net membership with wire segment coordinates and connected pins
- remove_wires_by_criteria: bulk-remove wires by coordinate filters
(y, x, min/max ranges, tolerance) with dry_run preview support
- verify_connectivity: compare actual wiring against an expected
net-to-pin mapping, report matches/mismatches/missing nets
New sexp_parser utilities:
- parse_wire_segments: extract (wire ...) blocks with start/end/uuid
- remove_sexp_blocks_by_uuid: atomically remove blocks by UUID set
parse_lib_symbol_pins() now falls back to searching external .kicad_sym
library files when a symbol isn't embedded in the schematic's lib_symbols
section. Splits lib_id ("LibName:SymName") to locate the library file,
then parses pins using the bare symbol name.
Search order: schematic dir, libs/, ../libs/, project root, project
root/libs/, kicad/libs/, and sym-lib-table entries with ${KIPRJMOD}
substitution. Handles nonexistent directories gracefully.
Fixes add_power_symbol for script-generated schematics that reference
project library symbols without embedding them (e.g. D1/SMF5.0CA in
ESP32-P4-WIFI6-DEV-KIT library).
add_label bypasses kicad-sch-api serializer entirely — generates
s-expression strings and inserts them directly into the .kicad_sch
file via atomic write. Fixes two upstream bugs: global labels silently
dropped on save (serializer never iterates "global_label" key), and
local labels raising TypeError (parameter signature mismatch in
LabelCollection.add()).
add_power_symbol now falls back to sexp pin parsing when the API
returns None for custom library symbols (e.g. SMF5.0CA). Extracts
shared resolve_pin_position() utility used by both add_power_symbol
and batch operations.
Batch labels also fixed — collected as sexp strings during the batch
loop and inserted after sch.save() so the serializer can't overwrite
them.
Reduce _rc() and transform_pin_to_schematic() rounding from 3 to 2
decimal places to match KiCad's 0.01mm coordinate quantum — prevents
union-find misses when wire endpoints and sexp-parsed pin positions
differ at the sub-quantum level.
Use schematic stem as subdirectory inside .mckicad/ so multi-sheet
analysis outputs (connectivity.json, etc.) don't collide.
_build_connectivity() used sch.list_component_pins() which returns
empty for custom library symbols. Added a second pass that falls back
to parse_lib_symbol_pins() + transform_pin_to_schematic() for any
component that returned 0 pins from the API. This brings custom IC
pins (e.g. ESP32-P4's 105 pins) into the union-find net graph.
kicad-sch-api has two parsing gaps: get_symbol_definition() returns
None for non-standard library prefixes (e.g. Espressif:ESP32-P4),
and there is no sch.global_labels attribute for (global_label ...)
nodes. This adds a focused parser that reads directly from the raw
.kicad_sch file as a fallback, integrated into the connectivity
engine, pin extraction, and label counting tools.
Root cause: kicad-sch-api doesn't back-populate comp.pins or sch.nets
on loaded schematics. All data is accessible through alternative APIs.
Pin extraction: use comp.get_symbol_definition().pins for metadata and
sch.list_component_pins(ref) for schematic-transformed positions.
Connectivity: new wire-walking engine using union-find on coordinates.
Walks wires, pin positions, labels, and power symbols to reconstruct
the net graph. Replaces broken ConnectivityAnalyzer/sch.nets fallbacks.
Eliminates 'unhashable type: Net' crash.
Hierarchy: use sch.sheets.get_sheet_hierarchy() instead of the broken
sheets.data.get("sheet", []) raw dict approach.
Labels: supplement sch.get_statistics() with sch.labels.get_statistics()
and sch.hierarchical_labels for accurate counts.
99 tests passing, lint clean.
New modules:
- patterns/ library: decoupling bank, pull resistor, crystal oscillator
placement with power symbol attachment and grid math helpers
- tools/batch.py: atomic file-based batch operations with dry_run
- tools/power_symbols.py: add_power_symbol with auto #PWR refs
- tools/schematic_patterns.py: MCP wrappers for pattern library
- tools/schematic_edit.py: modify/remove components, title blocks, annotations
- resources/schematic.py: schematic data resources
43 new tests (99 total), lint clean.
config.py evaluates os.environ at import time, but mckicad/__init__.py
eagerly imports config via 'from .config import *'. The old main.py
loaded .env after importing from mckicad, so the search paths were
always empty. Now .env is parsed with stdlib before any mckicad imports.
Also fix start.sh to use 'uv run' instead of stale venv/ path.