# 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 LCEDA, no incremental compilation). 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-E1107 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>/…`), 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`.

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. 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, ≤16 MiB, 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. 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)`). 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.

## 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 ipc2581]` — check + emit KiCad `.net` + BOM CSV (+ `design.lock`; `--emit ipc2581` adds the RFC-015 partner document)
- `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
  - `lib/@ti/dcdc/` — `ti_dcdc::controllers::multiphase::*`; `lib/@st/stm32/` — `st_stm32::f0::stm32f072cb::*`; `lib/@espressif/esp32/` — `espressif_esp32::modules::wroom_s3::*`; `lib/@raspberrypi/mcu/` — `raspberrypi_mcu::*`; `lib/@richtek/dcdc/` — `richtek_dcdc::buck_boost::rt6150b::*`. 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.
