Skip to content

Repository files navigation

Nudge

Tests License: MIT Node >=18

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.


Example Output

Before (Mermaid C4 Baseline) After (Nudge Optimized)
Core banking Mermaid baseline Core banking Nudge optimized layout

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:

Search service container Nudge optimized layout


How It Works

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
Loading
  1. Ingestion: Parses C4 Context or C4 Container diagrams from .mermaid, .puml/.plantuml, or .yaml specifications. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Export: Writes a best-effort SVG and PNG even if geometric issues remain.

Scope: one boundary per diagram

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.


Layout Engines & Algorithms

Nudge uses two distinct engines depending on the diagram shape:

1. Flat Layout (ELKjs)

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.

2. Context & Container Layout (Custom Engine)

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=legacy and 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.

Optional Visual-Hint Pipeline (Context & container diagrams)

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 by getLLMLabelPlacementHints.

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.


Features

  • 🎯 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_diagram tool 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/.mmd syntax, .puml/.plantuml C4-PlantUML syntax, and structured .yaml specifications.
  • πŸ“ Standardized Sizing & Grid: All nodes are standardized to a width of 200px. Heights: 200px for Person, 140px for Container/Database/External, 80px for 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.

Prerequisites

Before using Nudge, make sure you have:

  1. Node.js: Version 18 or newer.
  2. Playwright Browsers: Required for rendering diagrams.
    npx playwright install chromium
  3. 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-12b or other reasoning models.

Installation

git clone https://github.com/cookie-bytes/nudge.git
cd nudge
npm install

Usage

CLI

Deterministic layouts run by default without requiring an LLM server or network connection:

node src/cli/index.js path/to/diagram.mermaid

To 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 --enhance

Themes

Exported 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.

Configuration & Environment Variables

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.mermaid

Run 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.yaml

YAML Input Format

YAML 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.

Annotation Notes

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, or over). The two-anchor span form (Note over X,Y) is over only and centres over both anchors.
  • over places the note above the anchor, never covering it β€” a deliberate divergence from Mermaid, where over visually 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 warnings entry.
  • Unknown anchor id β€” the note is skipped with a warnings entry (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 pass
  • step_0_initial.png, step_1_label_hints.png β€” staged container visual-hint snapshots; in deterministic mode these are inspection snapshots without LLM calls
  • optimized.png β€” final layout as PNG
  • optimized.svg β€” final layout as a self-contained SVG with embedded styles
  • optimized.light.png / optimized.light.svg β€” the second palette, when --theme both is passed
  • layout.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 tests

npm 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.


MCP Server

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.

Tool: optimize_diagram

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.

Connecting to Claude Desktop

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.log

Example prompt for Claude Desktop

You 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]

Folder Structure

β”œβ”€β”€ .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

Contributing

Contributions are welcome. See CONTRIBUTING.md for setup instructions, code structure guidance, and PR guidelines.


License

This project is licensed under the MIT License.

About

C4 Context & Container Code Diagram Tool

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages