Architecture
Sister docs: PRD (intent), Implementation (deep-dive), Notes (decision log).
System view
flowchart TB
classDef client fill:#cce0e8,stroke:#1a1a1d,color:#1a1a1d,stroke-width:2px
classDef edge fill:#e0d5ed,stroke:#1a1a1d,color:#1a1a1d,stroke-width:2px
classDef core fill:#faedd6,stroke:#1a1a1d,color:#1a1a1d,stroke-width:2px
classDef host fill:#f4d6db,stroke:#1a1a1d,color:#1a1a1d,stroke-width:2px
subgraph Authors["🤖 Authors"]
LLM["LLM (Claude / GPT / etc.)
primary author"]
Human["Human via playground
secondary"]
end
subgraph Surface["🖼️ Surface"]
Playground["Astro playground site
textarea + live preview"]
Future["(M2) Embed web component
<diagram-engine>"]
end
LLM --> Playground
Human --> Playground
subgraph Engine["⚙️ Engine (single TS bundle, client-side)"]
Parser["Parser
recursive descent → AST"]
Validator["Validator
named-token errors"]
Layout["Layout engine
37 rules R1–R37"]
Renderer["SVG renderer
12 base shapes"]
Parser --> Validator
Validator --> Layout
Layout --> Renderer
end
Playground --> Engine
Engine --> SVG["SVG output
(deterministic bytes)"]
subgraph Deploy["☁️ Cloudflare"]
Pages["CF Pages
static Astro build"]
Tunnel["CF Tunnel
(preview branches)"]
end
Playground -.served from.-> Pages
Pages -.preview via.-> Tunnel
class LLM,Human client
class Playground,Future edge
class Parser,Validator,Layout,Renderer,SVG core
class Pages,Tunnel host
Data flow — Render
[1] Source text (≤200 tokens for a 10-node diagram)
web -> api -> postgres
api -> cache
│
▼
[2] Lexer: source → token stream
IDENT("web"), ARROW, IDENT("api"), ARROW, IDENT("postgres"),
NEWLINE, IDENT("api"), ARROW, IDENT("cache"), EOF
│
▼
[3] Parser (recursive descent): tokens → AST
Graph {
nodes: [web, api, postgres, cache],
edges: [(web→api), (api→postgres), (api→cache)],
direction: LR (default)
}
│
▼
[4] Validator: AST → ValidationResult
- check all referenced nodes are declared (implicit decl OK)
- check edge endpoints exist
- check no duplicate node IDs with conflicting shapes
- if error: return {ok: false, error: {token, expected, example}}
│
▼
[5] Layout engine: AST → LayoutTree (integer coordinates)
- apply R1–R10 (structural): flow direction, ranking, clustering
- apply R11–R20 (spatial): lane widths, padding, edge routing
- apply R21–R30 (semantic): grouping, ordering, alignment
- apply R31–R37 (visual): corner radius, stroke, icon padding
- all coordinates are integer; no floating-point drift
│
▼
[6] SVG renderer: LayoutTree → SVG string
- emit <svg> root with computed viewBox
- emit one <g> per node (shape + label + optional icon)
- emit one <path> per edge (with optional label group)
- emit defs for any custom inline SVG icons
│
▼
[7] DOM insertion (playground) or string return (embed/API)
Data flow — Repair loop (when validator errors)
[1] LLM emits source with a mistake:
web -> ap1 # typo: ap1 vs api
api -> cache
│
▼
[2] Validator runs after parse:
{
ok: false,
error: {
kind: "undeclared_reference",
token: "ap1",
line: 1, col: 8,
expected: "an identifier matching a previously seen node, or a new node declaration",
example: "web -> api # if you meant 'api', spell it consistently"
}
}
│
▼
[3] Error surfaced to LLM (in chat) or human (in playground)
Playground: red underline + tooltip with example
LLM: structured JSON error → repair in one shot
│
▼
[4] LLM emits repaired source:
web -> api
api -> cache
│
▼
[5] Re-render → SVG output
Repair rate (88%): on a held-out eval set, 88% of validator errors are fixed by the LLM on a single retry given the structured error.
Data flow — Deploy
[git push to main branch]
│
▼
[Cloudflare Pages build]
│ astro build → ./dist (static HTML/CSS/JS + SVG)
│ engine bundle = single TS → ESM module
│ playground entry = inline <script type="module">
▼
[CF Pages serves from edge]
│ static assets cached at PoP
│ no backend invocation per render
▼
[User browser]
│ loads Astro page
│ engine module evaluated client-side
│ render happens entirely in browser
▼
[SVG visible inline in playground]
No backend on the render hot path. This matters: a serverless backend would have added cold-start latency to every render and a per-render cost line item. Client-side keeps the playground free to host and instant to use.
Component responsibilities
| Component | Owns | Doesn’t own |
|---|---|---|
| Source text | Source of truth for the diagram | Layout, theming, style |
| Lexer | Source → token stream | AST shape, validation |
| Parser | Tokens → AST (with source positions for errors) | Validation logic, layout |
| Validator | AST shape correctness + structured error messages | Visual correctness (“is this readable?”) |
| Layout engine | AST → LayoutTree, all 37 rules, integer coords | Pixel-level rendering, fonts |
| SVG renderer | LayoutTree → SVG string, 12 shapes, icon refs | Layout decisions, validation |
| Playground site | Textarea, live preview, URL fragment encoding | Engine logic (just calls in) |
| CF Pages | Static hosting + edge cache | Render, validate (all client-side) |
| LLM (caller) | Authoring source, repairing on validator errors | Rendering, layout, validation |
Failure modes & recovery
| Failure | Detect | Recovery | Time |
|---|---|---|---|
| Invalid source syntax | Parser throws with line/col | Validator surfaces structured error to caller | <1 ms |
| Undeclared node reference | Validator catches | Structured error → LLM repair in one shot | <1 ms (validator) + 1 LLM call |
| Layout produces overlapping nodes | Visual regression (manual eyeball) | Add a new layout rule (R{N+1}), bump rule catalog | hours (rule design) |
| Renderer emits malformed SVG | Browser fails to render | Snapshot test in CI catches before deploy | caught pre-deploy |
| Determinism regression (same input, different output) | Byte-comparison test in CI | Block deploy until root-caused | caught pre-deploy |
| CF Pages outage | User sees host error | Static fallback via CF’s global edge; rare | minutes |
| Custom-icon SVG sanitization bypass | (future) untrusted icons would be a risk | M1 only renders user-pasted icons in the user’s own browser; no shared host | N/A in M1 |
Why these choices
| Decision | Alternative considered | Why this won |
|---|---|---|
| Hand-written recursive descent over Tree-sitter | Tree-sitter grammar | Grammar churn would have leaked into the LLM-facing surface; a hand-rolled parser keeps the grammar locked and the error messages curated |
| Custom layout over ELK.js | ELK.js (mature, Java port) | ELK is powerful but not byte-deterministic across versions; we need byte-stable output |
| SVG over Canvas | HTML5 Canvas | SVG is scalable, copy-pasteable into other tools, and diffable in version control |
| Astro over Next.js | Next.js | Static-first, lighter cold-start on CF Pages, no React runtime needed for a textarea + live preview |
| Cloudflare Pages over Vercel | Vercel | Plays well with the existing Cloudflare zone + tunnels; free tier is sufficient |
| Client-side render over serverless render | Cloudflare Workers / Lambda | Zero cold-start, zero per-render cost, the engine bundle is small enough (~80 KB gzipped) |
| Playground first, npm deferred | Publish npm first | Real adoption comes from people pasting into the textarea; npm without a known consumer is over-engineering |
| Single grammar over multi-dialect | Multiple flavors for different diagram families | Constraint #1 (single render path); ambiguity is the enemy of first-shot LLM success |
| Integer coordinates over float | Float coords with rounding at render time | Determinism is a first-class feature; float math drifts |
| 12 base shapes (not extensible in M1) | Plugin shape registry | Plugins re-introduce the dialect problem; lock the shape set, evolve it deliberately |
See also
- DSL grammar + layout rule catalog in Implementation
- Decision log in Notes