Skip to content

This page is generated from docs/analysis.md — edit it there.

Analysis: describe & lint

ArchLang compiles a plan to a drawing — but it can also read the plan back as facts. Two pure functions turn source into machine-readable, image-free output:

  • describe(source) — a semantic summary: rooms, areas, adjacencies, what every door/window/opening connects, the furniture, and a modelled access graph.
  • lint(source) — advisory W_* warnings about habitability, against a chosen profile.

Both are exported from the package (import { describe, lint } from "@chanmeng666/archlang") and surfaced on the CLI as arch describe / arch lint (add --json for the structured form). They power the Describe and Lint tabs in the playground. Neither renders anything, so a text-only agent can author a plan and verify it matches intent without ever looking at an image.

Philosophy. This is the line ArchLang draws on purpose: it reports facts and gives advice, but it never silently re-arranges your geometry. The compiler stays a faithful, deterministic renderer; the intelligence ships as data you read, not as an invisible architect that moves walls behind your back. See ADR 0005 — no invisible architect.

describe — the semantic summary

arch describe plan.arch --json returns a SceneSummary. For examples/studio.arch (abridged to the shapes that matter — run it yourself for the full object):

json
{
  "ok": true,
  "plan": "Studio 1BR",
  "units": "mm",
  "scale": "1:50",
  "bbox": { "w": 7000, "h": 6000 },
  "rooms": [
    {
      "id": "r_living",
      "label": "Living / Kitchen",
      "uses": ["living", "kitchen"],
      "area_m2": 24,
      "bbox": { "x": 0, "y": 0, "w": 4000, "h": 6000 },
      "adjacent": ["r_bed", "r_hall", "r_bath"]
    },
    {
      "id": "r_bath",
      "label": "Bath",
      "uses": ["bath"],
      "area_m2": 4.8,
      "bbox": { "x": 4000, "y": 4400, "w": 3000, "h": 1600 },
      "adjacent": ["r_living", "r_hall"]
    }
  ],
  "doors": [
    { "id": "d_main", "between": ["exterior", "r_living"], "width": 1000 },
    { "id": "d_bath", "between": ["r_hall", "r_bath"], "width": 800 }
  ],
  "windows": [
    { "id": "window_1", "room": "r_living", "width": 1500 }
  ],
  "openings": [
    { "id": "o_living", "between": ["r_living", "r_hall"], "width": 900 }
  ],
  "furniture": [
    { "id": "kitchen_sink_1", "category": "kitchen_sink" },
    { "id": "sofa_5", "category": "sofa", "label": "Sofa" }
  ],
  "access": { "…": "see below" },
  "totals": { "rooms": 4, "doors": 3, "windows": 3, "floor_area_m2": 42 }
}
FieldMeaning
rooms[].usesthe room's uses tags (or the inferred kind when none were authored)
rooms[].area_m2floor area, w × h ÷ 1 000 000, rounded to 2 dp
rooms[].adjacentids of rooms whose walls touch this one within tolerance (a shared corner alone doesn't count)
doors[].between / openings[].betweenthe two spaces the connector joins — a room id or the literal "exterior"
windows[].roomthe room the window lights
totalsroom / door / window counts and total floor area

A text-only agent reads this and confirms "4 rooms, 42 m², a bath adjacent to the hall (not the bedroom), a 1000 mm front door" — no rendering required.

The access graph

describe().access models the building as a graph of connectors (doors and openings) and walks it from the exterior. For the studio:

json
"access": {
  "entrances": ["d_main"],
  "hasEntrance": true,
  "edges": [
    {
      "doorId": "d_main", "kind": "door", "between": ["exterior", "r_living"],
      "nominalWidth": 1000, "estimatedClearWidth": 940,
      "hostCategory": "exterior", "hostWallId": "exterior_1",
      "exterior": true, "ambiguous": false
    },
    {
      "doorId": "o_living", "kind": "opening", "between": ["r_living", "r_hall"],
      "nominalWidth": 900, "estimatedClearWidth": 900,
      "exterior": false, "ambiguous": false
    }
  ],
  "rooms": [
    { "id": "r_living", "depthFromEntrance": 1, "reachable": true, "bottleneckClearWidth": 940 },
    { "id": "r_hall",   "depthFromEntrance": 2, "reachable": true, "bottleneckClearWidth": 900 },
    { "id": "r_bath",   "depthFromEntrance": 3, "reachable": true, "bottleneckClearWidth": 740 }
  ]
}
FieldMeaning
entrances / hasEntrancethe door(s) that connect the exterior to a room, and whether any exist at all
edges[].nominalWidththe connector's drawn width
edges[].estimatedClearWidththe usable opening: a door loses ~60 mm to its leaf and stop, an opening keeps its full width
edges[].exteriorwhether this connector reaches the outside
rooms[].depthFromEntrancehow many connectors you pass through from the nearest entrance (1 = opens straight off it); null if you can't get there
rooms[].reachablecan this room be reached from the exterior at all?
rooms[].bottleneckClearWidththe narrowest clear width along the widest path in from the entrance — the real constraint for moving furniture or a wheelchair (a widest-path search, so it reports the best route's worst pinch)

This is what makes a sealed-off room or a wheelchair-impassable corridor visible as data — the playground's Describe tab draws it as a reachability diagram.

lint — architectural soundness

arch lint plan.arch --json returns advisory W_* diagnostics, each with a byte span, a line/col, and a fix. Warnings never block rendering — they flag habitability, not validity. The rules, grouped by what they watch:

FamilyExample codes
RoomW_ROOM_TOO_SMALL, W_ROOM_DISCONNECTED, W_BEDROOM_NO_WINDOW, W_ROOM_OVERLAP
PlacementW_DOOR_OFF_WALL, W_WINDOW_OFF_WALL, W_OPENING_OFF_WALL
Door / circulationW_DOOR_CLEARANCE, W_SWING_OBSTRUCTED, W_NO_ENTRANCE
ReachabilityW_ROOM_UNREACHABLE, W_BATH_VIA_BEDROOM
Wet roomsW_ROOM_NOT_ENCLOSED, W_ROOM_NO_FIXTURE
Furniture / fixturesW_FIXTURE_FLOATING, W_FIXTURE_WRONG_ROOM, W_FURNITURE_OVERLAP, W_FURN_CLEARANCE

Every code is documented — with cause, fix, and example — in the error catalog, or run arch explain W_SWING_OBSTRUCTED.

Profiles

A profile is a named bundle of thresholds, applied with --profile (CLI) or lint(src, { profile }). The names come from LINT_PROFILES (src/lint.ts):

ProfileThresholds
residential-basic (default)doors ≥ 700 mm, rooms ≥ 4 m², no swing-clearance buffer
accessibility-advisorydoors ≥ 850 mm, rooms ≥ 5 m², 150 mm swing clearance
arch lint plan.arch                                  # residential-basic
arch lint plan.arch --profile accessibility-advisory

The flagship studio is clean under the default profile, but the stricter profile surfaces advisory notes — its 800 mm internal doors and ~4–5 m² hall and bath fall under the accessibility thresholds:

json
{
  "ok": true,
  "diagnostics": [
    {
      "code": "W_DOOR_CLEARANCE",
      "message": "Door is 800 mm wide (under the 850 mm minimum nominal width).",
      "line": 30, "col": 3,
      "fix": "Widen the door to at least the minimum clear width.",
      "hints": ["Widen it to at least 850 mm."]
    },
    {
      "code": "W_ROOM_TOO_SMALL",
      "message": "Room \"Hall\" is only 4.2 m² (under 5 m²).",
      "line": 22, "col": 3,
      "fix": "Increase its `size`, or merge it into an adjacent space."
    }
  ]
}

Profiles are advisory soundness checks, never a building-code compliance guarantee. Real accessibility and code review depend on jurisdiction; treat these as a helpful nudge, not a sign-off.

The agent loop

Together, describe and lint close the author → render → verify loop for an AI agent with no eyes on the drawing — see Use ArchLang from an agent:

  1. arch compile — render and get errors as data.
  2. arch describe --json — confirm the room count, labels, areas, and access match the brief.
  3. arch lint --json — clear the habitability warnings (each carries a fix).