Deterministic C4 layout engine, with optional AI polish
Nudge automatically produces clean, publication-ready C4 Model architecture diagrams. Its quality floor comes from deterministic layout rules: a custom Container Layout Engine for both context and container diagrams, orthogonal grid routing (A* pathfinding over a sparse orthogonal visibility graph) with rip-up-and-reroute, channel nudging, and collision-aware Connection Label placement. ELKjs remains the engine for flat diagrams with no person/external elements.
Local LLM reviewers are an optional enhancement layer. They can suggest connection label placement overrides for context and container diagrams, or ELKjs option patches for flat diagrams, but those suggestions are accepted only when they do not worsen the geometric score.
Nudge runs as a CLI tool for direct use from the terminal, and as a local MCP server so LLM clients like Claude Desktop can call it as a tool β generating and optimizing diagrams in a single conversational step.
| Before (Mermaid C4 Baseline) | After (Nudge Optimized) |
|---|---|
![]() |
![]() |
See the full comparison source in docs/core-banking-single-boundary-comparison.md.
A second optimized container example β the search service β shows the grid router's orthogonal connection lines, with each connection label and arrowhead kept clear of its line so a dash always precedes the arrowhead:
Nudge is built around a deterministic renderer with an optional accept-only-if-not-worse enhancement pass:
graph TD
A[Input YAML, Mermaid, or PlantUML] --> B[Parse Diagram Model]
B --> N[Normalize: wrap C4Context internals in a hidden synthetic boundary]
N --> C{Has boundary?}
C -->|Yes| D[Deterministic Container Layout Engine]
C -->|No| E[ELKjs Layered Layout]
D --> F[Render in Playwright]
E --> F
F --> G[Geometric Critique Analyzer]
G --> H{Enhancement enabled?}
H -->|Yes| I[Optional LLM Hint/Patch]
I --> J[Accept only if score is not worse]
H -->|No| K[Export SVG & PNG]
J --> K
G --> K
- Ingestion: Parses C4 Context or C4 Container diagrams from
.mermaid,.puml/.plantuml, or.yamlspecifications. C4Context diagrams are normalised by wrapping their internal architecture elements in a hidden synthetic boundary β persons and external systems stay outside β so context diagrams reuse the full container pipeline. One boundary per diagram β see Scope. - Deterministic rendering: Produces a complete baseline layout without needing cloud services. Context and container diagrams use Nudge's custom deterministic rules; flat diagrams without person/external elements use ELKjs.
- Geometric critique: Measures DOM bounding boxes to detect Element Overlaps, Connection-Line Element Crossings, Connection-Label Element Crossings, poor aspect ratio, and tight spacing. Container routing uses A* pathfinding over a sparse orthogonal visibility graph, hardest-first routing, and rip-up-and-reroute to optimize line paths and avoid corridors.
- Optional enhancement: When LLM calls are enabled, Nudge asks a local OpenAI-compatible model for small layout improvements. Container and context diagram hints are accepted only when the candidate score is no worse than the current accepted state. Flat diagram patches are applied through the existing critic loop.
- Export: Writes a best-effort SVG and PNG even if geometric issues remain.
Nudge lays out exactly one boundary per diagram, and refuses diagrams with sibling or nested boundaries rather than drawing them partially.
This mirrors C4 itself: a Container diagram shows the containers inside a single system in scope, with other systems outside it as external elements, and a Component diagram zooms into a single container. The boundary is the level of zoom.
β Unsupported diagram
This diagram declares 2 top-level boundaries ("Experience Layer" (frontend),
"Platform Services" (platform)). Nudge lays out one boundary per diagram.
In C4 a Container diagram shows the containers inside a single system in scope,
with other systems outside it as external elements. To draw this, either split it
into one diagram per system, or keep one boundary and declare the others as
System_Ext(...) outside it.
The source keyword is not restricted β System_Boundary, Container_Boundary and Enterprise_Boundary all describe one boundary, so an Enterprise_Boundary wrapping plain systems renders normally. Only nesting and siblings are refused.
The check runs in normalizeDiagramModel, so the CLI, the MCP server and the public API all behave identically. Programmatic callers get an UnsupportedDiagramError with code: 'UNSUPPORTED_DIAGRAM', a reason of multiple_boundaries or nested_boundary, and the offending boundary ids.
Nudge uses two distinct engines depending on the diagram shape:
Diagrams with no boundary and no person/external elements are laid out using the Eclipse Layout Kernel (ELKjs) with a layered algorithm. The current CLI can optionally run the LLM critic loop to tune ELKjs layout properties such as spacing, node distances, and routing directions. A stronger no-LLM flat-diagram baseline is tracked as follow-up work in REFRAMING.md.
Container diagrams containing boundary blocks bypass ELKjs entirely and use a custom deterministic layout pipeline. C4Context diagrams take the same pipeline: their internal architecture elements are wrapped in a synthetic boundary that drives the layout but is never drawn, while persons and external systems stay outside in the external zones. The central system of a context diagram renders with a [Software System] type label.
- Phase 1: Kahn Layering (Boundary Interior): Children of the boundary are sorted into horizontal layers using a modified Kahn's topological sort. Nodes receiving cross-boundary edges are automatically seeded as entry nodes in the top row (Layer 0); unconnected utility nodes are pushed down to minimise clutter. When every element sits on a relationship cycle (e.g. mutual request/response pairs), the first layer is seeded with the most source-like element(s) β maximum out-degree minus in-degree; remaining cycles are silently broken by appending leftover nodes to a final layer.
- Phase 2: Dedicated Utility Rows: Message buses and databases are excluded from Kahn's sort and reinserted into purpose-built rows. Busy buses widen into clear spines; all message buses are corner-anchored in the bottom-right. Databases sit in tighter rows beneath their deepest contributing service.
- Phase 3: Zone Classification & Connectivity Sorting: External nodes are classified into layout zones β callers go above, callees go below, and overflow nodes spill into left and right columns. Each zone is automatically sorted by the average layer/column index of the internal nodes it connects to, so external nodes align visually with their counterparts inside the boundary with minimal edge crossings.
- Phase 4: Orthogonal Grid Routing: Connection lines are routed using an A* pathfinding algorithm on a sparse orthogonal visibility graph. Vertices are generated at inflated element boundaries, centerlines, and channel midlines to keep the search space small and keep routes centered. A* search state includes the current heading to penalize bends. Face ports are reserved on use with cost penalties. The router routes lines hardest-first, followed by an iterative rip-up-and-reroute loop that optimizes global crossings, overlaps, and bends. Finally, a channel nudging phase offsets overlapping parallel segments into separate lanes. Standard straight orthogonal and diagonal lines are rendered. The grid router now routes every relationship Nudge accepts (326/326 across the test corpus, asserted absolutely), so the legacy candidate router is dead code retained only behind
NUDGE_ROUTER=legacyand slated for removal. - Phase 5: Rule-Based Edge Label Placement: Relationship labels are placed along the edge using four strategies evaluated in order: (0) straight-line midpoint, (1) target-anchored, (2) source-anchored, (3) edge-density-aware segment scoring. Every strategy checks for collision against node bounding boxes, source/target boxes, previously-placed labels, and nearby connection lines, so duplicate-text labels on co-terminal edges are separated and labels avoid busy corridors where possible. Long labels wrap automatically; technology notes (e.g.
[HTTPS]) are rendered in a smaller, semi-transparent style beneath the main text.
Before final export, the optimizer renders and scores a sequence of visual states:
step_0_initial.pngβ deterministic container layout.step_1_label_hints.pngβ connection label placement overrides (e.g. placing long connection labels at the source or target endpoints instead of the default middle) suggested bygetLLMLabelPlacementHints.
Each candidate is scored against Element Overlaps, Connection-Line Element Crossings, Connection-Line conflicts, Label-Line Intersections, bend count, and route length. A hint is accepted only when the candidate score is no worse than the current accepted state. Accepted hints are written to diagramModel._layoutOverrides.labelHints, and raw LLM responses are saved to visual_hints.json.
- π― Deterministic C4 Layout Engine: Produces a complete layout from structured rules before any LLM enhancement is considered.
- π Opinionated, Consistent C4 Geometry: Standardized sizing, dedicated Utility Rows, External Zone sorting, and repeatable route scoring make diagrams feel consistent from run to run.
- π Automatic Defect Detection: Finds Element Overlaps, Connection-Line Element Crossings, Connection-Label Element Crossings, and poor aspect ratios.
- β¨ Optional AI Polish: Local LLM reviewers can suggest connection label placements (source/target/middle) for context and container diagrams, or ELKjs patches for flat diagrams. Suggestions are accepted only through score-gated checks.
- π MCP Server: Exposes an
optimize_diagramtool over stdio so Claude Desktop and other MCP clients can generate and render diagrams conversationally. - π¨ Supports Mermaid, PlantUML & YAML: Seamless support for C4 diagrams in
.mermaid/.mmdsyntax,.puml/.plantumlC4-PlantUML syntax, and structured.yamlspecifications. - π Standardized Sizing & Grid: All nodes are standardized to a width of
200px. Heights:200pxfor Person,140pxfor Container/Database/External,80pxfor MessageBus β ensuring consistent alignment and a clean grid. - π§ Dedicated Bus & Database Rows: Message buses and databases are separated from ordinary service layers, with message buses always anchored in the bottom-right and databases visually paired beneath their owner service.
- π¬ 3-Line Descriptions: Node descriptions support a 3-line clamp, providing space for detailed technical notes without clipping.
- π Auto-Placed Annotation Notes: Free-text notes anchor to an element with a directional hint (
right/left/over); the geometry critic measures them for occlusion and relocates them to a clear side without worsening the layout. See Annotation Notes. - π·οΈ Collision-Aware Label Placement: Edge labels check four strategies sequentially β straight-middle, target-anchored, source-anchored, and edge-density-aware segment scoring. Each strategy checks for collision against node boxes, already-placed labels, and connection lines, preventing co-terminal labels from stacking and reducing label-edge intersections.
- π Orthogonal Grid Routing: Connection lines use A* search over a sparse orthogonal visibility graph, keeping track of heading to penalize bends. Multiple slots on faces act as shared port resources. The system runs hardest-first, performs rip-up-and-reroute optimization, and finishes with a channel nudging phase that spreads parallel segments. Legacy candidate routing remains as a fallback for unplaced leaf/cross-hierarchy elements.
- π Connectivity-Based Zone Sorting: External nodes are auto-sorted to align with the internal layer or column they connect to, reducing edge crossings without LLM intervention.
- π Local-First Runtime: Rendering runs locally with Playwright and bundled ELKjs. Optional AI polish can use a local OpenAI-compatible backend such as LM Studio.
Before using Nudge, make sure you have:
- Node.js: Version 18 or newer.
- Playwright Browsers: Required for rendering diagrams.
npx playwright install chromium
- Optional Local LLM Server: Required only when running LLM enhancement. Use an OpenAI-compatible API server on
http://127.0.0.1:1234(e.g., LM Studio).- Recommended Models:
google/gemma-2-9b-it,google/gemma-4-12bor other reasoning models.
- Recommended Models:
git clone https://github.com/cookie-bytes/nudge.git
cd nudge
npm installDeterministic layouts run by default without requiring an LLM server or network connection:
node src/cli/index.js path/to/diagram.mermaidTo enable optional AI polish / LLM layout enhancement, start your local LLM server (default port 1234) and pass the --enhance flag:
node src/cli/index.js path/to/diagram.mermaid --enhanceExported SVGs carry both palettes and follow the reader's system preference, so one file is readable on a light page and a dark one. PNGs can't do that β a raster has to pick β so they render dark.
Pass --theme to pin a palette instead, or both to get a matching pair of
files from a single run:
node src/cli/index.js path/to/diagram.mermaid --theme light--theme |
PNG | SVG |
|---|---|---|
auto (default) |
dark | follows prefers-color-scheme, dark where unknown |
dark / light |
that palette | that palette, pinned |
both |
one of each | one of each, pinned |
A theme changes only colour β the layout is identical β so --theme both costs
one extra screenshot rather than a second layout pass. With both, the dark
outputs keep the usual optimized.png / optimized.svg names and the light pair
is written alongside as optimized.light.png / optimized.light.svg.
Every colour is a named token in src/render.html, and both palettes are checked
against WCAG AA contrast floors by test/unit/theme_contrast.test.js.
You can customize Nudge's layout tuning behavior and LLM connections using environment variables:
| Variable | Description | Default / Fallback |
|---|---|---|
NUDGE_LLM_API |
The base URL of your OpenAI-compatible API endpoint. | http://127.0.0.1:1234 |
NUDGE_LLM_MODEL |
The model name Nudge should request. | google/gemma-4-12b |
NUDGE_LLM_API_KEY |
The API Key to send in the Authorization header. |
(none) |
OPENAI_API_KEY |
Alternative API key if NUDGE_LLM_API_KEY is unset. |
(none) |
NUDGE_NO_LLM |
Deprecated. Forces LLM calls to be skipped even if the --enhance flag is passed. |
(unset) |
Examples:
- Local LM Studio/Ollama (Custom Port):
export NUDGE_LLM_API=http://localhost:11434 export NUDGE_LLM_MODEL=gemma2:9b
- Remote Hosted API (e.g., OpenRouter / OpenAI):
export NUDGE_LLM_API=https://openrouter.ai/api/v1 export NUDGE_LLM_MODEL=google/gemma-2-9b-it export NUDGE_LLM_API_KEY=sk-or-...
Optimize the included Core Banking container diagram (the example shown in the comparison above):
node src/cli/index.js test/fixtures/diagrams/core/core-banking-single-boundary.mermaidRun on any Mermaid, PlantUML, or YAML file:
node src/cli/index.js path/to/diagram.mermaid
node src/cli/index.js path/to/diagram.puml
node src/cli/index.js path/to/diagram.yamlYAML is an alternative to Mermaid syntax that maps directly to Nudge's internal model. It gives you explicit control over node types, dimensions, and initial ELKjs layout parameters.
title: "My System"
layoutOptions:
"elk.algorithm": "layered"
"elk.direction": "DOWN"
"elk.spacing.nodeNode": "80"
"elk.layered.spacing.nodeNodeBetweenLayers": "110"
nodes:
- id: user
label: "Customer"
type: "person" # person | container | database | message_bus | external
description: "End user of the system"
- id: api
label: "API Gateway"
type: "container"
tech: "Node.js"
description: "Routes requests to downstream services"
- id: db
label: "Postgres"
type: "database"
description: "Stores all persistent data"
edges:
- from: user
to: api
label: "Sends requests [HTTPS]"
- from: api
to: db
label: "Reads & writes"See examples/ for full working examples including container diagrams with boundary blocks.
Diagrams can carry free-text annotation notes β folded-corner boxes anchored to an element and rendered near it. Notes are for commentary (ownership, status, caveats); they are not Architecture Elements and never participate in relationships.
Mermaid (adopts the sequence-diagram convention):
Note right of shop: Handles the full<br/>checkout journey
Note left of payments: Legacy provider,<br/>being retired
Note over inventory: Owned by Team Phoenix
Note over user,shop: Primary purchase journey
PlantUML (note the space before the colon; top of maps to over):
note right of shop : Handles the full<br/>checkout journey
note left of payments : Legacy provider,<br/>being retired
note top of inventory : Owned by Team Phoenix
- Anchoring β a note attaches to one element via a directional hint (
right,left, orover). The two-anchor span form (Note over X,Y) isoveronly and centres over both anchors. overplaces the note above the anchor, never covering it β a deliberate divergence from Mermaid, whereovervisually covers the element. Covering the anchor would defeat the purpose of an annotation.- Auto-placement β the author's hinted side is honoured first; if it occludes a neighbour or a connection line, Nudge tries the other sides in preference order and moves the note only if that improves the overall geometry. A note with no clean placement is left at its hint and reported via a
warningsentry. - Unknown anchor id β the note is skipped with a
warningsentry (fail-safe), never a hard error. - Multi-line text uses
<br/>.
Floating (unanchored) notes β a diagram-wide caption pinned to a canvas corner rather than an element. Use a corner (top-left, top-right, bottom-left, bottom-right) in place of ... of <element>, with no anchor id:
Note bottom-right: Integration predominantly via MuleSoft ESB (EMS pub/sub).
note top-left : Data classification β Internal # PlantUML parity
Floating notes sit in the margin outside the node area, so they never occlude an element and are exempt from the auto-placement search. Both hyphen (bottom-right) and space (bottom right) forms are accepted. See docs/c4-floating-note-implementation-plan.md for design notes.
Working fixtures: examples/system_context_with_notes.mermaid and examples/system_context_with_notes.puml.
Outputs are written to .nudge/:
iteration_N.pngβ screenshot at each flat-diagram critic-loop passstep_0_initial.png,step_1_label_hints.pngβ staged container visual-hint snapshots; in deterministic mode these are inspection snapshots without LLM callsoptimized.pngβ final layout as PNGoptimized.svgβ final layout as a self-contained SVG with embedded stylesoptimized.light.png/optimized.light.svgβ the second palette, when--theme bothis passedlayout.cache.jsonβ final ELKjs parameter patch (flat diagrams)visual_hints.jsonβ raw accepted/reviewed visual-hint payloads for context/container diagrams when LLM calls run
Run the test suite:
npm test # Run the full test suite (unit + integration + visual)
npm run test:unit # Run fast unit tests (parser, geometry math)
npm run test:integration # Run CLI and MCP integration tests
npm run test:visual # Run visual Playwright-driven rendering tests
npm run test:refactor # Run layout regression parity testsnpm test runs fast, Playwright-free unit tests first, followed by CLI and MCP server integration tests, and finally renders every .mermaid file in test/fixtures/diagrams/core/ using Playwright, analyzing geometry, verifying boundary containment, writing PNG/SVG snapshots and test_outputs/test_results.md, and grading with the built-in math scorer. NUDGE_VISUAL_TEST=true npm run test:visual enables the optional LLM visual grader. In either mode, if the grader cannot reach a local LLM it falls back to math scoring automatically.
Nudge exposes an optimize_diagram tool over stdio, compatible with Claude Desktop, Claude Code, and any MCP client.
By default, the MCP server runs deterministic rendering. Pass the parameter enhance: true to enable optional LLM-driven optimization.
| Parameter | Type | Required | Description |
|---|---|---|---|
content |
string | yes | Mermaid C4Context/C4Container, C4-PlantUML, or YAML diagram source |
format |
"mermaid" | "plantuml" | "yaml" |
no | Auto-detected from content if omitted |
enhance |
boolean | no | Enable optional LLM optimization / visual-hint enhancement pipeline (default: false) |
Returns: a JSON summary (success, iterations, finalCollisions) followed by a self-contained SVG string. A best-effort SVG is always returned even when collisions remain after 4 iterations.
Add the following to your claude_desktop_config.json (on macOS: ~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"nudge": {
"command": "node",
"args": ["/absolute/path/to/nudge/src/mcp/index.js"]
}
}
}Quit and relaunch Claude Desktop after editing the config. Verify the server loaded by checking:
tail -20 ~/Library/Logs/Claude/mcp-server-nudge.logYou have access to the nudge MCP server with an optimize_diagram tool.
I want a C4 Container diagram for the following system:
[describe your system β services, databases, external users, integrations]
Steps:
1. Write the diagram as Mermaid C4Container syntax
2. Call optimize_diagram with that content
3. Return the SVG and summarise the result
Or to optimize an existing diagram directly:
Call the nudge optimize_diagram tool with this diagram and return the SVG:
[paste Mermaid or YAML here]
βββ .nudge/ # Output directory for rendered iterations and final exports
βββ docs/ # Documentation and example images
βββ examples/ # Example C4 model YAML and Mermaid diagrams
βββ scripts/ # Developer utilities for iterating on SVG symbol designs
βββ src/
β βββ core/
β β βββ optimizer.js # Shared optimization loop β called by both CLI and MCP
β β βββ geometry.js # Pure geometric algorithms: overlap, intersection, label placement
β β βββ llm_client.js # Stateless LLM API client β visual hints and optimization calls
β βββ cli/
β β βββ index.js # CLI entry point β argument parsing, file I/O, console output
β βββ mcp/
β β βββ index.js # MCP stdio server β registers and handles optimize_diagram tool
β βββ mermaid_parser.js # Mermaid C4 syntax β internal JSON model
β βββ render.html # HTML shell loaded by Playwright β sources render_engine.js and renderer/
β βββ render_engine.js # Browser-side facade orchestration
β βββ renderer/ # Browser-side layout and rendering modules
β β βββ container/ # Kahn layering and container placement
β β βββ elk/ # ELKjs integration for flat diagrams
β β βββ labels/ # Collision-aware label placement
β β βββ routing/ # Default A* grid router & legacy candidate router
β β βββ svg/ # Shape rendering & SVG drawing
β βββ utils.js # fetchWithTimeout with cancellation signal support
βββ test/ # Fast unit tests, integration tests, visual tests, and fixtures
β βββ unit/ # Playwright-free unit tests (parser, geometry math)
β βββ integration/ # CLI and MCP integration tests
β βββ visual/ # Browser-based Playwright layout tests
β βββ fixtures/ # Centralized diagram and image fixtures
βββ test_outputs/ # Rendered PNGs, SVGs, and test result summary from npm test (git-ignored)
βββ LICENSE
βββ package.json
Contributions are welcome. See CONTRIBUTING.md for setup instructions, code structure guidance, and PR guidelines.
This project is licensed under the MIT License.


