← Back to project
● Shipped P1 Size M Developer tool

Diagram-Engine — Architecture

Render pipeline, parser/layout/renderer split, error model, and deploy topology.

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

ComponentOwnsDoesn’t own
Source textSource of truth for the diagramLayout, theming, style
LexerSource → token streamAST shape, validation
ParserTokens → AST (with source positions for errors)Validation logic, layout
ValidatorAST shape correctness + structured error messagesVisual correctness (“is this readable?”)
Layout engineAST → LayoutTree, all 37 rules, integer coordsPixel-level rendering, fonts
SVG rendererLayoutTree → SVG string, 12 shapes, icon refsLayout decisions, validation
Playground siteTextarea, live preview, URL fragment encodingEngine logic (just calls in)
CF PagesStatic hosting + edge cacheRender, validate (all client-side)
LLM (caller)Authoring source, repairing on validator errorsRendering, layout, validation

Failure modes & recovery

FailureDetectRecoveryTime
Invalid source syntaxParser throws with line/colValidator surfaces structured error to caller<1 ms
Undeclared node referenceValidator catchesStructured error → LLM repair in one shot<1 ms (validator) + 1 LLM call
Layout produces overlapping nodesVisual regression (manual eyeball)Add a new layout rule (R{N+1}), bump rule cataloghours (rule design)
Renderer emits malformed SVGBrowser fails to renderSnapshot test in CI catches before deploycaught pre-deploy
Determinism regression (same input, different output)Byte-comparison test in CIBlock deploy until root-causedcaught pre-deploy
CF Pages outageUser sees host errorStatic fallback via CF’s global edge; rareminutes
Custom-icon SVG sanitization bypass(future) untrusted icons would be a riskM1 only renders user-pasted icons in the user’s own browser; no shared hostN/A in M1

Why these choices

DecisionAlternative consideredWhy this won
Hand-written recursive descent over Tree-sitterTree-sitter grammarGrammar 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.jsELK.js (mature, Java port)ELK is powerful but not byte-deterministic across versions; we need byte-stable output
SVG over CanvasHTML5 CanvasSVG is scalable, copy-pasteable into other tools, and diffable in version control
Astro over Next.jsNext.jsStatic-first, lighter cold-start on CF Pages, no React runtime needed for a textarea + live preview
Cloudflare Pages over VercelVercelPlays well with the existing Cloudflare zone + tunnels; free tier is sufficient
Client-side render over serverless renderCloudflare Workers / LambdaZero cold-start, zero per-render cost, the engine bundle is small enough (~80 KB gzipped)
Playground first, npm deferredPublish npm firstReal adoption comes from people pasting into the textarea; npm without a known consumer is over-engineering
Single grammar over multi-dialectMultiple flavors for different diagram familiesConstraint #1 (single render path); ambiguity is the enemy of first-shot LLM success
Integer coordinates over floatFloat coords with rounding at render timeDeterminism is a first-class feature; float math drifts
12 base shapes (not extensible in M1)Plugin shape registryPlugins re-introduce the dialect problem; lock the shape set, evolve it deliberately

See also