[!IMPORTANT]
π€ Read this with your AI agent β don't read it by hand.
This repo is written agent-first. Point Claude Code, GitHub Copilot, Cursor, or any agent at it: "Read the README and AGENTS.md, then help me run / extend this." Structure +
AGENTS.mdare optimized for agent comprehension.
Floor plans as code β like Typst/LaTeX, but for architecture.
Text in, a precise architectural drawing out. Deterministic, zero-dependency, and built so an AI agent can verify its own plan without ever looking at an image.
βΆ Live Playground Β· π Docs Β· β¨ CLI reference Β· π¦ npm Β· π§© VS Code
π See it
Here is a whole program, and below it the actual drawing it compiles to β not a mock-up. Walls join and hatch themselves, the door draws its own swing arc, the window draws its glazing, and the furniture is placed by anchor, never by hand-computed coordinates.
plan "Attached 1BR" {
units mm
grid 100
north up
strip right at (0,0) gap 0 height 4000 { # a row of rooms, laid end to end
room id=r_living size 4000 label "Living" uses living
room id=r_bed size 3000 label "Bedroom" uses bedroom
}
wall id=w_north exterior thickness 200 { (0,0) (7000,0) }
wall id=w_south exterior thickness 200 { (0,4000) (7000,4000) }
wall id=w_west exterior thickness 200 { (0,0) (0,4000) }
wall id=w_east exterior thickness 200 { (7000,0) (7000,4000) }
wall id=w_part partition thickness 100 { (4000,0) (4000,4000) }
door id=d_main on w_south at 2000 width 1000 hinge near start swing into r_living
door id=d_bed on w_part at 2000 width 900 swing into r_bed # opens INTO the bedroom
window on w_west at 50% width 1400 # centred on the wall, by construction
window on w_east at 50% width 1200
furniture sofa in r_living anchor top-left inset 300 size 2000x900 label "Sofa"
furniture bed in r_bed anchor right inset 300 size 1500x2000 label "Bed"
}
β arch compile attached.arch β that program, rendered. Nothing above places a wall corner,
a door leaf or a sofa by hand.
βΆ Click the drawing β it opens the live playground with this exact plan already loaded. Change a number and watch it redraw; the compiler runs in your browser, nothing is sent to a server.
Why a link and not a live embed? ArchLang does ship an embeddable viewer β but GitHub's markdown sanitizer strips
<iframe>(it comes back as escaped text, exactly like<script>), so no README on GitHub can host one. The embed works everywhere GitHub isn't: see Embed a plan anywhere. Source:examples/attached.arch.
π Introduction
ArchLang is a small declarative language for floor plans. You declare a plan β walls, rooms, doors, windows, furniture β and the compiler renders a clean, professional SVG (also DXF, PDF, PNG, and a zero-dependency ASCII plan).
Coordinates are integer millimetres, so output is deterministic: the same source always produces byte-identical bytes, and changing one number changes exactly one thing. "Make the bedroom 1 m wider" is a one-number diff β not a re-roll of a raster image that silently redraws the kitchen too.
The compiler is pure TypeScript with zero runtime dependencies and is isomorphic β the same code runs in Node and in the browser, which is why the playground is fully client-side.
ArchLang is the floor-plan engine behind ArchCanvas, an AI design agent β but it stands alone and is useful in any app or script.
π‘ Why it is different
Most "AI floor plan" tools generate a picture. A picture cannot be checked, diffed, or reasoned about β and neither the model nor you can tell whether the bathroom is actually reachable.
ArchLang generates a program, and then lets you interrogate it as facts:
| Raster image generation | ArchLang | |
|---|---|---|
| Output | pixels | a .arch program β SVG / DXF / PDF / PNG / TXT |
| Edit "widen the bedroom" | re-roll the whole image | change one number |
| Same input twice | different image | byte-identical output |
| "Is the bath reachable?" | look at it and guess | arch describe --json β access graph |
| "Does it match the brief?" | eyeball it | arch validate --intent β exit code |
| Wrong syntax | β | errors returned as data, each carrying its own fix |
That last row is the whole design: compile() never throws. It returns diagnostics with byte
spans and a machine-applicable fix, which is what makes a tight self-correction loop possible.
π€ The agent loop
An agent can author a plan, correct itself, and confirm the plan matches the brief without rendering an image at all β which is what makes ArchLang cheap to drive from a text-only model.
flowchart TD
B([Brief]) --> S["<b>arch context</b> β the whole language, one call"]
S --> W["Write <b>.arch</b>"]
W --> C{"<b>arch compile --json</b>"}
C -->|"ok: false"| F["<b>arch fix</b><br/><i>each diagnostic carries its own fix</i>"]
F --> C
C -->|"ok: true"| D["<b>arch describe --json</b><br/><i>rooms Β· areas Β· adjacency Β· access graph</i>"]
D --> V{"<b>arch validate --intent</b><br/><i>does it meet the brief?</i>"}
V -->|"no"| G["<b>arch suggest</b><br/><i>candidate door / window statements</i>"]
G --> W
V -->|"yes"| O([SVG Β· DXF Β· PDF Β· PNG])
style B fill:#ede7f6,stroke:#6b3ae0,color:#1a1a1a
style O fill:#e8f5e9,stroke:#2e7d32,color:#1a1a1a
style C fill:#fff8e1,stroke:#7a6000,color:#1a1a1a
style V fill:#fff8e1,stroke:#7a6000,color:#1a1a1a
style D fill:#e8f5e9,stroke:#2e7d32,color:#1a1a1a
style F fill:#fdecea,stroke:#b3261e,color:#1a1a1a
style G fill:#fdecea,stroke:#b3261e,color:#1a1a1a
Cold start in one command. arch context prints the entire agent context β language spec,
workflow skill, CLI reference and every diagnostic code β as one system-prompt-ready document (the
same llms-full.txt the docs site serves).
npx @chanmeng666/archlang context # EVERYTHING: spec + skill + CLI + error catalog
npx @chanmeng666/archlang context --section errors # β¦or one section of it (the catalog alone: 60 KB β 13 KB)
npx @chanmeng666/archlang spec # just the language, one page (~2k tokens)
npx @chanmeng666/archlang help describe # one command, with worked examples
npx @chanmeng666/archlang compile plan.arch --json # render β { ok, diagnostics, summary }
npx @chanmeng666/archlang fix plan.arch --dry-run # the exact unified diff it would write, applying nothing
npx @chanmeng666/archlang describe plan.arch --json # VERIFY, without an image
npx @chanmeng666/archlang validate plan.arch --strict # the ship gate
Every command takes --json (result on stdout, messages on stderr) with deterministic exit codes
(0 ok Β· 2 user-source error Β· 1 IO Β· 3 usage) β and a typo earns that 3: arch lint --jsn exits 3 with did you mean --json? rather than quietly reading --jsn as a filename, and
arch comple suggests compile.
One manifest, no drift. The per-command help (arch <cmd> --help), the flag parser, and the
generated CLI reference are all rendered from the same
manifest β which is why they cannot advertise a flag a command doesn't take. arch manifest --json
is that manifest as data, and arch <cmd> --help is the cheap way to read one row of it.
Reads are bounded, so a big plan can't flood a context window: describe --select/--room,
lint|validate --code/--severity, context --section. Filtering what you read never changes
what gates β the exit code always weighs every diagnostic. And because arch fix rewrites your
source, it prints the unified diff first and takes --backup.
See SKILL.md.
Machine-native artifacts β Plan JSON, a GBNF grammar, an intent schema, and an optional MCP server
| Artifact | Use |
|---|---|
/plan.schema.json | Emit structured JSON, compile it with arch compile --from-json |
/archlang.gbnf | Constrain a local model to parseable output |
/intent.schema.json | Write the brief down as a contract; gate on it with validate --intent |
/llms-full.txt | The whole context bundle (arch context) |
MCP server (optional). @chanmeng666/archlang-mcp is a stdio Model Context
Protocol shim over the library, listed on the official registry as io.github.ChanMeng666/archlang-mcp:
claude mcp add archlang -- npx -y @chanmeng666/archlang-mcp
Prefer the CLI when your agent has a shell β a CLI costs nothing in the context window until it is called, whereas an MCP tool schema sits there permanently. The server exists so MCP-native hosts can discover ArchLang. The core stays zero-dependency; the SDK lives only in that package (ADR 0012).
In CI: .github/actions/arch-render renders every ```arch
fence in your Markdown to images in one step.
β¨ Features
It draws like an architect, not like a plotter
PochΓ©-hatched walls (by material), door swing arcs, window glazing, computed room areas,
dimension lines, layers, line weights, a north arrow, a scale bar and a title block. Real fixture
symbols for WC, basin, shower, bathtub, sink, counter, fridge and stove β plus dims auto to
synthesize dimension strings for you.
It checks architectural soundness, not just syntax
arch lint encodes tacit professional knowledge: a bathroom reachable only through a bedroom, a
wet room that isn't fully walled in, a door whose swing hits furniture or another door, a windowless
bedroom, an unenterable room, a too-narrow door, a bath/kitchen with no fixtures, and a room whose use
was merely inferred from an indirect label (W_ALIAS_MATCH β with a fix that pins the explicit
uses). All tunable via the ruleset.
It models how a person actually walks the plan
arch describe runs a clearance-eroded nav grid: per-room walk distance, the narrowest pinch on
the way in, and how circuitous the route is β with advisory lint for a too-tight
(W_PATH_TOO_NARROW) or roundabout (W_CIRCUITOUS_PATH) walk, and an opt-in
arch compile --overlay circulation that draws the routes on top of the plan.
Facts and advice β never an invisible auto-arranger (ADR 0005).
arch repair is the one explicit corrector: it pushes furniture out of walls, doorways and swing
arcs, and emits a change log you review.
Errors are data, and many carry a machine-applicable fix
compile() never throws on bad source β it returns diagnostics with byte spans, a catalogued
E_*/W_* code, and a fix. Where the edit is mechanical, the diagnostic also carries applicable
fixes that arch fix applies for you. --error-svg even turns a plan that won't compile into a
self-describing error card an agent can look at.
Parametric, scriptable, and still deterministic
Values, arithmetic, arrays, for/if/while and pure functions β plus relational placement
(right-of / below / β¦) and room strips, resolved by deterministic topological arithmetic, not
an optimizer. All of it expands at compile time: no runtime, no clock, no I/O. Optional metric unit
suffixes (4m / 40cm / 20mm) fold exactly to millimetres at lex time.
Five output formats Β· accessible SVG Β· IDE-grade tooling
SVG, DXF and a TXT ASCII plan with zero dependencies; PDF (vector, selectable text)
and PNG (deterministic raster) via optional, lazily-loaded add-ons the default install never
pulls. arch compile --accessible stamps the SVG with <title>/<desc> + role="img".
A full LSP (hover, completion, go-to-definition, rename, signature help), an arch fmt
formatter, an arch explain <CODE> catalog, a self-documenting CLI (arch <cmd> --help, rendered
from the manifest, worked examples included), and a
VS Code extension.
π Quick start
npx @chanmeng666/archlang new -o plan.arch # scaffold a starter plan
npx @chanmeng666/archlang compile plan.arch -o plan.svg
Or install it:
npm install @chanmeng666/archlang
As a library (zero dependencies, runs in Node and the browser):
import { compile } from "@chanmeng666/archlang";
const { svg, diagnostics } = compile(`
plan "Tiny" {
units mm
grid 50
wall exterior thickness 200 { (0,0) (4000,0) (4000,3000) (0,3000) close }
room id=r at (0,0) size 4000x3000 label "Studio"
door at (2000,3000) width 900 wall exterior hinge left swing in
window at (0,1500) width 1200 wall exterior
}`);
// compile() never throws β errors come back as data, each with a span and a fix.
if (diagnostics.some((d) => d.severity === "error")) console.error(diagnostics);
else writeFileSync("tiny.svg", svg);
Also exported, all pure: describe() (facts), lint() (soundness), validateIntent() +
projectSubscores() (does it match the brief?), repair(), applyFixes(), suggestTopology(),
renderAscii(), toDxf(), and the LSP core (completion, hover, β¦).
Develop this repo
npm install # one install bootstraps every workspace
npm run build # build the library + CLI into dist/
npm run check # typecheck + lint + the full test suite
npm run check:drift # every generated artifact must match its source
npm run playground:dev # build the core, then open the playground
πΌοΈ Gallery
Every one of these is a real, compiled example from examples/ β click through to the
source.
|
studio The flagship: fitted kitchen & bath, enclosed bath off a central hall. Lint-clean. |
two-bed A larger plan: central corridor, multiple rooms and openings. |
attached No hand-computed coordinates: strips, on-wall openings, anchors. |
Also in examples/: parametric (a for loop that generates units), themed (a custom
theme + brick hatch), relational (right-of / below), and accessible (accTitle/accDescr).
The docs gallery renders all of them live and
editable in the browser.
ποΈ How it works
ArchLang is a compiler pipeline. Source text becomes a backend-neutral Scene IR, and every backend is a pure serializer of that scene β which is why adding a format never touches the language.
flowchart TD
SRC["<b>.arch</b> source"] --> LEX["<b>lexer</b><br/><i>hand-written β tokens with byte spans</i>"]
LEX --> PAR["<b>parser</b><br/><i>recursive descent β AST Β· recovers, never throws</i>"]
PAR --> IR["<b>resolve()</b><br/><i>expand scripting Β· grid-snap<br/>relational placement Β· host openings</i>"]
IR -->|"render"| SCN["<b>toScene()</b><br/><i>wall union Β· hatches Β· page</i>"]
IR -->|"read back"| DESC["<b>describe() Β· lint() Β· validateIntent()</b><br/><i>the SAME resolved plan, as FACTS β<br/>no rendering required</i>"]
SCN --> SVG["<b>SVG</b><br/><sub>zero-dep</sub>"]
SCN --> DXF["<b>DXF</b><br/><sub>zero-dep</sub>"]
SCN --> TXT["<b>TXT</b><br/><sub>zero-dep</sub>"]
SCN --> PDF["<b>PDF</b><br/><sub>optional</sub>"]
SCN --> PNG["<b>PNG</b><br/><sub>optional</sub>"]
DESC --> FACTS(["rooms Β· areas Β· adjacency<br/>access graph Β· intent score"])
style SRC fill:#ede7f6,stroke:#6b3ae0,color:#1a1a1a
style IR fill:#eceef2,stroke:#464d59,color:#1a1a1a
style SCN fill:#eceef2,stroke:#464d59,color:#1a1a1a
style DESC fill:#e8f5e9,stroke:#2e7d32,color:#1a1a1a
style FACTS fill:#e8f5e9,stroke:#2e7d32,color:#1a1a1a
style SVG fill:#fbfbfc,stroke:#464d59,color:#1a1a1a
style DXF fill:#fbfbfc,stroke:#464d59,color:#1a1a1a
style TXT fill:#fbfbfc,stroke:#464d59,color:#1a1a1a
style PDF fill:#fbfbfc,stroke:#8a8f98,color:#1a1a1a
style PNG fill:#fbfbfc,stroke:#8a8f98,color:#1a1a1a
The dotted branch is the point: describe, lint and the intent check read the same resolved
plan the renderer does β so what an agent verifies is exactly what gets drawn, and it costs no
pixels to check.
compile() is pure, synchronous and deterministic β no I/O, no Date.now(), no Math.random().
The only place Node APIs are allowed is the CLI; everything else gets its environment through a
World seam. See AGENTS.md and the ADRs.
π¦ Ecosystem
| Package / surface | What it is |
|---|---|
@chanmeng666/archlang | The core: compiler, CLI, analysis. Zero runtime deps, isomorphic. |
@chanmeng666/archlang-mcp | Optional stdio MCP server (the SDK is quarantined here). |
| VS Code extension | Syntax + live diagnostics, hover, completion, rename. |
| Playground | Client-side editor: preview, describe, lint, intent scoring, apply-fix, embed. |
| Docs site | Guide, reference, CLI reference, ADRs, live examples. |
| π€ Dataset | Synthetic, self-verifying repair trajectories + authoring pairs. CC0. |
| GitHub Action | Render ```arch fences in any repo's Markdown. |
Embed a plan anywhere
A live, editable plan in any blog, wiki or docs page β one <iframe>, no build step, nothing
sent to a server (the source rides in the compressed #z= hash, so the page is self-contained):
<iframe src="https://playground.archlang.uk/embed.html#z=β¦" width="720" height="480"></iframe>
The playground's Embed button generates the snippet. Optional params: editable=1 (show a
compact editor that re-renders as you type) and theme=blueprint|dark|mono|presentation.
Not on GitHub, though. GitHub's markdown sanitizer strips
<iframe>β it renders as escaped text, the same way<script>does β so a README (here or anywhere on github.com) cannot host a live embed, no matter how it's written. The honest substitutes GitHub does allow are what this README uses: a static SVG the compiler really produced, linked to a playground permalink that opens the same plan live. On the docs site, where the sanitizer doesn't apply, every plan is live and editable in place.
π Documentation
- π Docs site β guide, reference, error catalog, ADRs, and a live, editable examples gallery. Every
```archfence on a docs page is itself an editable plan. - β¨ CLI reference β every command, flag and exit code (generated from the manifest, so it can't fall behind).
- spec.llm.md β the whole language in one page (~2k tokens) for AI agents; also
arch spec. - SKILL.md β the agent Skill: the
spec β compile β fix β describe β validateloop. - Language Reference Β· Error catalog Β· The intent contract Β· ADRs
- AGENTS.md β orientation for AI agents working in this repo.
π€ Contributing
Contributions are welcome! Please read the Contributing Guide and our Code of Conduct. Use the issue and pull-request templates when you open one.
β€οΈ Support & Sponsor
- Questions? Open a Discussion or see SUPPORT.md.
- Found a security issue? Follow SECURITY.md.
- If this project helps you, consider sponsoring β.
π License
Released under the MIT license.