The diagram is the code.
An architecture model for solution architects that lives in your repository as C# source: edit the canvas, and Roslyn rewrites the file. Or skip the canvas entirely — the same model is three text files an agent can write, and one command renders the picture with no browser open and no mouse.
- Storage
- Your Git working treeNo runtime database, at any layer
- Engine
- Roslyn DocumentEditorTargeted rewrites, never string rebuilds
- Guarantee
- Round-trip fidelityA UI edit diffs as only that edit
- Headless
- stemma renderModel in, image out. No browser, no mouse
- Narrative
- Story booksThe walkthrough that lands it, kept with the model
Your model is a file you can read.
No project format, no XML, no notation to learn. A model is C# records in Architecture/Architecture.cs, and anyone on the team can open it in a text editor and follow it.
// Bounded contextsvar ctxOrdering = new BoundedContext("ctx_ordering", "Ordering");var ctxPayments = new BoundedContext("ctx_payments", "Payments");var ctxFulfilment = new BoundedContext("ctx_fulfilment", "Fulfilment"); // Modules, each declaring the context it belongs tovar modCart = new Module("mod_cart", "Cart", "ctx_ordering");var modOrder = new Module("mod_order", "Order Lifecycle", "ctx_ordering");var modAuth = new Module("mod_auth", "Authorisation", "ctx_payments");var modLedger = new Module("mod_ledger", "Ledger", "ctx_payments"); // A relationship carries what moves, not just that// something movesvar flowOrdered = new DataFlow("flow_ordered", "mod_order", "mod_auth", "OrderPlaced");var depSettleAuth = new Dependency("dep_settle_auth", "mod_settlement", "mod_auth", "uses"); // Where a thing is in its life, and who answers for itvar tagLedger = Tag.For(modLedger, lifecycle: new Lifecycle(Status: "to-be-created", Phase: "Q4"), ownership: new Ownership(Squad: "Money", Domain: "Payments")); // The parts of a design that have no boxvar riskDualWrite = new Risk("risk_dualwrite", "Order and Ledger are written in two transactions", "mod_ledger");var qRefund = new Question("q_refund", "Who owns a partial refund after delivery?", "mod_ledger");
Contexts are declared, not drawn.
A bounded context is a record with an id and a name. On the canvas it renders as a lane that other elements nest inside — but the nesting is a real reference in the file, not a position on a grid. Move a module between lanes and one string argument changes.
Ids are stable; names are not.
Every element carries an id that never changes and a display name that can. Flows, dependencies, tags and views all reference the id, so renaming a concept as your understanding improves costs nothing and breaks nothing.
An arrow that says what it carries.
A DataFlow names its payload and its direction; a Dependency names its kind — uses, calls, reads, publishes, consumes. A link renders in a view only when both of its endpoints are present, so a filtered view never shows an arrow into nothing.
Target state lives on the model, not in a deck.
Lifecycle marks a thing current, target, to-adapt, to-be-created or deprecated, with a phase and validity dates. Ownership attaches a squad, a domain and RAPID roles. Both go on elements and on relationships, which is where migration plans usually fall apart.
Risks and questions are first-class.
A risk, an open question or a standing assumption is a model element anchored to what it concerns. It renders as a dotted edge to its subject, versions with it, and turns up on the Concerns board — instead of in a wiki page nobody opens.
One rename. Three lines. Nothing else moved.
Double-click the Ledger box on the canvas, type a longer name, press Enter. This is what git diff reported — all three hunks, with the file header removed and two over-long lines cut at the right margin.
@@ -39,7 +39,7 @@ public static class Architecture
var modAuth = new Module("mod_auth", "Authorisation", "ctx_payments");
var modSettlement = new Module("mod_settlement", "Settlement", "ctx_payments");
- var modLedger = new Module("mod_ledger", "Ledger", "ctx_payments");
+ var modLedgerReconciliation = new Module("mod_ledger", "Ledger & Reconciliation", "ctx_payments");
var modAllocation = new Module("mod_allocation", "Stock Allocation", "ctx_fulfilment");
var modShipping = new Module("mod_shipping", "Shipping", "ctx_fulfilment");
@@ -86,7 +86,7 @@ public static class Architecture
var tagSettlement = Tag.For(modSettlement, lifecycle: new Lifecycle(Status: "target", …
- var tagLedger = Tag.For(modLedger, lifecycle: new Lifecycle(Status: "to-be-created", …
+ var tagLedger = Tag.For(modLedgerReconciliation, lifecycle: new Lifecycle(Status: "to-be-created", …
var tagAllocation = Tag.For(modAllocation, lifecycle: new Lifecycle(Status: "current"), …
@@ -98,7 +98,7 @@ public static class Architecture
ctxOrdering, ctxPayments, ctxFulfilment,
modCart, modPricing, modOrder,
- modAuth, modSettlement, modLedger,
+ modAuth, modSettlement, modLedgerReconciliation,
modAllocation, modShipping,
- The declarationThe display name changed. The id did not, so every reference elsewhere still resolves.
- The local variable, twiceRoslyn renamed the symbol, so the tag and the aggregate list followed it. A find-and-replace would have missed one and still compiled.
- And nothing elseNo reordering, no reformatting, no blank line moved, no comment lost. The reviewer sees a rename, because that is what happened.
This is the contract the rest of the tool rests on. A suite of minimal, realistic and deliberately pathological fixtures runs every operation and asserts the diff byte for byte, and the codebase has one standing instruction about it: when the fidelity suite fails, the implementation is wrong, not the test.
Everything you are allowed to say.
Deliberately richer than a class diagram, deliberately poorer than an IDE. This is the whole language — there is no hidden second half.
Elements
| Record | Carries |
|---|---|
| Person | An actor, with a role — external, internal, user |
| SoftwareSystem | A system, usually a neighbouring one you don't own |
| Container | A deployable unit inside a system, with a kind — service, worker, cronjob, db |
| BoundedContext | A DDD context; renders as a lane other elements nest inside |
| Module | A unit of cohesion, optionally inside a context |
| Capability | A business capability, optionally inside a context |
| UseCase | A user-visible use case |
| Risk | A design risk, anchored to what it concerns |
| Question | An open design question, anchored the same way |
| Assumption | A standing assumption everything downstream rests on |
| Concept | A named box that asserts nothing about a system — for drawing an argument rather than an architecture |
Relationships, metadata, views
| Record | Carries |
|---|---|
| DataFlow | From, to, a payload name shown as the edge label, and a direction |
| Dependency | From, to, and a kind — uses, calls, reads, publishes, consumes |
| Relation | From, to, and a free label — an arrow that claims nothing beyond what it says |
| Lifecycle | Status (current, target, to-adapt, to-be-created, deprecated, or your own word), phase, valid-from, valid-until |
| Ownership | Squad, domain, and RAPID role lists — recommend, agree, perform, input, decide |
| Tag.For | Attaches a lifecycle and/or an ownership to any element or any link |
| View | A named subset with a base lens — module map, dependency graph, or all — stored as its own .cs file |
Nobody has to open it.
Everything above happens with a mouse in it. But a workspace is three text files in a vocabulary that is written down — so the canvas is an authoring surface, not the only one. An agent with a shell writes the model; one command turns it into a picture.
// Architecture/Architecture.csvar humanDiff = new Concept("con_human_diff", "a colleague's diff");var ask = new Concept("con_ask", "ask them why");var answered = new Concept("con_answered", "answered in a sentence");var agentDiff = new Concept("con_agent_diff", "an agent's diff");var nobody = new Concept("con_nobody", "nobody to ask");var artifacts = new Concept("con_artifacts", "tests, contracts, decision records");// Views/WhoDoYouAsk.cspublic static View Define() => new( Id: "view_ask", Name: "Who do you ask", BaseView: "all", ElementIds: new[] { "con_human_diff", "con_ask", "con_answered", "con_agent_diff", "con_nobody", "con_artifacts" });// stemma.layout.json"custom:view_ask": { "layout": { "kind": "grid", "columns": [["con_human_diff", "con_agent_diff"], ["con_ask", "con_nobody"], ["con_answered", "con_artifacts"]], "gap": [60, 40], "align": "center" }, "shapes": [ { "id": "sh_human", "kind": "label", "x": -140, "y": 10, "text": "human", "color": "#767884" } ]}
A box that asserts nothing.
Every other element kind is a claim — this is a bounded context, this is deployable. An argument's boxes claim nothing, and forcing one into a Module writes a falsehood into a file other people will copy from. Concept and Relation are the way out: a named box and a labelled arrow that mean only what they say.
The view is the question.
A view names the subset this particular picture is about. A link is drawn only when both of its endpoints are in it — the single most common reason a diagram comes out looking right and missing an arrow. So the validator reports that case by name, rather than leaving it to be noticed.
Say what the arrangement is, not where things are.
A language model is good at intent and poor at pixels, so it states the intent — three columns, sixty apart, centred — and Stemma solves it to coordinates. Nudging one box by hand records an offset against the constraint instead of replacing it, so the arrangement survives being corrected and the nudge travels with its slot when the grid re-solves. row, column and stack work the same way.
Ink is not architecture.
Swimlane labels, brackets, callouts — anything carrying no architectural meaning — are free-form shapes in the sidecar, never elements. Worth stating plainly, because it is the rule an agent breaks first: if you want to add an element so that a picture looks better, what you want is a shape.
$ stemma validate --workspace .
info model/is-a-figure 6 of 6 elements are Concepts — this is
a figure rather than an architecture,
which is a fine thing to be
stemma validate: 0 error(s), 0 warning(s)
$ stemma render --workspace . --view view_ask \
--out assets/img/posts/the-bottleneck-moved/who-do-you-ask.png
who-do-you-ask.png 1180x512 6 nodes
stemma render: wrote 1 file
# a fortnight later, after the model moved and the image did not
$ stemma render --check --workspace . --view view_ask \
--out assets/img/posts/the-bottleneck-moved/who-do-you-ask.png
stemma render --check: who-do-you-ask.png is out of date
elements 6 -> 7, links 5 -> 6, view members 6 -> 7
# non-zero, so the build stops rather than shipping a stale picture
- DeterministicSame input, same picture. Each render starts a short-lived backend of its own, so two can run at once and neither disturbs a workspace you have open.
- CheckableEvery render writes a manifest fingerprinting the model, the view, the layout and the profile it came from. --check re-computes it and fails the build when a committed image has stopped matching its source.
- Written downThe sidecar has a published JSON Schema and every file written points at it. The protocol reference explains the whole chain to somebody who will never open the app, with three worked examples that were built and rendered exactly as printed.
The workspace above draws a diagram that already exists on this site — the one in The Bottleneck Moved, originally made in draw.io. Reproducing it from a model, with one command and no mouse, transparent and identical across two machines, was the acceptance test for this whole pipeline. Its origin is less flattering: a later essay's diagrams were drawn from a Stemma model, by a 190-line Playwright script that started the app, opened a real browser and dragged every node into position with synthetic mouse gestures — three passes, re-reading an anchor node before each one, because dragging was the only thing that stuck. It worked, and it was a bug report against the product. stemma render is what deleted it.
One model. Whatever lens the room needs.
A view is a projection, not a copy — a named subset of the same model with its own layout, styling and notes. Switching one changes the lens, never the data.
An architecture nobody followed is an architecture that didn't happen.
Modelling the system is half the job. The other half is forty minutes in front of people who control the budget — and that walkthrough is normally a slide deck, rebuilt by hand every quarter, drifting from the model the moment either one moves.
books: - id: book_steering name: Steering review — extracting Payments audience: leadership pages: - viewId: moduleMap title: Where the money moves today narrative: Three contexts. Ordering and Fulfilment are already out of the monolith. Payments is the one still half in — Settlement is target state for Q3, Ledger does not exist yet… - viewId: dependencyGraph title: The dependency that sets the order of work narrative: Order Lifecycle still reads the legacy monolith. That arrow is why Payments cannot go first…
A view, plus the thing you would actually say.
Each page points at a view of the model and carries a title and a narrative. Not a caption — the sentence you say out loud when that picture is on the screen, written down so it survives you being on holiday.
One model, one deck per room.
A book is tagged for who it is for — leadership, engineering, security, operations. The committee that funds the work and the team that builds it get different sequences, different depth and different language, off the same model, with no second copy to keep honest.
The canvas follows the story.
Open a book and the tool becomes the deck: the canvas switches to each page's view, the narrative sits in a strip beneath it, and prev/next walks the room through it. Drag to reorder pages, edit the narrative in place, export the whole thing as a multi-page PDF for the people who want a document.
The deck cannot drift either.
A book stores the reference to a view, never a picture of it. Change the architecture and next quarter's walkthrough redraws itself from the current model — so the story you told the board and the code you shipped stay the same artifact. That is the part no diagramming tool can offer, because it has no model to point at.
Four layers, one direction, no second store.
The engine is pure Roslyn. It knows nothing about the web layer and nothing about any model provider, which is what keeps the fidelity guarantee testable in isolation.
Not a better canvas.
Three honest categories of tool already exist, and each is good at what it set out to do. This is what changes when the model has nowhere to drift to.
| Stemma | Canvas drawing tools | Model-as-language tools | Code analysers | |
|---|---|---|---|---|
| Source of truth | Your source files | A separate document | A separate language beside the code | Your source files, read-only |
| Redesign in the picture | Yes — the code follows | Yes, and nothing follows | Yes, and the code still doesn't follow | No |
| Drift | Structurally impossible | Immediate and permanent | Slower, but there are still two artifacts | None, at the price of not designing |
| Reviewed in a pull request | As a diff of the architecture | As an image somebody re-exported | As a diff of the description | Not applicable |
| Risk, ownership, questions | First-class, anchored, versioned | A sticky note, if you're lucky | Limited | Absent by construction |
| Telling the story | Narrated books that travel with the model | A slide deck, rebuilt by hand each time | Not addressed | Not addressed |
| Where a coding agent meets it | In the repository it is already editing | Nowhere near it | A file beside the code, easy to ignore | After the fact, as a report |
For a team the practical differences are narrow and concrete: an architecture review stops opening with twenty minutes of establishing whether the picture is current; a module reaching across a context boundary shows up in the diff a reviewer already reads; and a new architect has one file and one canvas to read instead of a wiki space to excavate. There is nothing to procure — it runs on a laptop, against a repository you already own, and nothing leaves it.
Technical specifications.
- Engine
- .NET 10 · Roslyn. Loads a repository through
MSBuildWorkspace, or a model-only folder through anAdhocWorkspace. All edits areDocumentEditorrewrites. - Host
- ASP.NET Core 10 — REST for workspace, layout, views and violations; SignalR for the operation stream and delta broadcast.
- Client
- React 19 · Vite ·
@xyflow/react· zustand · Tailwind. - Requirements
- A .NET 10 runtime. A model-only workspace opens with no SDK installed, no restore and no NuGet — the metadata references come from the assemblies the process is already running on.
- Model storage
Architecture/*.csandViews/*.csin your Git working tree.- Presentation
stemma.layout.json, committed beside the model: positions, node and edge styling, routing, notes, custom properties, free-form shapes, per-view layout choice.- Runtime database
- None. The model lives in memory while a workspace is open; permanent storage is the files.
- Operations
- Add, rename and remove elements; re-parent; set attributes; add and remove links; set link attributes; set lifecycle; set ownership; restore — each with its own round-trip fixtures.
- Views
- Module map, dependency graph and concerns board built in; any number of saved custom views, each stored as code.
- Layout
- Architectural (by type), hierarchical, force-directed and manual — tunable per view, remembered per view. A manual move flips that view to custom. Or a declarative constraint —
grid,row,column,stack— solved to coordinates and kept live, with a later drag recorded as an offset against it. - Canvas
- Pan, zoom (0.05×–2.5×), minimap, snap to 20px grid, multi-select, align and distribute, undo and redo, six connection dots per node, draw.io-style edge docking, four routing modes, both-end markers.
- Headless
stemma render— a workspace and a view become an image with no browser open and no mouse; profile, palette, theme, format, scale, padding and transparency as flags; a short-lived backend per render, so renders are isolated and can run in parallel.stemma validate— dangling endpoints, missing view members, half-drawn links, duplicate ids, busy views, with--jsonfor a caller acting on the answer.- Provenance
- Every render writes a sibling
.manifest.jsonfingerprinting the model, the view, the layout and the profile.stemma render --checkre-computes it and exits non-zero when a committed image no longer matches its source. - Authoring as text
- A published JSON Schema for
stemma.layout.json, pointed at from every file written, plus a protocol reference covering the whole chain — vocabulary, view, sidecar, render command — with three worked examples. Written for somebody who will never open the app. - Export
- PNG · true vector SVG from the publication profile · draw.io (honouring the view and the layout) · Mermaid · multi-page PDF view-books · a single-file interactive HTML report that opens offline with no network requests.
- Report
- Audience modes (builder, stakeholder, reviewer), per-view layer toggles, element search, and element-anchored comments that export as a pack and import back into the workspace.
- Story books
- Ordered, audience-tagged sequences of pages; each page references a view and carries a title and a narrative. Present mode drives the canvas from the page, with prev/next, drag-to-reorder, in-place narrative editing and multi-page PDF export. Stored as
Concepts/view-book.stemma.yamlbeside the model. - Validation
- Dangling references and architecture-rule violations surfaced inline on the canvas and in the status bar, and reported on the command line by
stemma validate. - Diagnostics
- A leveled, structured record of what the engine did — JSON lines under
~/.stemma/logs/, capped at 20 MB and 14 days without being asked, level changeable while running. Shareable bundles are redacted by default. There is no network sink and none can be configured: a log record contains your architecture. - Collaboration
- Git. Branch, tag, blame, merge, revert, pull request. Live sync over SignalR for clients open on the same workspace; external file edits reconcile without clobbering in-flight work.
- Fidelity
- Every operation carries minimal, realistic and pathological round-trip fixtures. A failing fixture blocks the change, not the other way round.
- Samples
samples/AuroraRail— a fictional rail-ticketing platform: 66 elements, four saved views, three narrative books, exercising every part of the DSL.samples/StemmaArchitecture— Stemma modelling itself.- Licence
- PolyForm Noncommercial 1.0.0 — source-available, not open source. Free for any noncommercial purpose: personal use, study, hobby projects, and use by charities, educational institutions, public research bodies and government. Commercial use of any kind needs a commercial licence.
It is still being built.
The three most recent releases. A project page can claim anything about a side project; dated releases are the only version of that claim you can check.
-
The headless half stemma render and stemma validate, a publication profile, layout as a declarative constraint, a render manifest with --check, true vector SVG, the Concept and Relation kinds, a published sidecar schema and the protocol reference — plus local-only diagnostics.
-
In the open The repository, PolyForm Noncommercial 1.0.0, and two workspaces worth reading first: Aurora Rail, and Stemma's own architecture modelled in Stemma.
-
Stemma, and a workspace with no solution in it The rename from Verso, and model-only workspaces: a folder holding Architecture/ and nothing else opens with no MSBuild, no SDK, no restore and no NuGet.
Horizons, not dates.
A side project built on evenings and weekends since 4 May 2026. Promising quarters would be a lie with a nice font, so each item carries an honest position instead. The words are Stemma's own lifecycle vocabulary — the same three it puts on your systems.
Now current
- The Roslyn engine and the fidelity suite that gates it
- The canonical model — elements, links, lifecycle, ownership, views as code, and the kinds that assert nothing
- The canvas — module map, dependency graph, saved views, layout, styling, shapes, notes
- The committed sidecar, with a published schema and a protocol reference for writing it by hand
- The headless CLI — render and validate, the publication profile, layout as a constraint, the render manifest
- Story books — narrated, audience-tagged page sequences, with present mode and PDF export
- The single-file HTML report, with audience modes and a comment loop
- Model-only workspaces and the from-scratch onboarding path
- Local-only diagnostics — structured logs, redacted bundles, no network sink
Next target
- Fidelity fixtures across the whole operation catalogue — every operation, no exception
- A desktop window, so it stops reading as a dev server
- One-command install as a global tool
- A workspace brief — a STEMMA.md dropped into every new workspace, so the agent that opens it needs no other document
- An embedded font in the vector export, so an SVG cannot re-wrap on a machine that lacks the one it was drawn in
- Auto-layout from the command line — the four algorithms are pure functions already, and reachable only from the canvas
- Clearer view management — faster switching, better handling
- An accessibility pass — keyboard, focus, contrast, reduced motion
- A curated sample gallery as living documentation
Later to‑be‑created
- More projections — Mermaid and PlantUML output, a live code-preview pane
- Git-backed sessions — a session is a branch, a save is a commit
- Deeper validation — background compilation, richer rules inline
- Canvas search and filter, and deep-linkable views
- A repository integration that regenerates the report on merge and comments the architecture change on the pull request
- Hosted report links — the one part that could ever be a service
What people ask first.
Nine objections, answered where you can find them rather than three emails in.
Does the diagram stay in sync with the code?
It cannot drift, because it is the same artefact. The model is C# in Architecture/*.cs inside your repository, and a canvas edit is a Roslyn DocumentEditor rewrite of that file. There is no export step to forget and no second copy to reconcile.
Does my whole system have to be written in C#?
No. C# is the notation the model is written in, not a constraint on what you model. The workspace can be a model-only folder with no solution in it, and the systems it describes can be written in anything. A .NET runtime is the only requirement, and a model-only workspace opens with no SDK, no restore and no NuGet.
Where is the data stored?
In your Git working tree, and nowhere else. There is no runtime database at any layer. The model is C# source; presentation — positions, styling, routing, notes — is a stemma.layout.json sidecar committed beside it. While a workspace is open the model lives in memory; permanent storage is the files.
How is this different from draw.io, Lucidchart or an EA tool?
Drawing tools produce a picture that is true on the day it is saved. Reverse-engineering tools read code but cannot edit it. Stemma closes the loop in both directions and guarantees round-trip fidelity, so a UI edit arrives as a diff containing only that edit — which is what makes the loop trustworthy enough to use.
What does a change look like in review?
A normal Git diff. Renaming one element on the canvas rewrites the declaration and every reference Roslyn can see, and touches nothing else: no reordering, no reformatting, no lost comment. Reviewers read it as the change it was.
How do stakeholders see the model without installing anything?
Two ways. A story book is an ordered, audience-tagged sequence of pages that drives the canvas as you present, stored beside the model so it cannot go stale either. Or export a single self-contained HTML report — one file, opens offline with no server, no login and no seat, with audience modes and element-anchored comments that import back.
Can an agent use this without opening the canvas?
That is the whole second half of the tool. A workspace is three text files — Architecture/*.cs, Views/*.cs and stemma.layout.json — and all three are documented for a writer who will never see the app. An agent authors them, runs stemma validate to catch the string ids nothing else checks, and runs stemma render to produce the image with no browser open and no mouse. Arrangement is expressed as a constraint (grid, row, column, stack) rather than as coordinates, because a model is good at intent and bad at pixels.
What stops a published diagram from going stale?
Every render writes a sibling manifest fingerprinting the model, the view, the layout and the profile it was drawn from. stemma render --check re-computes that fingerprint and exits non-zero when the committed image has stopped matching its source, so a build fails instead of a reader being misled. It is the same drift guarantee as the diff, extended past the app window.
What licence is it under?
PolyForm Noncommercial 1.0.0. Free for any noncommercial purpose — personal use, study, hobby projects, and use by charities, educational institutions, public research bodies and government. Commercial use of any kind needs a commercial licence: kontakt@bfrackowiak.pl. It is source-available, not open source, and the distinction is deliberate.
The argument behind all of it is an essay: The Diagram That Cannot Lie — why a diagram that can drift always does. On what diagrams are actually for, there is Diagrams Are Conversations, Not Documentation.
The repository.
The source is there, engine included, with the fidelity suite that gates it. It needs the .NET 10 SDK and Node 24 — no database, no container, nothing to procure — and ./run.sh --dev --workspace samples/AuroraRail opens the reference model. What is still in flight is stated on the roadmap and what has already landed is dated in the changelog, so you can judge the pace yourself.
PolyForm Noncommercial 1.0.0. Free for personal use, study, hobby projects, and for charities, educational institutions, public research bodies and government. Source-available, not open source: commercial use of any kind needs a commercial licence, which is a short email rather than a procurement process.
If you want to argue with any of the above — which is more useful to me than agreement — write to me. Architects who have lived with the drift problem long enough to be cynical about it are exactly the readers I want.
Reading this with a machine? The whole page is also plain markdown, the changelog is plain markdown, and the site's context file is llms.txt. The repository carries a JSON Schema for the presentation sidecar and a protocol reference for writing a workspace without opening the app.
Stemma is a pet project. It is built by one person on evenings and weekends, the source is readable by anyone, and it is not a company — there is no roadmap I owe anybody, no support line and no sales call at the end of this page. A commercial licence exists because the licence had to say something and that is the honest thing for it to say, not because there is a funnel behind it. The tool exists because I wanted it and nobody had built it.