diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6f203ba --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,60 @@ +# KiCAD MCP Server + +## Related Source + +- KiCad source code is located at `../kicad-source/` (absolute: `/home/eugene/Projects/kicad-source/`) + +## Testing + +### When to Write Tests + +Write tests for every non-trivial change to Python handler or business logic code: + +- **New MCP tools** — add tests for schema validation, handler dispatch, parameter validation, and the core logic path (happy path + key error cases). +- **Changes to existing tools** — add tests covering the changed behaviour; update any tests that no longer reflect reality. +- **Bug fixes** — add a regression test that would have caught the bug before adding the fix. +- **Refactors that delete or rename public methods** — add a test asserting the old name no longer exists (see `TestConnectionManagerOrphanedMethodsRemoved` for the pattern). + +You do **not** need tests for TypeScript/TS-layer glue code that only forwards calls to Python (the TS test runner is not yet configured). + +### Test Levels + +| Level | Use for | Marker | +| ----------- | -------------------------------------------------------------------------------------------------- | -------------------------- | +| Unit | Schema shape, parameter validation, pure logic, mock-heavy handler dispatch | `@pytest.mark.unit` | +| Integration | Real file I/O against a `.kicad_sch` / `.kicad_pcb` copy; WireManager, JunctionManager round-trips | `@pytest.mark.integration` | + +Keep unit tests free of file I/O. Keep integration tests free of business-logic assertions that belong in unit tests. + +### Where to Put Tests + +``` +tests/ + test_.py # all Python tests go here +``` + +Group related test classes inside a single file (e.g. `TestSchemas`, `TestHandlerDispatch`, `TestHandleAddSchematicWireRouting` all in `test_wire_junction_changes.py`). Name classes `Test` and methods `test_`. + +Use `python/templates/empty.kicad_sch` as the base fixture for integration tests — copy it to a `tempfile` directory, run the handler, then parse the result with `sexpdata`. + +### Running Tests + +Always use the `.venv` virtualenv for Python commands: + +```bash +npm run test:py # pytest tests/ -v +.venv/bin/pytest tests/ -v # all Python tests +.venv/bin/pytest -m unit # unit tests only +.venv/bin/pytest -m integration # integration tests only +.venv/bin/pytest --cov=python # with coverage report +.venv/bin/mypy python/ # type checking +``` + +## Git Workflow + +- **Never open a pull request automatically.** Commit and push when asked, but always wait for explicit instructions before running `gh pr create` or any equivalent command. + +## Python Code Style + +- **Never use `assert` in production code** — raise a specific exception (`ValueError`, `RuntimeError`, etc.) instead. `assert` is stripped in optimised builds and gives poor error messages. +- **Do not introduce logic-breaking workarounds to satisfy the type checker** (e.g. `x or ""` when `""` is not a valid substitute for `None`). Fix the types or narrow with a proper guard (`if x is None: raise ...`). diff --git a/python/commands/schematic_snap.py b/python/commands/schematic_snap.py index fa3568a..83c60a9 100644 --- a/python/commands/schematic_snap.py +++ b/python/commands/schematic_snap.py @@ -4,9 +4,18 @@ Snap-to-grid tool for KiCAD schematics. Snaps wire endpoints, junction positions, net labels, and optionally component positions to the nearest grid point. Modifies the schematic file in place. -The standard KiCAD eeschema grid is 2.54 mm (0.1 inch). Off-grid coordinates -cause wires that appear visually connected to fail ERC connectivity checks -because KiCAD uses exact integer (IU) matching internally. +The standard KiCAD schematic grid is 50 mil (1.27 mm). Component pins are +placed at multiples of 1.27 mm relative to the symbol origin, so absolute pin +coordinates end up as odd multiples of 1.27 mm (e.g. 26.67 mm = 21 × 1.27 mm). +These are valid on-grid positions that must not be moved. + +The coarser 2.54 mm (100-mil) grid is a common mistake: exactly half of all +valid 1.27 mm positions are not multiples of 2.54 mm and would be displaced by +1.27 mm — moving labels or wire endpoints off their pins and breaking +connectivity. + +Off-grid coordinates cause wires that appear visually connected to fail ERC +connectivity checks because KiCAD uses exact integer (IU) matching internally. """ import logging @@ -18,7 +27,7 @@ from sexpdata import Symbol logger = logging.getLogger("kicad_interface") -_DEFAULT_GRID_MM: float = 2.54 +_DEFAULT_GRID_MM: float = 1.27 # Element type names exposed in the public API _VALID_ELEMENTS = frozenset({"wires", "junctions", "labels", "components"}) @@ -91,7 +100,10 @@ def snap_to_grid( Args: schematic_path: Path to the ``.kicad_sch`` file. - grid_size: Grid spacing in mm (default 2.54 mm = 0.1 inch). + grid_size: Grid spacing in mm (default 1.27 mm = 50 mil). + Do NOT use 2.54 mm — half of all valid KiCAD pin + positions fall between 2.54 mm grid lines and would + be displaced 1.27 mm, breaking connectivity. elements: List of element types to snap. Valid values: ``"wires"``, ``"junctions"``, ``"labels"``, ``"components"``. Defaults to diff --git a/python/kicad_interface.py b/python/kicad_interface.py index 9e5ac46..f39e40b 100644 --- a/python/kicad_interface.py +++ b/python/kicad_interface.py @@ -3121,7 +3121,7 @@ class KiCADInterface: if not schematic_path: return {"success": False, "message": "schematicPath is required"} - grid_size = float(params.get("gridSize", 2.54)) + grid_size = float(params.get("gridSize", 1.27)) elements = params.get("elements") # None → defaults inside snap_to_grid result = snap_to_grid(Path(schematic_path), grid_size=grid_size, elements=elements) diff --git a/python/schemas/tool_schemas.py b/python/schemas/tool_schemas.py index d4eeac0..a71ed0c 100644 --- a/python/schemas/tool_schemas.py +++ b/python/schemas/tool_schemas.py @@ -1902,12 +1902,12 @@ SCHEMATIC_TOOLS = [ "gridSize": { "type": "number", "description": ( - "Grid spacing in mm. " - "Standard KiCAD schematic grid is 2.54 mm (0.1 inch). " - "Use 1.27 mm for high-density layouts. " - "Defaults to 2.54." + "Grid spacing in mm (default: 1.27 — standard KiCAD schematic grid). " + "Do NOT use 2.54: half of all valid KiCAD pin positions are at odd " + "multiples of 1.27 mm and would be displaced 1.27 mm, breaking " + "connectivity." ), - "default": 2.54, + "default": 1.27, }, "elements": { "type": "array", diff --git a/tests/test_snap_to_grid.py b/tests/test_snap_to_grid.py index 4089801..74e73a8 100644 --- a/tests/test_snap_to_grid.py +++ b/tests/test_snap_to_grid.py @@ -203,7 +203,7 @@ class TestSnapDefaults: path = _make_temp_schematic(extra) result = snap_to_grid(path) # defaults: grid=2.54, elements=None assert result["snapped"] >= 3 - assert result["grid_size"] == pytest.approx(2.54) + assert result["grid_size"] == pytest.approx(1.27) def test_idempotent(self): path = _make_temp_schematic(_wire_sexp(2.51, 5.03, 7.56, 5.03)) @@ -213,6 +213,23 @@ class TestSnapDefaults: content_after_second = path.read_text(encoding="utf-8") assert content_after_first == content_after_second + def test_default_grid_is_1_27mm(self): + # Regression: default was 2.54 mm, which displaces valid KiCAD pin + # coordinates that fall on the 50-mil (1.27 mm) grid but not on the + # 100-mil (2.54 mm) grid — e.g. 26.67 mm = 21 × 1.27 mm. + # With the correct 1.27 mm default those coordinates must be left + # untouched (snapped == 0, already_on_grid >= 1). + # 26.67 / 2.54 == 10.5 → would snap to 25.40 mm (off by 1.27 mm). + # 26.67 / 1.27 == 21.0 → already on grid, no move. + path = _make_temp_schematic(_wire_sexp(335.28, 26.67, 350.52, 26.67)) + result = snap_to_grid(path) # default grid + assert result["grid_size"] == pytest.approx(1.27) + assert result["snapped"] == 0, ( + "Wire at valid 50-mil pin coordinates was displaced by default snap — " + "default grid must be 1.27 mm, not 2.54 mm" + ) + assert result["already_on_grid"] >= 1 + def test_custom_grid(self): # 1.27 mm grid — wire at 1.25 should snap to 1.27 path = _make_temp_schematic(_wire_sexp(1.25, 1.25, 2.51, 2.51))