AGENTS.md · diff

git:20260830.859f419 to git:20260908.4fb92cd

1 added, 1 removed. Audit A to A.

# CLAUDE.md
CoHDL v2 — a compiler for the AI-native PCB schematic language. Rust, single crate. The compiler pipeline and emitters have **zero external dependencies** (a deliberate choice: byte-stable, reproducible output is a hard constraint, so everything — TOML lock file, S-expression netlist, diagnostics JSON — is hand-rolled and deterministic). One scoped exception per RFC-014/DR-020: the LSP layer (`src/lsp.rs`) depends on pinned `lsp-types` (+ its `serde`/`serde_json` requirements) for the protocol's message shapes only — the JSON-RPC transport loop stays hand-rolled, and nothing outside the LSP layer may use these crates.
The previous implementation is on the `legacy` branch. It is reference material only — v2 is a ground-up rebuild; do not port legacy code.
## Source of truth
The language design lives in `docs/design/` (snapshot of the conol.ai design repository — see `docs/design/README.md`). Binding order:
1. `docs/design/10-language-specification.md` — what the language IS (Accepted RFCs only)
2. `docs/design/rfc-001…007-*.md` — the seven Accepted P0 RFCs (normative detail)
3. `docs/design/09-mvp-definition.md` — current scope line + exit criteria
4. `docs/provisional-syntax.md` — MVP-pragmatic choices for constructs with **no Accepted RFC yet** (part/MPN, net annotations, modules). These are provisional; an RFC on conol.ai supersedes them. (Pin roles and package variants graduated to RFC-008.)
- Never implement beyond the MVP cut list in `09-mvp-definition.md` (no incremental compilation; the "no LCEDA" cut was superseded by user direction 2026-08-27 — see the `.enet` paragraph below). Originally-cut items that have since graduated via Accepted RFCs and ARE implemented: structural-variant pattern matching (RFC-008), `cohdl fmt` canonical form (RFC-009), `cohdl check/build --json` (RFC-010), the formal error-code registry (RFC-011), `#[intent("...")]` opaque-metadata annotations (RFC-012), layout constraints — `layout {}` + `#[placement_hint]` → `layout.json`, E10xx checks (RFC-013), the LSP server `cohdl lsp` (RFC-014), the IPC-2581 handoff document `build --emit ipc2581` (RFC-015; contract in `docs/ipc2581.md`, schema gate needs `xmllint`), the module system (RFC-016: file-tree module paths, `use` imports, cross-package `pub` enforcement, E207-E209; references are REWRITTEN to fully-qualified names in `resolve.rs` — downstream maps are fq-keyed, `resolve::short()` for display), the library registry (RFC-017: `#[doc]` reference documents + `footprint` as a resolvable declaration kind — `footprint:` in parts is a symbol reference, never a string), and the pad/footprint format (RFC-018: `pad` declarations with a closed shape/layer/plating vocabulary, `pad N: Sym at (x, y)` placements + courtyard/silkscreen_ref in footprint bodies, the `Length`/`mm` unit as RFC-001's eleventh type, E805-E807 — pad-vs-device consistency checked at BUILD, empty footprints exempt as stage-one placeholders — and geometry projection into `out/footprints/*.kicad_mod` + IPC-2581 `Pin` elements; starter pads in `std/pads.cohdl`, remaining placeholders in `std/footprints.cohdl`), mechanical locating holes (RFC-022: `mount_hole N: PLATING at (x, y) diameter D`, disjoint from pad numbering, E810; RFC-023 extends it with an optional `shape:` reusing RFC-018's `PadShape` set plus a shape-dependent geometry field — `diameter D` for a circle, `size: (w, h)` for rect/oval, absent `shape:` defaults to circle), and array-typed instances (RFC-024, REDESIGNED from its own withdrawn first draft: `inst NAME: [Device; N]` declares ONE array-typed instance whose N elements are each fully real, addressed as `NAME[i]` — 0-based — EVERYWHERE an ordinary instance reference is valid: net members, `place`, `fn`-call args. A bare unindexed `NAME` is never a valid reference. Range/list fan-out `NAME[a..=b].PIN` / `NAME[i, j].PIN` survives as sugar over `NAME[i]`, still net-member-only. Elements are internally `NAME_0`…`NAME_{N-1}`; expanded in `check/expand.rs`, E202 for out-of-bounds, E211 for malformed/misplaced; does NOT solve daisy-chain wiring or arithmetic-derived `place` data), rotated pad placements (RFC-025: `pad N: Sym at (x, y) [rotate ANGLE]`, E811; KiCad 3-arg `(at x y angle)`, size never swapped), and back-side placement (RFC-026: `place … [rotate ANGLE] [side top|bottom]`, E1008; layout.json `"side"` only when bottom, IPC `layerRef="B.Cu"` + Xform mirror, pad x mirrors BEFORE rotation matching pcbnew Flip-then-orient; kicad_board.py must Flip only AFTER board.Add or headless pcbnew segfaults), and Quilter physics-constraint hints (RFC-027: seven structured attributes — `#[ground]` `#[high_current]` `#[impedance]` on nets, `#[bypass]` `#[crystal_oscillator]` `#[switching_converter]` `#[bga_fanout]` on insts — plus a `diff_pair(...) […]` physics bracket; E1009 structural / E110 unit mismatches; `build` emits the eight-file Quilter CSV set matching the supplied templates exactly — mA/nF/ohm/GHz scales, pad-number pins, multi-pad pins flatten to one row per pad — only when the design carries physics facts; RFC-028 lets `#[bypass]` target a bare `Pin`-typed fn parameter and the crystal/converter attributes take `Instance`-typed fn parameters, resolved per call site via `Binding::Pin`/`Binding::Instance` — one attribute inside `fn decouple` annotates every call site), package dependency versioning (RFC-029: a real `[dependencies]` manifest section — exact `X.Y.Z` versions ONLY, range syntax rejected permanently at parse — plus `cohdl.lock` recording each dependency's version AND sha256 content hash, re-verified on every check/build BEFORE any .cohdl parsing; hash mismatch = hard error. std is now an ordinary package (`cohdl.toml` + `src/`) — std is NOT special, and a package's version comes ONLY from its manifest's `[package] version`; directory names are convention, never mechanism (a registry family dir holds side-by-side versions in arbitrarily-named subdirs, discovered by reading their manifests; duplicate identity = hard error). `cohdl update [--dep NAME]` is the only sanctioned pin change (and migrates pre-RFC-029 manifests); `--std`/`COHDL_STD` survive as dev overrides behind the unsuppressable E1105 warning; E1101-E1108 in a new error block; `fmt` canonicalizes `[dependencies]` sorted; the pipeline's package set is explicit — `check_files_in_with_deps` — with dep files displayed `<name>/…`. Amendment (user-directed 2026-08-25): resolution walks the TRANSITIVE closure — every resolved package's own `[dependencies]` joins the compile and the lock records the closure; the root manifest's pin is the single authority (wins silently over any dep's pin), divergent dep pins with no root pin are the E1108 hard error (help: pin at the root to choose), check/build/LSP stay offline (E1102 names the requirer, "run `cohdl install`") while `install`/`update`/`add` fetch missing closure members, and under `--no-std`/`--std` a dep's std pin is skipped — std is settled outside the registry there, an escape of caller context, never of the name), the package registry (RFC-030: registry.cohdl.org — the RFC specifies the EXTERNAL CONTRACT only; the closed three-tier namespace is structural in the name itself — bare = CoHDL official, `@brand/name` = verified manufacturer, `@contrib/name` = community — validated client-side pre-flight AND server-side authoritative; CLI verbs `login/publish/add/remove/install/update` compose with RFC-029's lock mechanism unchanged — the SERVER's recomputed content hash is what cohdl.lock records; transport = the system `curl` (no crate dependency), archive = deterministic uncompressed tar (the RFC's ".tar.gz (or equivalent)"), cache = `~/.cohdl/registry` as an extra RFC-029 registry family; E1201-E1206 in their own block, distinct from E11xx by design — E1204 unreachable is NEVER conflated with E1103 hash mismatch; server implementation in `registry/` — Cloudflare Workers + D1 + R2 + KV + Workers Assets, Vite/TanStack/React UI, standalone like `editors/vscode`, root CI does not build it; a published version also carries `[package] license/description/repository` (per-version — the newest version is what "package-level" derives from) and an index of its RFC-017 `#[doc]` documents, which the package page renders through a no-raw-HTML Markdown subset. Registry policy: **`license` is REQUIRED to publish** — refused server-side (400 → E1202) and in the CLI pre-flight before packing, with no license-list validation (proprietary terms are fine; `""` is silence). None of these keys affect a verdict or an emitted byte), and the VS Code extension (RFC-019/DR-025: a real installable extension at `editors/vscode/` — TextMate grammar + `vscode-languageclient` wiring over `cohdl lsp`, `cohdl.path` setting; TypeScript/Node package with its own CI job and grammar-coverage test — ZERO compiler changes; the dependency-free constitution scopes the Rust crate, not this editor package; live-VS-Code session is a human checkpoint like KiCad). and silkscreen graphics for footprints (RFC-031: a `silkscreen { … }` footprint-body block — four closed primitives `line`/`circle`/`arc`/`polygon` plus two semantic marker shorthands `pin_1_marker near pad N shape dot|triangle` and `polarity_marker cathode_pin N shape band|arrow` that EXPAND to checked primitives in `emit::silk` — one expansion shared by both emitters so they cannot drift; E812; standoff is measured from the pad's EDGE, not its centre, or the mark would sit on the copper it marks; KiCad gets native `fp_line`/`fp_circle`/`fp_arc`/`fp_poly` on F.SilkS, and IPC-2581 gains its FIRST silkscreen output — every primitive reduced to a filled `Contour` polygon because `Features` accepts only a `StandardShape` and `Line`/`Arc` are `Simple`). RFC-001…031 are Accepted and implemented. Deviations from accepted text are ledgered in `docs/compliance-report.md`.
+ Never implement beyond the MVP cut list in `09-mvp-definition.md` (no incremental compilation; the "no LCEDA" cut was superseded by user direction 2026-08-27 — see the `.enet` paragraph below). Originally-cut items that have since graduated via Accepted RFCs and ARE implemented: structural-variant pattern matching (RFC-008), `cohdl fmt` canonical form (RFC-009), `cohdl check/build --json` (RFC-010), the formal error-code registry (RFC-011), `#[intent("...")]` opaque-metadata annotations (RFC-012), layout constraints — `layout {}` + `#[placement_hint]` → `layout.json`, E10xx checks (RFC-013), the LSP server `cohdl lsp` (RFC-014), the IPC-2581 handoff document `build --emit ipc2581` (RFC-015; contract in `docs/ipc2581.md`, schema gate needs `xmllint`), the module system (RFC-016: file-tree module paths, `use` imports, cross-package `pub` enforcement, E207-E209; references are REWRITTEN to fully-qualified names in `resolve.rs` — downstream maps are fq-keyed, `resolve::short()` for display), the library registry (RFC-017: `#[doc]` reference documents + `footprint` as a resolvable declaration kind — `footprint:` in parts is a symbol reference, never a string), and the pad/footprint format (RFC-018: `pad` declarations with a closed shape/layer/plating vocabulary, `pad N: Sym at (x, y)` placements + courtyard/silkscreen_ref in footprint bodies, the `Length`/`mm` unit as RFC-001's eleventh type, E805-E807 — pad-vs-device consistency checked at BUILD, empty footprints exempt as stage-one placeholders — and geometry projection into `out/footprints/*.kicad_mod` + IPC-2581 `Pin` elements; starter pads in `std/pads.cohdl`, remaining placeholders in `std/footprints.cohdl`), mechanical locating holes (RFC-022: `mount_hole N: PLATING at (x, y) diameter D`, disjoint from pad numbering, E810; RFC-023 extends it with an optional `shape:` reusing RFC-018's `PadShape` set plus a shape-dependent geometry field — `diameter D` for a circle, `size: (w, h)` for rect/oval, absent `shape:` defaults to circle), and array-typed instances (RFC-024, REDESIGNED from its own withdrawn first draft: `inst NAME: [Device; N]` declares ONE array-typed instance whose N elements are each fully real, addressed as `NAME[i]` — 0-based — EVERYWHERE an ordinary instance reference is valid: net members, `place`, `fn`-call args. A bare unindexed `NAME` is never a valid reference. Range/list fan-out `NAME[a..=b].PIN` / `NAME[i, j].PIN` survives as sugar over `NAME[i]`, still net-member-only. Elements are internally `NAME_0`…`NAME_{N-1}`; expanded in `check/expand.rs`, E202 for out-of-bounds, E211 for malformed/misplaced; does NOT solve daisy-chain wiring or arithmetic-derived `place` data), rotated pad placements (RFC-025: `pad N: Sym at (x, y) [rotate ANGLE]`, E811; KiCad 3-arg `(at x y angle)`, size never swapped), and back-side placement (RFC-026: `place … [rotate ANGLE] [side top|bottom]`, E1008; layout.json `"side"` only when bottom, IPC `layerRef="B.Cu"` + Xform mirror, pad x mirrors BEFORE rotation matching pcbnew Flip-then-orient; kicad_board.py must Flip only AFTER board.Add or headless pcbnew segfaults), and Quilter physics-constraint hints (RFC-027: seven structured attributes — `#[ground]` `#[high_current]` `#[impedance]` on nets, `#[bypass]` `#[crystal_oscillator]` `#[switching_converter]` `#[bga_fanout]` on insts — plus a `diff_pair(...) […]` physics bracket; E1009 structural / E110 unit mismatches; `build` emits the eight-file Quilter CSV set matching the supplied templates exactly — mA/nF/ohm/GHz scales, pad-number pins, multi-pad pins flatten to one row per pad — only when the design carries physics facts; RFC-028 lets `#[bypass]` target a bare `Pin`-typed fn parameter and the crystal/converter attributes take `Instance`-typed fn parameters, resolved per call site via `Binding::Pin`/`Binding::Instance` — one attribute inside `fn decouple` annotates every call site), package dependency versioning (RFC-029: a real `[dependencies]` manifest section — exact `X.Y.Z` versions ONLY, range syntax rejected permanently at parse — plus `cohdl.lock` recording each dependency's version AND sha256 content hash, re-verified on every check/build BEFORE any .cohdl parsing; hash mismatch = hard error. std is now an ordinary package (`cohdl.toml` + `src/`) — std is NOT special, and a package's version comes ONLY from its manifest's `[package] version`; directory names are convention, never mechanism (a registry family dir holds side-by-side versions in arbitrarily-named subdirs, discovered by reading their manifests; duplicate identity = hard error). `cohdl update [--dep NAME]` is the only sanctioned pin change (and migrates pre-RFC-029 manifests); `--std`/`COHDL_STD` survive as dev overrides behind the unsuppressable E1105 warning; E1101-E1108 in a new error block; `fmt` canonicalizes `[dependencies]` sorted; the pipeline's package set is explicit — `check_files_in_with_deps` — with dep files displayed `<name>/…`. Amendment (user-directed 2026-08-25): resolution walks the TRANSITIVE closure — every resolved package's own `[dependencies]` joins the compile and the lock records the closure; the root manifest's pin is the single authority (wins silently over any dep's pin), divergent dep pins with no root pin are the E1108 hard error (help: pin at the root to choose), check/build/LSP stay offline (E1102 names the requirer, "run `cohdl install`") while `install`/`update`/`add` fetch missing closure members, and under `--no-std`/`--std` a dep's std pin is skipped — std is settled outside the registry there, an escape of caller context, never of the name), the package registry (RFC-030: registry.cohdl.org — the RFC specifies the EXTERNAL CONTRACT only; the closed three-tier namespace is structural in the name itself — bare = CoHDL official, `@brand/name` = verified manufacturer, `@contrib/name` = community — validated client-side pre-flight AND server-side authoritative; CLI verbs `login/publish/add/remove/install/update` compose with RFC-029's lock mechanism unchanged — the SERVER's recomputed content hash is what cohdl.lock records; transport = the system `curl` (no crate dependency), archive = deterministic uncompressed tar (the RFC's ".tar.gz (or equivalent)"), cache = `~/.cohdl/registry` as an extra RFC-029 registry family; E1201-E1206 in their own block, distinct from E11xx by design — E1204 unreachable is NEVER conflated with E1103 hash mismatch; server implementation in `registry/` — Cloudflare Workers + D1 + R2 + KV + Workers Assets, Vite/TanStack/React UI, standalone like `editors/vscode`, root CI does not build it; a published version also carries `[package] license/description/repository` (per-version — the newest version is what "package-level" derives from) and an index of its RFC-017 `#[doc]` documents, which the package page renders through a no-raw-HTML Markdown subset. Registry policy: **`license` is REQUIRED to publish** — refused server-side (400 → E1202) and in the CLI pre-flight before packing, with no license-list validation (proprietary terms are fine; `""` is silence). None of these keys affect a verdict or an emitted byte), and the VS Code extension (RFC-019/DR-025: a real installable extension at `editors/vscode/` — TextMate grammar + `vscode-languageclient` wiring over `cohdl lsp`, `cohdl.path` setting; TypeScript/Node package with its own CI job and grammar-coverage test — ZERO compiler changes; the dependency-free constitution scopes the Rust crate, not this editor package; live-VS-Code session is a human checkpoint like KiCad). and silkscreen graphics for footprints (RFC-031: a `silkscreen { … }` footprint-body block — four closed primitives `line`/`circle`/`arc`/`polygon` plus two semantic marker shorthands `pin_1_marker near pad N shape dot|triangle` and `polarity_marker cathode_pin N shape band|arrow` that EXPAND to checked primitives in `emit::silk` — one expansion shared by both emitters so they cannot drift; E812; standoff is measured from the pad's EDGE, not its centre, or the mark would sit on the copper it marks; KiCad gets native `fp_line`/`fp_circle`/`fp_arc`/`fp_poly` on F.SilkS, and IPC-2581 gains its FIRST silkscreen output — every primitive reduced to a filled `Contour` polygon because `Features` accepts only a `StandardShape` and `Line`/`Arc` are `Simple`)., and typed logical composition (RFC-032: `subdesign` — a new declaration kind, a peer of `device`/`trait`/`fn`/`part`/`footprint`, resolved through RFC-016 unchanged. Explicit `Pin`-typed ports with RFC-002 obligations; RFC-007 generics and RFC-024 arrays reused VERBATIM at `subdesign name: Type<…>` use sites; retained hierarchy paths `Design::use::inst` feed RFC-005 unchanged; the container itself never becomes a component/designator/BOM row — ports resolve to phantom `(node, PORT)` members that merge net equivalence classes and are stripped at assembly. An internal `layout {}` is a DEFAULT the outer design transforms as one unit (`place use_site at … [rotate] [side]` — the RFC-025/026 mirror-x-then-rotate math via trig.rs, local rotation REVERSED on bottom) or overrides per instance through the dotted reach-in path (`place a.c_in at …`), the SOLE internals exception — `net`/`spec` stay behind ports. E1301-E1307; containment cycles checked at declaration (E1304, full cycle named); use sites in `fn` bodies rejected (E1307). Implementation: `check/subdesigns.rs` + `bodies.rs` shim (ports as Pin-params) + `expand.rs` (nodes in pass 1, port conns in pass 2, `compose_place` finalization). fmt deviation, ledgered: statement order is preserved (ports block canonicalized in place); layout-last is convention, not enforced reordering). RFC-001…032 are Accepted and implemented. Deviations from accepted text are ledgered in `docs/compliance-report.md`.
The RFC-018 parenthetical above records its original landing paths. In the
live tree `lib/std/src/` contains only `prelude.cohdl`; pads and footprints
are owned by the focused component packages listed below.
**Live deviation — arbitrary rotation angles.** `rotate ANGLE` (both `place` and `pad`) accepts ANY whole degree in `0..=359`, not RFC-020's closed `{0, 90, 180, 270}`. Directed by the board author; RFC-020's text (and its "not arbitrary-angle rotation" non-goal) is superseded pending an RFC. `360` and above stays an error — a full turn is `0`, and `rotate 450` is likelier a slip than a deliberate 90. The enabling piece is `src/trig.rs`: a checked-in fixed-point sine table (`tools/gen_trig.py`, scale 2^64, quadrant symmetry off 0..=90) plus a 256-bit-intermediate multiply. **Never use `f64::sin`/`cos` for placement geometry** — it resolves to the platform libm, whose last bit is not portable, and emitted coordinates are byte-stability-critical. The table is exact at the four cardinals, so every pre-deviation board still emits identical bytes (regression-tested against both examples). `emit::silk`/`emit::kicad_mod` do use `f64` for arc tessellation; that precedent deliberately does NOT extend to rotation.
**Distribution (no RFC — repository tooling, user-directed 2026-08-11).** Compiler releases are `vX.Y.Z` tags: `.github/workflows/release-cohdl.yml` gates on tag == `[package] version` + `cargo test`, builds `[profile.dist]` (stripped; `[profile.release]` keeps `debug = true` for local profiling) for five targets (mac arm64/x86_64, linux musl x86_64/aarch64 — static, one artifact per arch runs on every distro — and windows x86_64), and publishes `cohdl-vX.Y.Z-<target>.tar.gz` + `sha256sums.txt` to a GitHub release. `install.sh` (repo root, curl-pipe-sh) and `cohdl self-update` (`src/selfupdate.rs` — system curl per the RFC-030 precedent, system tar for the compressed archive since RFC-030's own hand-rolled tar is deliberately uncompressed, verification via `src/hash.rs`) consume that artifact contract; the three are ONE contract — change them together. Since v0.5.0 the two macOS binaries are Developer ID-signed (hardened runtime + secure timestamp, identity `4F93FJA939`) and notarized in the release workflow before packaging — the `APPLE_CERT_P12`/`APPLE_CERT_PASSWORD`/`APPLE_API_KEY_P8`/`APPLE_API_KEY_ID`/`APPLE_API_ISSUER` repo secrets; the p12 bundles its intermediate CA, the throwaway-keychain import needs a real p12 password (`security` rejects an empty-password MAC), a bare executable cannot be stapled so Gatekeeper checks the ticket online, and the notarize step gates on the literal `status: Accepted` line because notarytool can exit 0 on a non-Accepted verdict. Artifact names are untouched — the three-place contract below is unchanged. The VS Code extension's releases (`vscode-v*`) pass `--latest=false` so the repository's "latest" is always a compiler release. The repo is public; v0.3.0 (2026-08-17) is the first shipped compiler release, verified live end-to-end (one-liner install + `self-update --check`). The repository's v0.1.0/v0.2.0 releases predate the rebuild and carry old-scheme artifacts (unversioned names, linux-gnu, no `sha256sums.txt`) — compiler versions count up from 0.3.0, and `install.sh` probes each candidate release for this platform's contract asset before selecting it, so legacy (and still-uploading) releases are skipped, never chosen as the numeric maximum.
**Package API docs (no RFC — registry tooling, user-directed 2026-08-19; contract in `docs/apidocs.md`).** docs.rs-style per-version documentation on registry.cohdl.org: the REAL compiler extracts a deterministic `schema_version: 1` JSON (`cohdl docs` → `src/emit/docsjson.rs` — every named top-level declaration + impls + a `foreign` section inlining dependency-owned pads/footprints/devices the previews need; geometry as canonical `geom::mm` strings; silkscreen markers pre-expanded via the SAME `emit::silk::graphics` both geometry emitters consume; `World.intents` is the one additive resolver change). The document travels as a SIDECAR — `PUT /packages/{name}/{version}/docs`, owner-authenticated, ≤200 MB (200,000,000 bytes), re-uploadable — and must NEVER enter the published tar (the tar is the RFC-029 hash identity; RFC-030 line 47). `cohdl publish` uploads it best-effort after a successful publish (extraction needs a cleanly-checking package; failure warns and never fails the publish — publish itself stays compile-free); `cohdl docs --publish` is the backfill for already-published versions. Server: byte-preserved, content-addressed R2 sidecar objects selected through D1 `versions.api_docs_r2_key` (legacy fixed-key fallback; `api_docs_size` remains the presence/size flag), `GET /api/apidocs?pkg&version`, `api_docs` boolean on package-detail version rows. Through 16,000,000 bytes the Worker fully parses and projects part-search rows; larger compact canonical documents stream with fixed length and R2-verified SHA-256, and deliberately carry no search projection. The application cap is distinct from Cloudflare's plan-dependent request-body ceiling. UI: an **API** tab on the package page (`?view=api`, `item=`, `q=`, `kind=` search params) with an item explorer, auto-drawn schematic-symbol SVGs (side assignment from RFC-008 pin roles is a tooling-layer convention, not a language guarantee), and exact footprint SVG previews (authoring frame is y-down = SVG's; pad rotation renders as `rotate(-angle)`). API docs are fetched only when that tab or an item deep link is selected and released promptly after it is left. E12xx codes are reused, none added. Zero impact on every existing artifact/verdict is pinned by `tests/apidocs.rs`.
**Registry search (RFC-030/DR-036 amendment, user-directed 2026-08-24).** `cohdl search QUERY [--json]` is a project-free, login-free, read-only client of stable `GET /search`. QUERY is trimmed, 3+ Unicode scalar values, ≤128 UTF-8 bytes, and contains no control characters. The endpoint has separate package/part result families and per-family `has_more` — never total counts — with `kind=all|package|part`, `limit=1..50` (default 20), and `offset=0..10000` (default 0, applied independently). Only the most-recently-published version is searched. Part rows come from that version's package-local `pub part` API-doc `items`, NEVER private or `foreign` items; FQ ownership is re-derived from the uploading package, and fixed projection budgets bound hostile excess while preserving the raw sidecar. Search covers package/FQ/short name, device, intent, args, variant, and primary/alternate AVL fields within those bounds; re-upload replaces that version's rows. Each existing package's most-recent sidecar needs one `cohdl docs --publish` backfill after the search index lands. The browser's `/api/search` stays package-only and unchanged. Human and `--json` modes carry identical bounded rows; `--json` is the `/search` discovery shape, NOT RFC-010 diagnostics. No hits = exit 0; invalid query = exit 2; registry/protocol failure = E1204/exit 1.
**Native `.kicad_pcb` emission (no RFC — emitter tooling, user-directed 2026-08-25; contract in `docs/kicad_pcb.md`).** `build --emit kicad_pcb` writes `out/<name>.kicad_pcb` — a KiCad 10 board file (`(version 20260206)`, pad nets BY NAME, no net table) straight from the IR: placements verbatim (CoHDL's authoring frame IS KiCad's +y-down board frame — never copy the IPC emitter's y-negation), net-bound embedded footprints from the SAME shared derivation as `.kicad_mod` (`kicad_mod::pad_plans`/`body_graphics` — one derivation, two dialects, RFC-031's anti-drift shape), the RFC-020 outline on Edge.Cuts (winding-aware arc midpoints), deterministic sha256-derived uuids, and IPC-shelf/40mm-grid staging for unplaced parts. RFC-026 back side in KiCad's own on-disk encoding, pinned empirically against pcbnew-written boards: fp angle = R+180 in (−180,180], local y negated everywhere, F.*→B.*, `justify mirror`, pad-local rotation REVERSED, chamfer corners swap vertically — semantic parity verified live against pcbnew output for both examples and both OpenMicroKBD revisions (v1: 88 footprints / 33 bottom; v2: 140 / 53). `--emit` is now repeatable with distinct values (same value twice stays the F12.5 duplicate error). This retires the `tools/kicad_board.py`/pcbnew dependency (script kept for reference); a routed board is protected only while FOREIGN to `out/.cohdl-manifest` — route on a copy outside `out/`, never in place.
**Schematic explorer (no RFC — standalone tooling, merged from `feat/explorer` 2026-08-27; details in `explorer/README.md`).** `explorer/` is a read-only, live-reloading schematic projection of a checked design: a Rust extractor crate (`explorer/extractor/` — compiler pipeline → deterministic ExplorerModel JSON v1, plus a std-only hand-rolled HTTP + SSE `--serve` mode that binds loopback only) and a React/ELK frontend (`explorer/web/`). Like `registry/` and `editors/vscode/`, the extractor is a standalone crate OUTSIDE the compiler's zero-dependency rule (path-dep on `cohdl`, serde for JSON) with its own CI job; it is read-only against `cohdl.lock`/`design.lock` and resolves deps in the offline check/LSP shape (`ResolveOpts { skip_transitive: &[], fetch: None }`). Hot reload is two composed loops: the watcher re-extracts on `.cohdl`/`cohdl.toml` mtime change (~500 ms; parser error recovery keeps the full design with span-exact diagnostics riding in the model, and a hard extraction failure keeps the last-known-good model with a `live_error` overlay), and `npm run dev` (port 5198) proxies `/api` to the extractor (5199) for frontend HMR. AI writes ONLY the `views/<Design>.view.json` partition labels (`skills/view-gen/SKILL.md`) — a bad view file changes grouping, never topology. The explorer also ships as a signed+notarized+STAPLED universal macOS app (`CoHDL Explorer.app` in a DMG): `explorer-vX.Y.Z` tags (version = `extractor/Cargo.toml`) drive `.github/workflows/release-explorer.yml`, which reuses the compiler release's APPLE_* secrets and publishes `--latest=false` like the VS Code track; the bundle pieces live in `explorer/app/` (Info.plist with `LSUIElement`, icns from the brand icon, `make_dmg.sh`), and app mode is `src/app.rs` — triggered by the `.app/Contents/MacOS/` path with no argv, osascript folder picker, free-port serve, browser open, and an SSE-viewer idle exit (5 min after the last tab closes) so the Dock-less app never lingers.
**EasyEDA / LCEDA Pro netlist (no RFC — emitter tooling, user-directed 2026-08-27; contract in `docs/easyeda.md`).** `build --emit easyeda` writes `out/<name>.enet` — the Constitution's second named netlist target (LCEDA Pro JSON v2.0.0, **File → Import → Netlist**), whose MVP cut ("one faithful target is enough proof") is hereby superseded by the board author. A NETLIST, not a board: geometry stays with `kicad_pcb`/`ipc2581`, and all three `--emit` values compose. The byte shape is pinned against the v1 compiler's emitter (`legacy` branch, `crates/cohdl-codegen-lceda` — reference for the FORMAT only, per the no-porting rule): v1's top-level struct order, string-sorted object keys everywhere (v1's serde_json BTreeMap bytes), `gge<n>` Unique IDs counting up in designator natural order. Semantics are the `.net` re-projected through the SAME shared derivations (`kicad::principal_value`, footprint symbol fq path, `Device::pins_for` physical expansion — the two netlists cannot disagree; cross-checked in `tests/easyeda.rs`); multi-pad logical pins flatten to one `pinInfoMap` row per pad (the RFC-027 convention), `nc` pins are guaranteed-absent (DR-012), a pin-less mechanical part keeps an empty map, and every net gets a `designRule.netRule` row with the empty `TrackPhysics` binding. Hand-rolled JSON via `emit::json::json_str` — zero dependencies. A live LCEDA Pro import is the human checkpoint, as pcbnew is for `kicad_pcb`.
## Commands
- `cargo build` / `cargo test` — build and run all tests
- `cargo run -- check <file-or-dir> [--json]` — parse + resolve + type-check + residual DRC (RFC-010 `--json`)
- `cargo run -- build <file-or-dir> [--json] [--emit easyeda] [--emit ipc2581] [--emit kicad_pcb]` — check + emit KiCad `.net` + BOM CSV (+ `design.lock`; `--emit easyeda` adds the LCEDA Pro `.enet` netlist, `--emit ipc2581` the RFC-015 partner document, `--emit kicad_pcb` the native KiCad 10 board; `--emit` repeats with distinct values)
- `cargo run -- update <dir> [NAME|--dep NAME]` — re-resolve to the greatest published semantic version (registry first, local fallback), rewrite `[dependencies]` + `cohdl.lock` (RFC-029/030; also migrates a manifest with no `[dependencies]`)
- `cargo run -- add NAME[@X.Y.Z] <dir>` / `remove NAME <dir>` / `install <dir>` / `login` / `publish <dir>` — the RFC-030 registry verbs (COHDL_REGISTRY overrides registry.cohdl.org; COHDL_HOME overrides `~/.cohdl`)
- `cargo run -- search QUERY [--json]` — search registry packages + most-recently-published public parts; no project or login, bounded results with per-family `has_more`
- `cargo run -- docs <dir> [--out FILE] [--publish]` — emit the deterministic package API-docs JSON (docs/apidocs.md; feeds the registry's API explorer) to stdout/FILE, or upload it to the registry for the already-published version (`--publish` is the backfill path; `cohdl publish` uploads automatically)
- `cargo run -- self-update [--check]` — replace the running binary with the newest `vX.Y.Z` GitHub release (`--check` reports only); `cohdl --version` prints version + release target triple
- `cargo run -- fmt <file-or-dir> [--check]` — rewrite `.cohdl` into canonical form (RFC-009)
- `cargo run -- lsp` — the LSP server on stdio (RFC-014; editor setup in `docs/lsp.md`)
- `harness/` — the generate→check→repair demo harness (script, not product)
- `python3 tools/gen_passive.py` — regenerate `lib/passive/` (then `cargo run -- fmt lib/passive --check`); `python3 tools/verify_passive_mpns.py [--all]` re-checks the emitted part numbers against Yageo
## Architecture (pipeline = the verdict ladder)
`parses ⊂ resolves ⊂ type-checks ⊂ connects ⊂ passes residual DRC ⊂ emits netlist`
- `src/span.rs`, `src/diag.rs` — SourceMap, spans, diagnostics (every diagnostic: stable code + precise span; codes registry in `docs/error-codes.md`)
- `src/lex.rs`, `src/ast.rs`, `src/parse.rs` — hand-written lexer + recursive-descent parser (deterministic, no unbounded lookahead — Constitution hard constraint)
- `src/resolve.rs` — cross-file symbol table (project files + the manifest's exact dependency set)
- `src/check/` — the heart: units (RFC-001), impl satisfaction (RFC-003), generics (RFC-007), fn expansion with cycle detection (RFC-006), pin exhaustiveness at final assembly (RFC-002)
- `src/ir.rs` — flat post-expansion design IR (instances, nets, nc)
- `src/lock.rs` — design.lock + the pure-function designator allocator with checked injectivity postcondition (RFC-005)
- `src/drc.rs` — exactly four rules (RFC-004): voltage-exceed, polarity-mismatch, single-driver, multi-driver. **Never add a fifth structural rule** — that's a type-system job.
- `src/emit/` — KiCad `.net` (S-expr) + BOM CSV + `layout.json` (RFC-013) + IPC-2581 XML (RFC-015)
- `lib/` — the library root: bare package families live directly below it and manufacturer packages at `lib/@scope/name`; manifests drive discovery and no package needs a resolver code path of its own
- `lib/std/` — core traits only: `TwoTerminal`, `Capacitor`, `Resistor`, `Polarized`, `Diode`, `IC`, and `Connector`. Because std is every package's implicit prelude, it must never accumulate devices, parts, pads, footprints, or domain-specific traits
- `lib/passive/` — root namespace `passive::*`; chip resistors, MLCCs, chip inductors, their lands, and passive helper circuits. Resistor/MLCC source files are GENERATED by `tools/gen_passive.py` (never hand-edit a generated parts file). Yageo primary throughout: RC_L resistors and CC-series MLCCs, both part-number schemes derived from Yageo's own datasheets; Vishay CRCW alternates on resistors and real Samsung CL alternates on capacitors (`tools/passive_data/samsung_cl.json` — Samsung's thickness code is not derivable, so it is a table, not a rule). Which capacitors exist is datasheet data (`yageo_mlcc.json`), not a guess, and every emitted part number is checked against Yageo's specsheet endpoint by `tools/verify_passive_mpns.py` — a part that does not resolve is omitted, never asserted
- `lib/usb/` — `usb::connectors::type_c::*`, `usb::connectors::micro_b::*`, and `usb::pd::ccg6df::*`; `lib/connectors/` — general connectors such as `connectors::headers::smd_254::*` and `connectors::headers::castellated_254::*`
- `lib/esd/`, `lib/diode/`, `lib/flash/`, `lib/ldo/`, `lib/led/`, `lib/mic/`, `lib/mosfet/`, and `lib/osc/` expose component declarations at their package roots; reusable QFP/LQFP, BGA, CSP/WLCSP, and SOIC/TSSOP fabrication geometry lives in `lib/qfp/`, `lib/bga/`, `lib/csp/`, and `lib/soic/`
- `lib/@ti/dcdc/` — `ti_dcdc::controllers::multiphase::*`; `lib/@st/stm32/` — the pinned-ST-source generated device catalog and source-joined exact parts at `st_stm32::*` (fabrication geometry is dependency-owned, never copied into this manufacturer package); `lib/@espressif/esp32/` — the pinned-Espressif-source generated ESP32-lineage device/part catalog at `espressif_esp32::*`, with vendor module geometry locally owned and reusable QFN geometry dependency-owned; `lib/@raspberrypi/mcu/` — `raspberrypi_mcu::*`; `lib/@richtek/dcdc/` — `richtek_dcdc::buck_boost::rt6150b::*`; `lib/@sifli/sf32/` — `sifli_sf32::*` (moved out of `@contrib/sf32` 2026-08-30; its QFN68 land stays package-local rather than in `qfn` because it is SiFli's own modified land — segmented EP stencil, sixteen thermal vias-in-pad — and the generic `qfn::QFN68N35P700X700_1EP549X549` is deliberately quarantined for qualified parts; the fabrication audit in tests/library.rs covers `@sifli/` alongside `@contrib/`). Quoted scoped manifest names normalize to underscore package namespaces in CoHDL source
- `tests/` — fixture tests mapped 1:1 to the MVP exit criteria
## Non-negotiable invariants (from the Constitution / RFCs)
- Same source + same exact dependency set → same verdict, same designators, same netlist **bytes**. No HashMap iteration order leaking into output — use BTreeMap or explicit sorts everywhere output-adjacent.
- Zero unit coercion; a bare number where a unit is expected is a compile error naming expected vs. actual.
- Every diagnostic names the exact construct (instance, pin, trait, unit) — never a bare "type mismatch".
- The designator allocator asserts injectivity as a postcondition on every run.
- Unicode `Ω` / `°C` must produce a targeted "use `ohm` / use `C`" diagnostic, not a generic lex error.