CLAUDE.md · diff

git:20260904.a69ab46 to git:20260904.b54fdcc

10 added, 2 removed. Audit A to A.

# CLAUDE.md
Guidance for Claude Code (and any AI agent) working in this repository.
The **canonical, always-current** project status, architecture, commands, and conventions live in
**[AGENTS.md](AGENTS.md)** — read it first. It is imported below so it loads with this file; for the
exact shipped state and versions, defer to AGENTS.md → "Project status" and `CHANGELOG.md` rather
than memory.
@AGENTS.md
## Orientation (the rest is in AGENTS.md)
- **What this is:** ArchLang — a small declarative language that compiles `.arch` floor-plan source
to professional **SVG** (also DXF/PDF/PNG). Pure TypeScript, **zero runtime dependencies**,
isomorphic (runs in Node and the browser). A published, deployed monorepo, not a WIP.
- **Build & run:** `npm run build` · `npm test` (vitest) ·
`npm run cli -- compile examples/studio.arch -o out.svg`. A single root `npm install` bootstraps
every workspace.
- **Brand:** the logo is an "A" drawn as an A-frame house floor plan.
`brand/archlang-logo-master.svg` is the byte-sacred source — every variant is a **fill-swap only**
(never re-trace/simplify/re-fit path data, no small-size tier). The two public sites run the shared
**"The Compile Boundary"** design system — a cool source surface and a warm sheet surface split by a
compile seam, **both LIGHT: there is no dark mode and no dark surface on either site**. Its token
block is **duplicated byte-identically** in `docs-site/.vitepress/theme/style.css` and
`playground/src/styles/tokens.css` (change one, change the other). See
[ADR 0014](docs/adr/0014-one-light-world.md) — which supersedes
[ADR 0010](docs/adr/0010-compile-boundary-design-system.md) §1/§2/§6/§7, so read 0010's carbon/mylar
prose as history — and `brand/README.md` first.
## Non-negotiable invariants (break these and CI fails)
- **`compile()` is pure, synchronous, deterministic.** No I/O, no `Date.now()`, no `Math.random()`
in `src/` core; output is byte-for-byte stable and snapshot/golden-tested. Node APIs and real time
are allowed **only** in `src/cli.ts` + `src/cli/`; everything else gets its environment via the `World` seam.
Route number formatting through `fmt()` so floats don't drift. The parse-stage memo's `PlanNode`
is shared and must never be mutated downstream — clone before mutating (a `repair()` in-place edit
made output history-dependent; fixed in `51a47ee`).
- **Don't hand-edit generated files.** `dist/`, `editors/*.tmLanguage.json`,
`playground/src/arch-language.js`, `docs-site/.vitepress/theme/arch-highlight.js`,
`docs/error-codes.md`, `docs/cli-reference.md`, `spec.llm.md`,
`llms-full.txt`, `schemas/plan.schema.json`, `schemas/intent.schema.json`,
`grammars/archlang.gbnf`, the twenty committed `examples/*.svg` the README embeds (the
`README_SVGS` list in `scripts/gen-example-svgs.ts`) and the two axonometric renders
`docs/axonometric.md` embeds (`VIEW_SVGS`, same generator) are generated — edit the source
(`src/grammar/tokens.ts`, `src/error-catalog.ts`, `src/manifest.ts`, `PLAN_JSON_SCHEMA`,
`INTENT_JSON_SCHEMA`, `examples/`, `SKILL.md`) and run the matching `npm run gen:grammars` /
`gen:errors` / `gen:cli` / `gen:spec` / `gen:llms` / `gen:plan-schema` / `gen:intent-schema` /
`gen:gbnf` / `gen:example-svgs`. CI fails on drift. The SVGs joined this list late and are the
clearest case for it: hand-committed and never re-rendered, three of them showed the README a
building compiled before four separate rendering fixes, and nothing was watching.
- **A generator's TEMPLATE can go stale even when `check:drift` is green.** The gate compares
generator *output* to the committed file — it proves reproducibility, not correctness. A generator
that hardcodes a language fact reproduces the same *wrong* text forever: `gen-llm-spec.ts` shipped a
v1.12 CLI + no `strip` for three releases, and `gen-grammars.ts` hardcoded a number regex without the
unit suffixes. **Derive from the source of truth (`KEYWORDS`/`RULES`/`buildManifest()`), never
retype it**, and give each generator a guard that fails when a source-of-truth entry has no
rendering (as `gen-llm-spec.ts` now does for every `KEYWORDS.control` entry, not just `element`).
- **A derived POSITION comes from the shape, never from its bounding box or centroid.** Six silent
bugs shipped this way and were fixed in v1.25.0 — a label drawn off its own floor, a walk reported
at half its true length, witness lines hanging metres off a sloped facade, a door swung into a wall
its room does not touch, a fixture backed onto a wall outside its room, and every courtyard-wall
window facing backwards. **`arch lint` reported none of them.** The grep that finds the next one is
`room.size`/`r.size.w` with no nearby `r.poly` branch. Fix locally and in closed form — probe one
wall thickness off each face and ask which side has floor — and never reach for the wall boolean
union to answer a `describe()` question. Inventory: `docs/research/2026-08-06-competitor-borrowing-roadmap.md` §9.1.
- **Every new language form ships with a byte-identity law, pinned by test:** a plan that does not
use it renders, describes and lints exactly as before. `site`, the door kinds, `zone`, `paper`,
- `polygon`, `arc`, `roof` and `void` all have one. Prove it with a SHA-256 sweep over the shipped
+ `polygon`, `arc`, `roof`, `void`, the v1.35 `height`/`sill`/`head` datum and the v1.35 `--view`
+ all have one. Prove it with a SHA-256 sweep over the shipped
examples, not by eyeballing — and if a golden moves, that is a finding to explain before it is a
diff to bless. Take the baseline with the **same digest body the test will run**, not a lookalike
in a throwaway script: `test/roof-void-byte-identity.test.ts`'s first attempt used a scratch script
whose payload separator differed by one character and produced four "failures" over artifacts that
were in fact byte-identical. And the sweep's payload is the whole agent-facing surface — SVG,
`describe()` **and** `lint()` — because a form that quietly appends an empty key to every summary
leaves the drawing untouched and is still a behaviour change for every `arch describe --json`
consumer.
- **Heights DRAW NOTHING, and `describe()` reports them only when the source wrote one.** The v1.35
vertical datum (`src/datum.ts`) puts `height` on a plan, a `level` and a `wall`, and `sill`/`head`
on the openings — a floor plan is a horizontal cut, so **a plan with no height clause is
byte-identical everywhere**, SVG, `describe()` and `lint()` alike (pinned over all 30 examples and
every storey by `test/height-byte-identity.test.ts`). The gate is ONE boolean,
`ResolvedPlan._heightsAuthored`, read by `describe()` and Plan JSON so they cannot disagree, and it
is whole-PLAN rather than per-storey. The fallback chain wall → level → plan → `STOREY_HEIGHT` has
exactly one implementation (`ResolveCtx.storeyHeight`) — never re-derive it in an element — and
**elevation accumulates the storeys below, never `level × height`**.
- **The axonometric view (`src/view/`) is a PICTURE, never a measurement.** `compile(src, { view })`
and `arch compile|preview --view iso|axon` draw extruded walls, floors and stacked storeys as
ordinary Scene primitives — and three things are law: **`describe()`/`lint()` never import
`src/view/`** and take no view option; **no `Math.cos`/`sin`/`tan`/`atan` under `src/view/`**
(implementation-approximated in ECMAScript, and CI spans two OSes × three Node versions —
`Math.sqrt` is exactly rounded, so both cameras are square-root expressions); and the painter's
order is TOTAL, its depth quantised through `fmt2`. A compile with no `view` is byte-identical
everywhere (pinned over all 30 examples). The view computes no footprint of its own: `joinWallSet`
hands it the plan's own wall outline. See `docs/axonometric.md`.
- **A drawn fixture symbol ignores its `label`, and fixture categories are DATA, not keywords.** The
129 catalogued words across 83 families live in one `FIXTURE_FAMILIES` table
(`src/elements/fixtures-glyphs.ts`) with their semantics in `src/fixtures-catalog.ts`; an
uncatalogued word falls back to the labelled rectangle on purpose. Adding a family is a table row
and a catalog entry — never a new element, never a `switch` arm. Keep the three catalog flags
distinct: `requiresWall` means **services only**, `directional` means the symbol has a back worth
turning to a wall, and `underlay` (a piece that lies flat and is stood on) is read **only** through
the shared `solidFurniture()` predicate, so the overlap rule, the clearance rule, the nav grid and
the per-room flood fill cannot disagree about what a rug is.
- **Errors are returned, never thrown** for user-source problems: push a `Diagnostic` with a byte
`span` and a catalogued `E_*`/`W_*` code (`src/error-catalog.ts` — a test enforces every raised
code has an entry and vice-versa).
- **Adding an element = one module** in `src/elements/` exporting an `ElementDef`, registered in
`src/elements/defs.ts`. Dispatch goes through the registry, not a switch.
## Verify your work the way the tool is used
After a change, prove it through the CLI, not by eyeballing SVG:
`arch compile --json` (renders, errors-as-data) · `arch describe --json` (rooms, areas, adjacency,
door connections) · `arch lint --json` (architectural soundness). For the v1.13 authoring loop also
drive `arch fix --dry-run` (preview the machine-applicable diagnostic fixes), `arch suggest`
(advisory door/window statements as data), `arch validate --graph` (interior-door adjacency vs. an
intended graph), and `arch compile -f txt` (the zero-dep ASCII plan — a text-only look with no
raster binary). For the v1.14–v1.15 additions also drive `arch validate --intent <f>` (gate a plan on
a brief's intent contract, exit 2 on a gating miss) / `arch score --brief <f>` (the continuous
intent-satisfaction meter), and read `arch describe --json`'s `freedom` block (which element positions
are hand-authored vs resolver-derived) before nudging coordinates. For the v1.17 CLI surface, drive
the affordances an agent actually reaches for: `arch <cmd> --help` (manifest-rendered flags + worked
examples — if you are guessing a flag, ask the CLI instead), the narrowing reads `arch describe
--select <keys>` / `--room <ids>` and `arch lint|validate --code <CODE>` / `--severity <sev>`, `arch
context --section <spec|workflow|cli|errors>`, and `arch fix --dry-run` (which now prints the exact
unified diff it would write) / `--backup`. Two invariants to prove, not assume, whenever you touch
that layer: **a display filter must never change an exit code or `ok`** (they come from the unfiltered
diagnostic set), and **an unrecognized flag or verb must exit 3** with a did-you-mean — never be
swallowed as a filename. For the v1.25 surface, drive `arch describe --json --select site` (the five
derived direction names — `street`/`back`/`equator_side`/`sunrise_side`/`sunset_side`; they are a
**drafting heuristic for an aspect, not a daylight measurement**, and there is deliberately no sun
model, latitude or date), the door kinds (`door pocket … slide left`, and note `describe().doors[].kind`
appears only when it is not the default `hinged`), and `arch lint --code W_POCKET_RUN|W_DIM_OVERLAP`
with `arch fix --dry-run` for their machine fixes. For the v1.28–v1.29 surfaces, drive the three
things a reading cannot settle: **`arch describe --json --select voids`** (a `void` is reported with
its extent and its room but is deliberately NOT subtracted from that room's area — a consumer
needing the net figure subtracts, so never "fix" the area); the **roof refusals**, which are the
whole design (`arch lint --code E_ROOF_CURVED` / `arch explain E_ROOF_CURVED` — an `arc` edge is
refused rather than approximated, and `roof polygon` is the answer); and the **underlay walkability
check**, which is the one furniture claim a drawing cannot show — put a `rug` and then a `sofa` on
the same rectangle across a plan's only route and confirm `arch describe --json`'s circulation walks
- THROUGH the first and `arch lint` raises `W_ROOM_NO_CLEAR_PATH` on the second. Keep the flagship
+ THROUGH the first and `arch lint` raises `W_ROOM_NO_CLEAR_PATH` on the second. For the v1.35
+ surfaces, drive the two things a reading gets wrong: **`arch describe --json --select heights`** on a
+ plan that authors a `height` and on one that does not — the whole block is gated on one whole-plan
+ flag, so a plan authoring nothing must emit nothing, and elevation ACCUMULATES the storeys below
+ rather than being `level × storey_height` (write two `level … height` blocks of different heights and
+ read the third floor); and **`arch compile --view iso`**, which is a picture and not a drawing —
+ confirm `describe()`/`lint()` are unmoved by it, and that all four refusals exit 3 rather than falling
+ back to the plan (`--view` with `-f txt`, with `--ascii`, with `--level`, with `--overlay`). Keep the flagship
`examples/studio.arch` **lint-clean and import-free**, and update snapshots/goldens
(`vitest -u`, `UPDATE_GOLDENS=1 vitest run test/visual.test.ts`, `ASCII_UPDATE=1 vitest run
test/ascii.test.ts`) only after reviewing the diff — never to green a red suite.
Beyond the CLI, prove the surfaces the core suite does not compile. `npm run check` +
`npm run check:drift` is the floor; add **`npm run typecheck:all`** when you touch anything outside
`src/`+`test/` (it is the only thing that compiles the playground, docs-site, MCP shim and VS Code
extension), **`npm run docs:build`** for any `docs/*.md` edit, and **`npm run e2e:playground` /
`npm run e2e:docs`** (Playwright, against the BUILT sites — build the core first) when you touch
those apps. Prose is gated too: `test/docs-table-pipes.test.ts` scans every tracked `.md` for a bare
`|` inside inline code in a table cell (write `\|`), `test/docs-fences.test.ts` requires every
```` ```arch ```` fence on a published page to compile or carry `static`, and
`test/docs-flags.test.ts` checks that every `arch … --flag` you write in a hand-maintained doc is a
flag that command declares. **The whole verification system — tiers, guards, and the red-run
response for each — is mapped in [docs/testing.md](docs/testing.md).**
## Conventions
Follow [Conventional Commits](https://www.conventionalcommits.org/). Run the lint/test commands
before proposing changes. Commit or push only when asked. Keep AGENTS.md and this file accurate when
you change build steps, structure, or conventions.