diagram-contracts
v0.2.1
Published
Rule Catalog driven layout runtime — compile diagrams to deterministic SVG from Draw TSX, Draw Text, or Layout IR
Maintainers
Readme
diagram-contracts
A Universal Layout Runtime that compiles the Rule Catalog into a Constraint Graph and deterministically generates Artifacts.
diagram-contracts is neither a shape DSL nor a layout DSL. All layout — VStack, HStack, Table, Connector — is expressed as a Rule Catalog and executed by the same Graph Runtime. AI can safely describe diagrams, layouts, and relationship structures; the engine converts them to SVG deterministically.
Sample Output
AWS System Architecture (Draw TSX → SVG)
Source:
assets/readme/system-architecture.draw.tsx— entry diagramassets/readme/system-parts.draw.tsx— VPC and service part componentsassets/readme/components/aws.draw.tsx— reusable AWS primitives (AwsVpc,AwsService,AwsAccount, …)
ER Diagram (Draw TSX → SVG)
Source: assets/readme/showcase-tsx.draw.tsx
Regenerate:
diagram-contracts render assets/readme/system-architecture.draw.tsx -o assets/readme/system-architecture.svg
diagram-contracts render assets/readme/showcase-tsx.draw.tsx -o assets/readme/showcase-tsx.svgInput Formats
| Format | Notation | Use Case |
|--------|----------|----------|
| Draw TSX | Restricted TSX (.draw.tsx) | Recommended. Type-safe production definitions, componentization, complex layouts |
| Layout IR | JSON | Tool integration, normalized intermediate representation |
| Draw Text | Mermaid-style text | Quick input, documentation embedding, Mermaid import |
All three formats are converted to Layout IR (JSON); the downstream layout engine is shared.
Draw TSX (Recommended)
Define diagrams in restricted TSX in .draw.tsx files. TypeScript type checking, component decomposition, and IDE completion are available — suitable for production definitions.
<Document id="arch" padding={20} fontFamily="Inter, sans-serif">
<HStack gap={60} align="center">
<Box id="client" shape="roundedRect" width={120} height={60}>
<Text textAlign="center">Client</Text>
</Box>
<Box id="api" shape="roundedRect" width={120} height={60}>
<Text textAlign="center">API Server</Text>
</Box>
</HStack>
<Connector from="client" to="api" endArrow="arrow" route="orthogonal" />
<AlignConstraint targets={["client", "api"]} axis="horizontal" align="center" />
</Document>Component Definitions
Draw TSX supports defining and using components (functions). Reusable diagram elements can be split into files and imported.
// components/aws.draw.tsx
export function AwsVpc({ label, children }: { label: string }) {
return (
<Box shape="rect" padding={0} stroke="#8c4fff" strokeWidth={2} fill="white">
<VStack gap={0}>
<Box shape="rect" fill="#8c4fff" stroke="#8c4fff" strokeWidth={0} padding={8}>
<HStack gap={6} align="center">
<Icon name="aws:vpc-group" width={18} height={18} />
<Text fontWeight="bold" fontSize={12} foreground="white">{label}</Text>
</HStack>
</Box>
{children}
</VStack>
</Box>
);
}Component and built-in resolution follows the React convention: a defined or imported component takes precedence over a built-in element of the same name. Naming a component Cell, Divider, Row, etc. therefore uses your component (not the built-in), and the compiler emits a one-time warning noting the shadow so the precedence is never silent. Rename the component to reach the built-in element. Imported components are in scope both at the document tree and inside other component bodies (including transitively, when an imported file imports its own components).
Caution — structural names. Shadowing a structural built-in used by a composite element (
Row/CellinsideTable) means every<Row>/<Cell>in the document — including those inside a<Table>— resolves to your component, which breaks table layout. Prefer names that do not collide with built-ins for reusable components.
Children
Element children may be other elements, components, the {children} slot (inside a component body), and {array.map(...)} over a literal array:
<VStack gap={5}>
{["alpha", "beta", "gamma"].map((label, i) => <Item t={label} n={i} />)}
</VStack>The array must be a literal so its length is known at compile time; the callback must be an inline function returning a single JSX element. Its parameter (and optional index) bind per element, while enclosing component props referenced in the body stay deferred until that component is expanded. Mapping over a runtime value (e.g. a component prop) or any other unsupported child expression is a compile error, never a silent drop. An unknown element name (neither a built-in nor a defined/imported component) is likewise a compile error.
Prop Expressions
Draw TSX is compiled statically — there is no JSX runtime. Prop expressions are therefore restricted to a grammar the compiler can constant-fold at compile time:
| Supported | Example |
|-----------|---------|
| Literals (number / string / boolean / template) | width={50}, fill="#f00", visible={false} |
| Props references (inside a component) | height={h} |
| Arithmetic + - * / % with standard precedence | height={h - 16}, width={2 + 3 * 4} (= 14) |
| Parentheses and unary minus, arbitrarily nested | width={-(h + 2) * 3 + 100} |
| String concatenation (string + string) | label={"REST " + "API"} |
| Template literals with expression spans | id={`item-${i + 1}`} |
| Arrays and object literals of the above | targets={["a", "b"]}, geometry={{ align: "x" }} |
Arithmetic over component props is folded when the component is expanded, so height={h - 16} with <Shell h={100} /> renders 84.
Everything else is a compile error naming the prop and the offending syntax — unsupported expressions are never silently dropped to element defaults:
- Function calls (
Math.max(…)), member access (obj.w), ternaries, logical operators (&&,||), spread attributes ({...rest}), and any other syntax → error. - Arithmetic requires numbers;
+also accepts two strings. Mixed string/number+is an error (no implicit coercion). - Division or modulo by zero — and any other non-finite result, including overflowing literals like
1e999— → error at compile time; non-finite values never reach layout. - Referencing a prop that the caller did not provide inside an expression → error.
- Exception:
null/undefinedas the entire prop value mean "prop not set" (standard JSX semantics) and fall back to the element's schema default without a diagnostic. Inside an expression ({5 + null},{-null},{`a${undefined}`}) they are a compile error.
Geometry Roles
A mechanism for referencing internal child elements from outside a component. Specify align / connect roles via the geometry property so Connectors and AlignConstraints reference the correct element without breaking component internals.
<VStack id="svc" geometry={{ align: "svc-icon", connect: "svc-icon" }}>
<Icon id="svc-icon" name="aws:ecs" width={48} height={48} />
<Text fontSize={11}>ECS</Text>
</VStack>
<Connector from="svc" to="db" /> {/* connects from svc-icon rect */}
<AlignConstraint targets={["svc", "db"]} /> {/* aligns on svc-icon rect */}See docs/geometry-role.md for details.
Property Validation
The compiler dynamically derives allowed property lists from the SSoT in layout-rules.yaml and reports unknown properties as errors.
[error] Box: Unknown property "paddingTop" on <Box>. Allowed: fill, height, opacity, padding, ...Text Measurement
During CLI rendering, text width and height are computed accurately using opentype.js metrics from fonts specified by fontFamily (@fontsource packages). fontWeight="bold" is reflected as well.
Layout IR
The JSON intermediate representation that all input formats converge on. Suitable for tool integration or external generation.
{
"kind": "document",
"props": { "padding": 20 },
"children": [
{
"kind": "hstack",
"props": { "gap": 60, "align": "center" },
"children": [
{ "kind": "box", "id": "a", "props": { "width": 120, "height": 60 } }
]
}
]
}Draw Text (Quick Input / Mermaid Import)
A lightweight notation that uses Mermaid flowchart syntax as its input format. Suitable for documentation embedding, quick input, and importing existing Mermaid assets.
flowchart LR
client[Client]:::primary
api[API Server]:::surface
db[(Database)]
client -->|REST API| api
api -.->|SQL| db
client ~~~ api ~~~ dbdiagram-contracts is not a Mermaid compatibility implementation. It adopts Mermaid as an import format; layout is determined by diagram-contracts' own Rule Catalog Driven approach.
See docs/draw-text.md for details.
Web Embedding
Place Draw Text source in HTML and render with client-side JS.
<pre class="diagram-contracts" data-theme="default">
flowchart LR
client[Client]:::primary
api[API Server]:::surface
db[(Database)]
client -->|REST API| api
api -.->|SQL| db
client ~~~ api ~~~ db
</pre>
<script type="module">
import { renderDrawText } from 'diagram-contracts/browser';
for (const el of document.querySelectorAll('.diagram-contracts')) {
renderDrawText(el.textContent, {
mount: el,
theme: el.dataset.theme,
});
}
</script>Architecture
Draw TSX ──→ compile ──┐
Draw Text ──→ parse ────┼→ Layout IR (JSON)
Layout IR ──────────────┘
↓ normalize (geometry role resolution)
↓ Rule Catalog lookup (kind → Fragment Factory)
↓ Graph Fragment generation (slots + operations + edges)
↓ merge → Constraint Graph
↓ evaluate (topological resolve)
Resolved Layout Artifact (JSON)
↓ render
SVG (inline) / Canvas / PNGDesign Philosophy: Rule Catalog Driven Layout
Layout construct is not an executor. Layout construct is a Rule Fragment Generator.
Layout constructs (VStack, HStack, Table, Connector, etc.) do not execute layout themselves. Each construct is a Rule Catalog entry that only generates the corresponding Graph Fragment (slot + operation + candidate edge). Actual layout computation is performed in bulk by the Graph Runtime.
- Rule Catalog defines the language:
layout-rules.yamlandconstraint-rules.yamldeclaratively define all layout behavior style_propsis the property SSoT: layout params + style_props drive compiler property validation- Table is a Rule Bundle: no dedicated Table algorithm; expressed as a row×column constraint rule set
- Connector is a first-class Graph element: Node, Group, Table, and Connector all coexist on the same Graph; group connections and cell connections are supported
- Geometry Roles: safely reference internal component elements from outside
- Size calculation always uses bounding rect;
shapeis visual only - Same input always produces the same SVG (deterministic)
- AI must not write SVG coordinates, paths, or transforms
Performance
Pipeline stage timings (Node.js v24, JIT-warmed, 200 iterations, median / p95 in parentheses):
| Stage | ER Diagram (298 nodes) | AWS Architecture (217 nodes) | |-------|------------------------|------------------------------| | compile (TSX→IR) | 1.7ms (4.5ms) | 2.1ms (3.4ms) | | normalize | 0.7ms (0.9ms) | 0.7ms (1.0ms) | | evaluate | 10.3ms (15.6ms) | 5.9ms (10.1ms) | | renderSvg | 0.3ms (0.8ms) | 0.3ms (0.6ms) | | TOTAL | 13.2ms (19.9ms) | 9.1ms (13.7ms) |
Cold start (first run, no JIT): ER Diagram 114ms / AWS 52ms. In browser Web embedding, only the first render is cold; JIT optimization applies thereafter.
Re-benchmark: node scripts/bench.mjs --warmup 50 --iterations 200
CLI
# Draw TSX → SVG (recommended)
diagram-contracts render diagram.draw.tsx -o output.svg
# Layout IR (JSON) → SVG
diagram-contracts render input.json -o output.svg
# Draw Text → SVG
diagram-contracts render diagram.txt -o output.svg
# stdin → SVG
echo '{"kind":"document","props":{"padding":20},"children":[]}' | diagram-contracts render
# Validate IR
diagram-contracts validate input.json
# Output normalized IR / resolved artifact
diagram-contracts ir input.json --stage normalized
diagram-contracts ir input.json --stage artifactAuto-detects input format: JSON ({), Draw Text (flowchart/graph), Draw TSX (<).
Node.js API
import {
parseDrawText,
compileDrawTsx,
parseIr,
renderToSvg,
renderDrawTextToSvg,
renderDrawTsxToSvg,
layout,
} from "diagram-contracts";
// Draw TSX → SVG (recommended)
const svg = renderDrawTsxToSvg(`
<Document padding={20}>
<Box id="a" shape="roundedRect"><Text>Hello</Text></Box>
</Document>
`);
// Draw Text → SVG
const svg2 = renderDrawTextToSvg(`
flowchart LR
A[Start] --> B[End]
`);
// Step-by-step pipeline
const { ir } = parseDrawText(source);
const { artifact } = layout(ir, { tokens: {}, theme: { defaults: {}, variants: {} } });
const svgString = renderSvg(artifact);Browser
<!-- Auto-initialization (recommended) -->
<script type="module" src="diagram-contracts/browser"></script>
<pre class="diagram-contracts">
flowchart LR
A[Start] --> B[End]
</pre>
<!-- Manual API -->
<script type="module">
import { renderDrawText, renderIr } from "diagram-contracts/browser";
const svg = renderDrawText("flowchart LR\n A --> B");
document.getElementById("target").innerHTML = svg;
</script>Elements with class .diagram-contracts, [data-diagram-contracts], or pre[class*="language-draw"] are auto-rendered on page load. New elements are detected via MutationObserver.
Mermaid Input Support
Draw Text can use Mermaid flowchart syntax as an input format. Existing Mermaid sources can be used as-is.
| Category | Support |
|----------|---------|
| flowchart TD/TB/LR | ✅ |
| graph TD | ✅ |
| Node brackets [], (), ([]), {}, [()], (()), [[]] | ✅ |
| Visible edges -->, ---, -.->, ==> | ✅ |
| Edge labels \|label\| | ✅ |
| Invisible edges ~~~ | ✅ (→ alignConstraint) |
| subgraph ... end | ✅ |
| :::className | ✅ (→ variant) |
| %% comment | ✅ |
| flowchart BT/RL | ❌ not supported in v1 |
| sequenceDiagram, etc. | ❌ flowchart only |
Why this approach?
Existing layout tools each use different algorithms:
| Tool | Approach | Limitation | |------|----------|------------| | CSS Flexbox / Grid | UI layout model | Cannot handle Connectors (arrows). Poor fit for diagram relationship structures | | Graphviz | Automatic graph placement | Node/Edge focused. Weak at structural layouts like Table/Grid | | Mermaid | Graphviz-based + per-type renderers | Separate algorithm per diagram type. Cannot compose declarative constraints | | D2 | dagre-based | Same as above. Hard to add custom constraints |
diagram-contracts solves this by expressing everything as Rule Catalog entries and reducing them to the same Graph Runtime:
- VStack / HStack → Rule Fragments that determine child position and size
- Table → Rule Bundle for row×column grid (no dedicated algorithm)
- Connector → Rule Fragment depending on source/target anchor slots
- AlignConstraint → Rule Fragment generating equivalence constraints between slots
All coexist on the same Constraint Graph, so consistent constraint resolution applies even when Nodes, Connectors, and Tables are mixed.
Documentation
Specifications, notation, and architecture are collected in docs/.
| File | Description | |------|-------------| | docs/diagram-contracts.md | Language specification — IR, Rule Catalog, Graph Runtime | | docs/layout-algorithm.md | Layout algorithm via Rule Catalog + Graph Runtime | | docs/geometry-role.md | Geometry Roles — external references to internal component elements | | docs/diagram-vocabulary.md | Elements, properties, and IR kinds | | docs/draw-text.md | Draw Text — Mermaid syntax as input format |
Invariants
- Bounding is rect — layout = rect, shape = decoration
- Connector is a first-class Graph element — coexists on the same Constraint Graph as Node, Group, Table
- Layout IR is JSON — convergence point for all input formats
- Rule Catalog is the language definition —
layout-rules.yaml/constraint-rules.yamlare the SSoT for all layout behavior style_propsis the property SSoT — compiler validation is dynamically derived from YAMLrequiredis a hard constraint — fail-fast if unsatisfiable
