26. April 2026
Most of the tools I reach for when documenting architecture were built in another era. PlantUML wants a JVM. Asciidoctor wants Ruby. Structurizr wants both, depending on which piece you’re using. Each one is a perfectly fine tool. Together they’re a small operations problem – the kind of thing that quietly breaks every CI pipeline I’ve ever set up, and the kind of thing AI coding agents have an especially hard time bootstrapping inside a sandbox.
Over the last few weeks I’ve been building a Rust-native replacement for the parts of that stack I actually use. Three projects, all on GitHub, all single static binaries, all designed to talk to each other and to AI agents:
This post is a tour of what’s there, why I built it the way I did, and where the three pieces meet.
forge started as a question about Structurizr DSL. I liked the idea of one model, many views – describing a system once and rendering different perspectives from it – but I wanted more view types than the C4 set, and I wanted the toolchain to be a single binary that an AI agent could install with brew install and then use without a JVM in sight.
A .forge file describes a system as code:
forge "My Platform" {
model {
user = person "User" { description "End user" }
api = system "API" {
web = container "Web API" { technology "Node.js / Express" }
db = container "Database" { technology "PostgreSQL"; tags "database" }
web -> db "reads/writes" "SQL"
}
user -> api.web "uses" "HTTPS"
}
views {
systemContext api "Context" { include *; autoLayout lr }
container api "Containers" { include *; autoLayout tb }
}
}
From that single source, forge build produces SVG diagrams, forge generate builds a static documentation website, and forge check lints the model against eight built-in architecture rules plus any custom rules you put in .forge-rules. There are 13 view types – the usual C4 system context, container, and component diagrams, plus pipeline, deployment, tech stack, branching strategy, data model, trust boundaries, team ownership, API catalog, event flows, and an animated walkthrough mode for presentations.
A few of the design decisions worth calling out:
forge can render the difference between two versions of a model, which turns architecture review into something closer to a code review.forge import reads PlantUML C4 and Mermaid sources. forge analyze scans a codebase and produces a starting .forge file, including PlantUML and Mermaid diagrams it discovers in the repo. You don’t have to abandon what you’ve drawn before.forge mcp exposes the toolchain to AI coding agents over the Model Context Protocol. An agent can read a .forge model, lint it, and propose edits the same way it would propose source code changes – without needing to learn 13 different SVG renderers.puml is the smaller of the three, and the one with the clearest goal: read a .puml file, write SVG, no Java. I keep accumulating PlantUML diagrams in old projects, and I want to be able to render them on a fresh laptop without apt install default-jre first.
puml examples/sequence.puml -o out.svg
puml examples/class.puml -o out.png # PNG, 2x scale
puml examples/class.puml -o out.svg --watch # re-render on save
The compatibility goals are deliberately modest. Same .puml source files work unchanged. Output is semantically equivalent SVG, not pixel-identical – the layout engine is mine and the rendering style is consistent across diagram types rather than reproducing PlantUML’s per-diagram quirks. So far that has covered every diagram I actually use day-to-day: sequence, class, activity, state, component, deployment.
A couple of pieces I’m happier with than I expected to be:
puml init-genai drops a consistent set of “how to author .puml in this project” templates into the locations Codex, Copilot, and Claude Code read. AI agents tend to invent PlantUML syntax when they don’t know what’s idiomatic; this gives them a shared starting point.adoc is the most opinionated of the three. It’s an AsciiDoc processor that takes the AsciiDoc Language specification – not the Asciidoctor implementation – as the source of truth, and documents every place it diverges.
That distinction matters because Asciidoctor is the AsciiDoc implementation, and the language has historically been defined by Asciidoctor’s behaviour. The new spec is changing that, and there’s a useful niche for a tool that follows the spec strictly, points at the spec when something looks wrong, and emits diagnostics with byte-precise source spans.
adoc input.adoc # emit input.html
adoc -o out.html input.adoc # explicit output path
adoc -a toc -a sectnums input.adoc # set document attributes
adoc input.adoc > input.html # render to stdout
A few features that came out of using it on real documents:
serde. The eventual plan is a Unix-shaped extension model: adoc --emit-ast doc.adoc | my-filter | adoc --from-ast --to html5. You write filters in any language as long as they speak the AST schema.--diagnostic-format=json mode for editor integrations.<<id>> inside backticks and getting a literal string instead of a working xref.adoc init-genai, like its puml cousin, drops author-guidance templates for AI tools so they stop reaching for Markdown idioms inside an AsciiDoc document.The HTML5 backend mirrors Asciidoctor’s attribute-driven stylesheet model – :linkcss:, :stylesheet: x.css, :stylesheet!:, :copycss: all behave the way you’d expect – and the built-in stylesheet is light/dark aware out of the box.
These projects look like three separate tools, but they’re really one toolchain split across three binaries because the boundaries are useful:
forge analyze walks a repository looking for .puml, .plantuml, .iuml, .mmd, and .mermaid files plus mermaid-fenced blocks inside markdown, runs them through the import parser, and merges the resulting elements and relationships into the model. Each source file gets a slug-prefixed namespace so two diagrams that both define customer don’t collide. The end state is a single .forge model that has absorbed everything the team has already drawn, ready to be linted, rendered, and published.
The published documentation site that forge generate produces is plain HTML with embedded SVG. There’s no JavaScript framework, no build step beyond the forge binary itself, and the colors track prefers-color-scheme so the site looks at home on a dark-themed OS without any toggle to flip.
When I started writing software, “no runtime dependencies” was a nice-to-have – the kind of thing you mentioned in a README to signal good taste. Now it’s load-bearing.
Half the reason is that AI coding agents are a primary user. An agent installing adoc inside a sandboxed container can brew install grahambrooks/adoc/adoc and be rendering documents in seconds. The same agent installing Asciidoctor needs Ruby, needs gems, needs to figure out which gem to ask for, and burns context window on the bootstrap. The friction is not just developer ergonomics – it’s whether the agent can do the task at all before its context fills up.
The other half is that I’ve grown tired of CI pipelines that break because someone bumped a JVM version or a Ruby gem dropped support for the version we were pinned to. A static binary built from a calver-tagged release (2026.4.26, in this case) is a thing that either works or doesn’t, and tomorrow’s CI run will not surprise you.
The three projects are usable today – I’m using them on real documents – but each has obvious gaps:
include::, ifdef/ifeval) or block-attribute lines ([source,rust], admonitions). The block parser, inline parser, and HTML5 converter cover the core language; the DESIGN.md status matrix is honest about the rest.If any of this is useful to you – as a user, a contributor, or just someone with opinions about how architecture documentation should work – the repositories are linked above and issues are open.