Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
# CLAUDE.md
akcli — AI-native KiCad schematic CLI (read/draw/check/review/sim; Altium is
import-only). Zero runtime dependencies is a hard rule: stdlib only, no new
packages in `[project.dependencies]` (`jlc` stays the only networked surface).
## Commands
A globally installed `akcli` shadows this checkout — always prefix with
`PYTHONPATH=src` (or work in an editable install):
```
PYTHONPATH=src python -m pytest tests/ -q # full suite
PYTHONPATH=src python -m pytest tests/test_ops.py -q # one file
PYTHONPATH=src python -m akcli <cmd> ... # run the CLI
python -m ruff check src tests tools hooks # lint (CI-exact scope)
python -m mypy --config-file pyproject.toml # typed beachhead (see [tool.mypy].files)
python3 tools/golden_regen.py # regenerate golden snapshots (review the diff!)
```
CI = ruff + mypy + pytest on ubuntu/macos/**windows** × py3.11–3.14, plus a
real-`kicad-cli` KiCad job, wheel smoke, hooks, plugin validate. **Local green
means nothing until ruff AND mypy AND pytest all pass** — mypy runs in the
same CI job as ruff and is the one people forget locally.
## Windows portability (the recurring release-breaker)
Every historically red release CI (0.4.0, 0.8.0, 0.9.0, 0.10.0) failed only on
`windows-latest`, always on platform text/process semantics. Rules:
- **Text I/O always pins `encoding="utf-8"` and, on writes, `newline="\n"`**
(Windows defaults: cp1252 locale codec + `\n` → `\r\n` translation, which
breaks byte-count assertions and output determinism). Enforced by
`tests/test_text_io_portability.py` — an AST gate over `src/akcli`,
`tools/` and `hooks/` (`_vendor` excluded but hand-patched); new
`write_text`/`read_text`/text-mode `open` calls must comply or CI fails.
- Artifact files whose bytes matter (`.kicad_sch`) are written in **binary**
(`os.fdopen(fd, "wb")` in `writers/atomic.py`) — keep it that way.
- Paths emitted into JSON/goldens use `as_posix()` (backslashes break
byte-stable snapshots).
- Subprocess: `shlex.split(..., posix=False)` semantics differ on Windows
(POSIX splitting eats `C:\...` backslashes — see
`hooks/pretooluse_draw_guard.py`); child processes need the real env
(`SYSTEMROOT`); only real executables can be spawned (no POSIX script
stubs — `WinError 193`).
- You cannot run Windows locally: treat any new file-I/O / subprocess /
path-formatting code as suspect until the CI matrix has seen it.
## Releases
Follow `docs/releasing.md`. Order is load-bearing: bump versions
(`pyproject.toml` + both plugin manifests), move CHANGELOG `[Unreleased]` →
`[X.Y.Z]`, push `main`, **wait for CI green on that commit, only then**
`git tag vX.Y.Z && git push origin vX.Y.Z`. The Release workflow now verifies
CI green on the tagged commit itself (`ci-green` job; `workflow_dispatch`
`force` is the documented bypass) — but tagging a red commit still wastes a
version number, so the manual wait stays. Release commits are squashed
feature batches, exactly the payload most likely to hide a Windows-only bug.
PyPI dist name is `akcli-kicad` (import package and CLI stay `akcli`). PyPI publishing uses trusted publishing (OIDC, no token), gated on the `PYPI_TRUSTED_PUBLISHING`
**repository** variable (Settings → Secrets and variables → Actions →
Variables — not an Environment variable: the job's `if:` is evaluated before
its environment resolves, so an environment-scoped variable there is silently
skipped); the `pypi` GitHub Environment must still exist for OIDC trusted
publishing to bind to (see `docs/releasing.md`). GitHub Releases publish
regardless.
## Op-vocabulary lockstep (adding/changing an op or macro)
**`src/akcli/opspec.py` is the ONE vocabulary source.** The JSON schemas, the
`akcli.ops` validator/template tables and the capability matrix are all
generated or derived views of it — there is nothing to keep in sync by hand.
Three steps, ONE commit:
1. Add/edit the `OpSpec` in `src/akcli/opspec.py`: fields (`name`, `kind`,
`required`, `template`, `coord` for group-local coordinates, `doc`),
cross-field `Constraint`s, capability flags (`kicad`/`altium`/`caps_note`,
core ops only), and the wiring name — `handler` for a core/sugar op
(a `@op`-decorated function in `src/akcli/writers/handlers/`, registered in
`writers/registry.py`) or `expander` for a macro (`src/akcli/ops/macros.py`).
A new `Field.kind` also needs a `_KIND_SCHEMA` row in `tools/schemas.py`
and a `_KIND_CHECKS` predicate in `src/akcli/ops/validate.py`; a new
enumerated vocabulary goes in `opspec.ENUMS` (order is load-bearing —
it is rendered straight into the schema).
2. `python3 tools/schemas.py build` — regenerates `schemas/ops.schema.json`
+ `ops.capabilities.json` from opspec AND refreshes the packaged mirror
`src/akcli/schemas/`. `python3 tools/schemas.py check` is the CI drift
gate (and `tests/test_ops_kit.py` runs the same comparison).
3. Docs count gates: every "N ops"/"N macros" claim (English + 中文 regexes) in
README×3 / ROADMAP / docs/ / skills/ / commands/ must match the registry
(`tests/test_docs_conformance.py`); `tests/test_ops.py` census; the op-name
list in `commands/circuit-draw.md`. Also update the op-pattern prose in
`skills/akcli-schematic-authoring/SKILL.md` (+ the op enumeration in
`skills/akcli-circuit-design/SKILL.md`) in the same commit — the count gate
can't see a missing pattern writeup, and the skills have drifted behind
`commands/circuit-draw.md` before.
New error codes need `errors.py` `ERROR_CODES` + `_CODE_EXIT` +
`REMEDIATION` (1:1, enforced) + the census in `tests/test_errors.py`.
## New top-level CLI command checklist (adding an `akcli <verb>`)
Less mechanically enforced than the op lockstep above, but skipping a step
ships a command that is undiscoverable or untested:
1. `src/akcli/commands/<name>.py` with a `register(sub, common) -> None`
(mirror an existing module) + lazy-imported handlers; wire the import AND
the `_xxx_cmd.register(sub, common)` call into `build_parser()` in
`cli.py`. `akcli capabilities` covers it for free — introspected from the
live parser, asserted by `test_manifest_covers_every_subcommand`
(`tests/test_capabilities.py`) — no manual registry to update.
2. `docs/cli-reference.md`: a new `### akcli <verb> ...` section with AT
LEAST one fenced ```` ```bash ```` example line. `test_fenced_commands_are_valid`
(`tests/test_docs_conformance.py`) shlex-splits every fenced `akcli …`
line in the docs against the live parser, so an unknown flag fails CI —
but only once the example exists to be checked.
3. Add a row to the README `| Command | What it does |` table if the verb
joins the primary workflow surface (not test-gated — a discoverability
courtesy).
4. Decide whether the command also needs a companion slash command
(`commands/<verb>.md`) and/or a teaching skill
(`skills/akcli-*/SKILL.md`). If you add either, the skills/commands doc
count gate (`tests/test_docs_conformance.py`) checks every "N
skills"/"N commands" claim against the live `skills/*/SKILL.md` /
`commands/*.md` glob counts — update every claim (README×3, INSTALL,
ROADMAP, docs/codex-plugin.md) in the SAME commit.
5. Tests: exercise the handler in `tests/test_cli.py` (or a dedicated
`tests/test_<name>.py`) for exit code + stdout/stderr split, and
`--json` output — a hand-built `_dumps({...})` payload must carry a
version stamp or be wrapped in `_stamp(...)`, enforced by the AST gate in
`tests/test_json_version_stamps.py`.
## Invariants (verified by tests — don't regress)
- **Byte-identical re-apply**: deterministic UUIDv5 seeded from root uuid +
the node's own CONTENT — `designator`, `tag:coords`, `label:name:anchor`,
`sheet:file:name`; **never the op index**, so an op-list edit keeps every
untouched node's identity — plus `_append_top_idempotent` replace-in-place
and a per-apply claim registry that refuses two ops seeding one uuid
(annotation `key`s ignore coords too). Every new op needs an apply-twice
byte-equality test AND an insert-op-then-reapply no-duplicate test.
- **Connectivity is the only hard write gate**; geometry/layout findings are
advisory. `arrange`/group moves must stay net-preserving (before/after
`netdiff.equivalent` asserted).
- **Netdiff neutrality**: graphics ops, properties, title block never touch
nets (netbuild ignores them) — assert it for anything new.
- KiCad grammar claims are **fixture-first**: prove a new S-expression shape
against a real `kicad-cli` (e2e test, skip-gated on availability) before
shipping; committed ground truth lives in `tests/fixtures/kicad/`.
- Renderer/JSON outputs are deterministic (same input bytes → same output
bytes) — golden snapshots in `tests/golden/` catch drift; regenerate only
deliberately.
## Key layout
`src/akcli/opspec.py` (THE op vocabulary, as data) · `src/akcli/ops/`
(`vocab` derived from opspec · `validate` · `macros` · `groups` · `template`) ·
`writers/kicad.py` (executor) · `writers/geometry.py` (transform core —
`pin_world` is canonical) · `readers/sexpr.py` + `readers/kicad*.py` ·
`netbuild.py`/`netdiff.py` (net engine + diff rails) · `checks/` ·
`arrange.py`/`groupframe.py` (layout/groups) · `commands/` (one module per CLI
family, registered in `cli.py`) · docs: `docs/op-list-authoring.md` (authoring
bible), `docs/SPEC.md`, `docs/cli-reference.md`.
2126c3284This file is pinned to the selected release. Download the original to open it in your design tool.