Skip to content

Commit bfb27de

Browse files
authored
Merge pull request #1 from ChanMeng666/feat/physical-correctness-circulation
feat: physical-correctness & circulation pass (Claude × Codex, ADR 0006)
2 parents 2ac0407 + 3d0b04f commit bfb27de

30 files changed

Lines changed: 1150 additions & 76 deletions

AGENTS.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,11 @@ usage), and source can come from stdin (`-`). Beyond `compile`/`watch`/`fmt`/`ex
100100
`arch spec` (print the whole language — `spec.llm.md`), `arch describe` (semantic JSON: rooms,
101101
areas, adjacency, door connections — backed by `describe()` in `src/describe.ts`), `arch lint`
102102
(architectural soundness `W_*` warnings — `src/lint.ts`), `arch validate` (parse+resolve+lint, no
103-
render), and `arch new` (scaffold). `describe`/`lint` share the pure analysis layer in
104-
`src/analyze.ts`; all are exported from `src/index.ts`. This is the standard interface for AI agents
103+
render; `--strict`/`--fail-on-warning` makes warnings fail too — the pipeline ship-gate), `arch new`
104+
(scaffold), and `arch repair` (the explicit opt-in source-to-source corrector — pushes furniture out
105+
of walls and emits new `.arch` + a change log; `src/repair.ts`, see ADR 0006). `describe`/`lint` share
106+
the pure analysis layer in `src/analyze.ts` (+ `src/analyze/occupancy.ts`, the circulation
107+
flood-fill); all are exported from `src/index.ts`. This is the standard interface for AI agents
105108
— there is intentionally no MCP server (see the README's agent section).
106109

107110
## Architecture & Conventions

CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added — physical-correctness & circulation (Claude × Codex adversarial pass)
11+
12+
A second Claude Code × Codex review (prompted by AI-generated plans that rendered with furniture
13+
through walls, fixtures piled in doorways, and rooms with no door) hardened the renderer and the
14+
soundness layer, **without** turning `compile()` into an arranger. See the new
15+
[ADR 0006](docs/adr/0006-solver-as-explicit-transform.md): a solver may exist only as an explicit
16+
source-to-source transform, never as invisible render behavior.
17+
18+
- **Render fidelity:** `dims auto walls` annotates each distinct wall thickness once (deduped); the
19+
new mode is also included in `dims auto all`. Per-room dimensions (`dims auto rooms`) now sit in the
20+
page margin on the side each room faces, instead of overlapping the room label/area inside the room.
21+
- **New lint rules (advisory, deterministic facts — ADR 0005-compliant):**
22+
`W_FURNITURE_WALL_COLLISION` (a piece drawn through a wall solid, via AABB intrusion over
23+
`segmentRectangle`, opening-aware), `W_DOORWAY_BLOCKED` (furniture in a door's clear landing — the
24+
walk-through path, distinct from the swing arc), and `W_ROOM_NO_CLEAR_PATH` (a grid flood-fill in
25+
`analyze/occupancy.ts` finds a room whose doorways can't reach a usable patch of floor). New ruleset
26+
knobs `doorwayLandingMm` and `minClearAreaM2`; the accessibility profile tightens the landing depth.
27+
- **Strict gating:** `arch validate --strict` (alias `--fail-on-warning`) makes advisory warnings
28+
fail too (exit `2`) — the gate a generation pipeline runs so it can't ship a plan lint flagged. The
29+
agent contract (`SKILL.md`, `spec.llm.md`) now mandates this gate and an explicit furniture-placement
30+
discipline (back fixtures to walls with `against wall`, keep every room reachable, keep doorways
31+
clear).
32+
- **Catalogued footprints:** a known fixture placed `against wall` may omit `size` and take its
33+
conventional footprint from `fixtures-catalog.ts` (closed-form, never a guess).
34+
- **`arch repair`:** a new opt-in, source-to-source corrector. It pushes furniture out of walls and
35+
emits **new `.arch` source plus a change log** (never an invisible edit); ambiguous, scripted, or
36+
`against wall` pieces are reported, not guessed. Exported as `repair()` from the public API.
37+
- **eval:** the offline harness now fails any golden that has a physical-correctness violation (the
38+
three new codes), guarding authorability regressions.
39+
40+
### Fixed
41+
42+
- The formatter (`arch fmt`) silently dropped the `dims auto` directive; it is now preserved.
43+
1044
## [1.3.2] - 2026-06-28
1145

1246
### Changed — docs site & playground brought up to v1.3 (no compiler changes)

SKILL.md

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,34 @@ npx @chanmeng666/archlang help
3333
5. **Verify intent without an image:** `arch describe plan.arch --json` returns the rooms (with
3434
areas and adjacency), what each door connects, and totals. Confirm the room count, labels, and
3535
areas match what was asked.
36-
6. **Check soundness:** `arch lint plan.arch --json` flags habitability problems: a room with no
37-
door, a windowless bedroom, an implausibly small room, a too-narrow door, no entrance, a bathroom
38-
reachable only through a bedroom, a bathroom not fully walled in, a door whose swing hits furniture
39-
or another door, and a bath/kitchen with no fixtures. Fix the warnings or tell the user about them.
36+
6. **Gate on soundness — don't ship a flagged plan.** Run `arch validate plan.arch --strict --json`
37+
(parse + resolve + lint in one pass). `--strict` makes **every advisory warning fail** too
38+
(exit `2`), so this is the gate a generation pipeline runs before it ships. If not `ok`, read each
39+
`diagnostics[].fix`, edit the source, and re-run until it passes — or, if a warning is a deliberate
40+
choice, tell the user explicitly. The lint flags: a room with no door, a windowless bedroom, an
41+
implausibly small room, a too-narrow door, no entrance, a bathroom reachable only through a bedroom,
42+
a bathroom not fully walled in, a door whose swing hits furniture/another door, a bath/kitchen with
43+
no fixtures, **furniture drawn through a wall (`W_FURNITURE_WALL_COLLISION`)**, **a fixture blocking
44+
a doorway (`W_DOORWAY_BLOCKED`)**, and **a room packed so you can't step in
45+
(`W_ROOM_NO_CLEAR_PATH`)**.
46+
47+
## Placement discipline (write it right the first time)
48+
49+
A geometry-blind generator that emits absolute coordinates and ignores lint produces plans that
50+
render but are physically wrong (furniture through walls, fixtures piled in doorways, rooms with no
51+
door). Avoid that by construction:
52+
53+
- **Every room needs a way in.** Put a `door` or a cased `opening` on a wall of *every* room — an
54+
open-plan space still needs a modeled opening to the space it connects to, or it reads as sealed.
55+
- **Back plumbing/kitchen fixtures onto a wall with `against wall <id>`, not raw `at`.** `against wall`
56+
is closed-form and fails loudly if ambiguous, so the fixture lands flush against the real wall face
57+
instead of floating or penetrating. Use `in <roomId>` so the side is inferred.
58+
- **Keep furniture inside the room and out of the walls.** A piece's whole footprint must sit within
59+
the room rectangle; never let it cross a wall centerline (that's `W_FURNITURE_WALL_COLLISION`).
60+
- **Leave the doorway clear.** Keep furniture out of the straight approach on both sides of every door
61+
(≥300 mm), and out of the leaf's swing arc — so a person can actually walk in.
62+
- **Verify, then gate.** `arch describe --json` to confirm the intent (rooms, areas, access graph),
63+
then `arch validate --strict --json` to prove it's sound before you ship.
4064

4165
## Commands
4266

@@ -46,8 +70,9 @@ arch compile plan.arch -o out.svg --json # render (also -f dxf|pdf|png)
4670
echo '<source>' | arch compile - -o - -f svg # compile stdin → SVG on stdout
4771
arch describe plan.arch --json # semantic facts: rooms, areas, adjacency, door connections
4872
arch lint plan.arch --json # architectural soundness warnings
49-
arch validate plan.arch --json # parse + resolve + lint, no render (fast check)
73+
arch validate plan.arch --strict --json # parse + resolve + lint; --strict fails on warnings too (the ship gate)
5074
arch fmt plan.arch --write # canonical formatting
75+
arch repair plan.arch -o fixed.arch # emit corrected source (furniture pushed out of walls) + change log
5176
arch new -o plan.arch # scaffold a starter plan
5277
arch explain E_ROOM_SIZE --json # look up any diagnostic code
5378
```
@@ -60,7 +85,9 @@ arch explain E_ROOM_SIZE --json # look up any diagnostic code
6085
- **Fixtures draw real symbols:** `furniture wc|basin|shower|bathtub|kitchen_sink|counter|fridge|stove …`
6186
renders a plan symbol (not an empty box); standard sizes are also in `lib/fixtures.arch`. Put fixtures
6287
in every bath and kitchen so the plan reads professionally and lint stays quiet.
63-
- **`dims auto`** draws dimension strings for you (`overall`, `rooms`, or `all`) — no need to place each `dim`.
88+
- **`dims auto`** draws dimension strings for you (`overall`, `rooms`, `walls`, or `all`) — no need to
89+
place each `dim`. `rooms` puts each room's size in the margin (clear of the label); `walls` annotates
90+
each distinct wall thickness once.
6491
- Edit is cheap: "make the bedroom 1 m wider" is a one-number change, then recompile.
6592

6693
Treat the CLI as the source of truth — author, render, and verify through it rather than reasoning
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# 6. A solver is an explicit source transform, never invisible render behavior
2+
3+
- **Status:** Accepted
4+
- **Date:** 2026-06 (v1.4 planning)
5+
6+
## Context
7+
8+
[ADR 0005](0005-no-invisible-architect.md) drew the line that `compile()` renders facts
9+
and `lint` advises, but neither *arranges*. A second adversarial review (Claude Code ×
10+
Codex), prompted by AI-generated plans that rendered with furniture penetrating walls,
11+
fixtures piled in doorways, and rooms with no door, re-opened the obvious question:
12+
13+
> Building games (The Sims, Project Highrise) never let a player drag a sofa through a
14+
> wall. Why can't ArchLang borrow those algorithms so AI-authored plans are automatically
15+
> physically correct and circulation-sound?
16+
17+
The review found the failing plans were mostly an **integration failure**, not a missing
18+
solver: ArchLang's lint *already* flagged the unreachable rooms, floating fixtures, and
19+
obstructed door swings — the upstream generator ignored every warning and shipped anyway.
20+
But it also exposed a real risk: the natural "fix" is to let the compiler nudge the sofa
21+
out of the wall. That is the ADR 0005 temptation again, now wearing a physics costume.
22+
23+
The key clarification: **the boundary is not "deterministic vs non-deterministic."** A
24+
deterministic routine that moves a sofa, picks a wall, or reroutes circulation is *still
25+
designing* — it can be pure and byte-stable and still break the promise that source
26+
coordinates mean exactly what they say. The real line is **faithful rendering vs choosing
27+
intent.**
28+
29+
## Decision
30+
31+
Amend ADR 0005 with one clause:
32+
33+
> **A constraint-solver / arranger may exist in ArchLang only as an explicit, opt-in
34+
> command whose output is new `.arch` source plus a change log. It may never run inside
35+
> `compile()`, and it may never alter render output for a given source.**
36+
37+
Concretely:
38+
39+
- **`compile()` stays faithful.** When furniture overlaps a wall or a fixture blocks a
40+
door, the renderer draws exactly what was authored. It does **not** clip, snap, or
41+
relax — doing so would silently hide an authoring error. Catching the problem is
42+
`lint`'s job (`W_FURNITURE_WALL_COLLISION`, `W_DOORWAY_BLOCKED`, `W_ROOM_NO_CLEAR_PATH`).
43+
- **Closed-form, unambiguous placement remains core-legal** (as ADR 0005 already allows):
44+
`against wall <id>`, relational room placement — every target explicit, fail-fast on
45+
ambiguity, no search.
46+
- **Any *corrective* arranging is a source-to-source transform.** A future `arch repair`
47+
reads a `.arch`, emits a *new, inspectable* `.arch` plus a per-move change log
48+
explaining what it did and why, and refuses (with a diagnostic) anything it cannot
49+
resolve unambiguously. The author reviews the diff; nothing happens invisibly.
50+
51+
| Where it belongs | Example |
52+
|------------------|---------|
53+
| `compile()` | render the authored plan, faithfully; **never** auto-correct |
54+
| `lint` (advisory facts) | AABB furniture-vs-wall collision; door-landing clearance; grid flood-fill circulation reachability |
55+
| explicit transform (`arch repair`, agent loop) | move a fixture to its nearest legal wall; nudge furniture out of a wall; emit new source |
56+
57+
This also settles which "game algorithms" map where. **AABB collision** and **grid
58+
flood-fill / navmesh reachability** are legitimate *fact* computations and live in
59+
`analyze`/`lint`. **Force-directed relaxation, simulated annealing, and rectangle
60+
packing** are policy-heavy arrangers; they are out of scope for `compile()` and belong
61+
only behind the explicit-transform seam, if ever.
62+
63+
## Consequences
64+
65+
**Pros.** `compile()` keeps its zero-surprise, byte-stable guarantee even as physical-
66+
correctness intelligence grows. Every correction is reviewable source, never hidden
67+
behavior. The new lint rules give an AI author a strong enough signal to converge on a
68+
sound plan on its own (especially under `arch validate --strict`).
69+
70+
**Cons.** A physically broken plan still renders broken (with loud warnings) until
71+
someone — the agent, `arch repair`, or a human — fixes the source. ArchLang remains a
72+
compiler with a separate, opt-in corrector, not a forgiving drag-and-drop design tool.
73+
We accept that: it is the same trade ADR 0004 and 0005 already made, extended to physics
74+
and circulation.
75+
76+
This ADR governs the v1.4+ roadmap (furniture-vs-wall and doorway-clearance lint, the
77+
circulation flood-fill, `arch validate --strict`, and the `arch repair` transform).

docs/error-codes.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
Every diagnostic carries a stable code. Look one up with `arch explain <CODE>`
66
(e.g. `arch explain E_ROOM_SIZE`). Errors abort rendering; warnings do not.
77

8-
**34 errors** · **24 warnings**
8+
**34 errors** · **27 warnings**
99

1010
| Code | Severity | Summary |
1111
| --- | --- | --- |
@@ -47,15 +47,18 @@ Every diagnostic carries a stable code. Look one up with `arch explain <CODE>`
4747
| [`W_BEDROOM_NO_WINDOW`](#w_bedroom_no_window) | warning | Bedroom has no window. |
4848
| [`W_DOOR_CLEARANCE`](#w_door_clearance) | warning | Door is narrower than the minimum clear width. |
4949
| [`W_DOOR_OFF_WALL`](#w_door_off_wall) | warning | Door does not lie on any wall. |
50+
| [`W_DOORWAY_BLOCKED`](#w_doorway_blocked) | warning | A doorway's landing is blocked. |
5051
| [`W_EMPTY_PLAN`](#w_empty_plan) | warning | Empty plan. |
5152
| [`W_FIXTURE_FLOATING`](#w_fixture_floating) | warning | A plumbing/kitchen fixture is not against a wall. |
5253
| [`W_FIXTURE_WRONG_ROOM`](#w_fixture_wrong_room) | warning | Fixture sits outside its declared room. |
5354
| [`W_FURN_CLEARANCE`](#w_furn_clearance) | warning | A fixture's use-space is blocked. |
5455
| [`W_FURNITURE_OVERLAP`](#w_furniture_overlap) | warning | Two pieces of furniture overlap. |
56+
| [`W_FURNITURE_WALL_COLLISION`](#w_furniture_wall_collision) | warning | Furniture penetrates a wall. |
5557
| [`W_HATCH_SCALE`](#w_hatch_scale) | warning | Hatch scale must be positive; using 1. |
5658
| [`W_NO_ENTRANCE`](#w_no_entrance) | warning | The plan has no exterior door. |
5759
| [`W_OPENING_OFF_WALL`](#w_opening_off_wall) | warning | Opening does not lie on any wall. |
5860
| [`W_ROOM_DISCONNECTED`](#w_room_disconnected) | warning | Room has no door — it can't be entered. |
61+
| [`W_ROOM_NO_CLEAR_PATH`](#w_room_no_clear_path) | warning | A room cannot be entered or crossed. |
5962
| [`W_ROOM_NO_FIXTURE`](#w_room_no_fixture) | warning | Bathroom or kitchen has no fixtures. |
6063
| [`W_ROOM_NOT_ENCLOSED`](#w_room_not_enclosed) | warning | Bathroom is not fully enclosed. |
6164
| [`W_ROOM_OVERLAP`](#w_room_overlap) | warning | Rooms overlap. |
@@ -530,6 +533,19 @@ door at (0,0) width 500 wall exterior # lint: under 700 mm
530533
door at (9999,9999) width 900 # warning: not on a wall
531534
```
532535

536+
## W_DOORWAY_BLOCKED
537+
538+
*warning* — A doorway's landing is blocked.
539+
540+
**Cause.** A piece of furniture/fixture sits in the clear landing space immediately on either side of a door opening, so you cannot pass through the doorway even when the leaf is open. This is the approach path, distinct from the leaf's swing arc (`W_SWING_OBSTRUCTED`).
541+
542+
**Fix.** Clear the space directly in front of and behind the door, or move the door.
543+
544+
```arch
545+
door at (6000,3000) width 800
546+
furniture wc at (5800,3050) size 700x400 # lint: WC blocks the doorway
547+
```
548+
533549
## W_EMPTY_PLAN
534550

535551
*warning* — Empty plan.
@@ -592,6 +608,18 @@ furniture sofa at (300,300) size 2000x900
592608
furniture bed at (1000,500) size 1500x2000 # lint: overlaps the sofa
593609
```
594610

611+
## W_FURNITURE_WALL_COLLISION
612+
613+
*warning* — Furniture penetrates a wall.
614+
615+
**Cause.** A furniture/fixture rectangle intrudes into a wall's solid (it crosses the wall's thickness band rather than sitting flush against its face), so it would physically pass through the wall — a coordinate or size mistake. A piece merely touching the wall face is fine.
616+
617+
**Fix.** Move or resize the piece so it sits fully inside the room (against the wall face, not through it), or anchor it with `against wall <id>`.
618+
619+
```arch
620+
furniture sofa at (350,2300) size 2000x900 # lint: crosses the partition at y3000
621+
```
622+
595623
## W_HATCH_SCALE
596624

597625
*warning* — Hatch scale must be positive; using 1.
@@ -640,6 +668,18 @@ opening at (9999,9999) width 1000 # warning: not on a wall
640668
room id=r at (0,0) size 3000x3000 # lint: no door on its perimeter
641669
```
642670

671+
## W_ROOM_NO_CLEAR_PATH
672+
673+
*warning* — A room cannot be entered or crossed.
674+
675+
**Cause.** Furniture, fixtures, door swings and their clearances fill the room so densely that a person stepping through a door/opening has no clear floor path into the usable space — the room is technically reachable but physically blocked.
676+
677+
**Fix.** Open up the layout: move or shrink the furniture nearest the door so there is a continuous walkable strip from each entrance into the room.
678+
679+
```arch
680+
furniture shower at (5000,3000) size 2000x2000 # lint: fills the bathroom against its only door
681+
```
682+
643683
## W_ROOM_NO_FIXTURE
644684

645685
*warning* — Bathroom or kitchen has no fixtures.

docs/language-reference.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ plan "My Home" {
4949
| `grid <n>` | Snap module in mm. All coordinates round to the nearest multiple. `0` disables. | `0` |
5050
| `scale 1:50` | Printed scale, shown in the title block. | none |
5151
| `north up\|down\|left\|right\|<deg>` | North direction for the north arrow. | `up` |
52-
| `dims auto [overall\|rooms\|all]` | Auto-draw dimension strings without hand-placing each `dim`: `overall` (the bounding extents), `rooms` (each room's width + height), or `all` (both; the default when no scope is given). | off |
52+
| `dims auto [overall\|rooms\|walls\|all]` | Auto-draw dimension strings without hand-placing each `dim`: `overall` (the bounding extents), `rooms` (each room's width + height, placed in the page margin on the side the room faces), `walls` (one deduped thickness call-out per distinct wall thickness), or `all` (all three; the default when no scope is given). | off |
5353

5454
## Values & expressions
5555

@@ -374,7 +374,7 @@ opening id=o_living at (4000,3700) width 900 wall partition # living ↔ hall,
374374

375375
```
376376
furniture <kind> [id=<id>] at (x,y) size <w>x<h> [label "<text>"] [rotate 0|90|180|270] [in <room>]
377-
furniture <kind> [id=<id>] against wall <ref> [segment <n>] [offset <mm>] [side left|right] size <along>x<depth> [label "<text>"] [in <room>]
377+
furniture <kind> [id=<id>] against wall <ref> [segment <n>] [offset <mm>] [side left|right] [size <along>x<depth>] [label "<text>"] [in <room>]
378378
```
379379

380380
A schematic labelled rectangle (bed, sofa, desk…). Known plumbing & kitchen
@@ -385,8 +385,10 @@ labelled rectangle.
385385

386386
A piece can be placed two ways: absolutely with `at (x,y)` (optionally turned with
387387
`rotate`), or snapped **`against wall <ref>`** so its back sits on the wall and its
388-
rotation is derived for you. `in <room>` records which room owns the piece (used by
389-
the lint rules). The full placement rules, the fixture symbol catalogue, and the
388+
rotation is derived for you. A known fixture placed `against wall` may **omit `size`**
389+
to take its catalogued footprint (e.g. `furniture wc against wall w1 in bath`); `at`
390+
placement and uncatalogued kinds still need an explicit `size`. `in <room>` records
391+
which room owns the piece (used by the lint rules). The full placement rules, the fixture symbol catalogue, and the
390392
fixture-aware lint checks live on the dedicated **[Furniture & fixtures](furniture.md)**
391393
page. Standard fixtures are also importable components at typical residential sizes:
392394

0 commit comments

Comments
 (0)