Stemma

The diagram is the code.

Stemma is my side project: an architecture model that lives in your repository as plain C# files. People work with it on a canvas. Agents work with it as text. Both change the same files, so the picture and the model never go separate ways.

Where to find it How the two layers work

The Stemma canvas: three bounded contexts as lanes, Ordering, Payments and Fulfilment, with modules inside them, labelled data flows between them, and external systems along the top.
Two layers, one model

One for people, one for people and agents.

The model has two faces, and both are the same files in Git. I built it like this because an architecture today has two audiences: the people who decide and build, and the agents that write more and more of the code. The second audience is why I think architects should write what agents read; the long version is Every Approval Is a Missing Rule.

Visual layer · for people

Agreement in the room.

The canvas, the views and the story books. Here an architect, a team and the people who pay for the work look at the same picture and agree on it. Every edit on the canvas is a small Roslyn rewrite of the C# file, so agreeing on the picture means agreeing on the model.

  • Module map and dependency graph, plus your own saved views
  • Story books that drive the canvas in a review meeting
  • A one-file HTML report for people who will not install anything
Text layer · for people and agents

Agreement with the agents.

The same model as three text files: Architecture.cs, one file per view, and a layout sidecar with a JSON Schema. An agent reads and writes them, checks them and renders them from a shell, and a human reviews the result as a normal diff. You get this out of the box: no plugin, no API key, no export step.

  • stemma validate catches broken ids the compiler cannot see
  • stemma render draws any view to PNG or SVG, no browser open
  • --check fails the build when a committed image is out of date
The source

Your model is a file you can read.

No project format, no XML, no new notation. The model is C# records in Architecture/Architecture.cs, and anyone on the team, or any agent, can open it in an editor and follow it.

Architecture/Architecture.csexcerpt
// 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");
Bounded contexts

Contexts are declared, not drawn.

A bounded context is a record with an id and a name. On the canvas it is a lane and modules sit inside it, but the nesting is a real reference in the file, not a position on a grid. Move a module to another lane and one string argument changes.

Elements

Ids are stable, names are not.

Each element has an id that never changes and a display name that can. Flows, dependencies, tags and views point at the id, so you can rename a concept when you understand it better and nothing breaks.

Relationships

An arrow says what it carries.

A DataFlow names its payload and its direction. A Dependency names its kind: uses, calls, reads, publishes or consumes. A link is drawn in a view only when both ends are in that view, so a filtered view never shows an arrow going nowhere.

Lifecycle & ownership

Target state lives in the model, not in a deck.

Lifecycle marks a thing as current, target, to-adapt, to-be-created or deprecated, with a phase and validity dates. Ownership adds a squad, a domain and RAPID roles. Both work on elements and on relationships, and the relationships are usually where a migration plan goes wrong.

Concerns

Risks and questions are model elements.

A risk, an open question or an assumption is an element attached to what it is about. It shows as a dotted edge to its subject, it is versioned with it, and it appears on the Concerns board. Not in a wiki page nobody opens. Why I treat open questions as a deliverable is a separate essay.

The edit

One rename. Three lines. Nothing else moved.

I double-clicked the Ledger box on the canvas, typed a longer name and pressed Enter. This is the git diff I got: all three hunks, without the file header, and with two long lines cut at the right margin.

git diffafter renaming one element in the UI1 file changed · +3 −3
@@ -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 stayed, so every other reference still works.
  • The local variable, twiceRoslyn renamed the symbol, so the tag and the list followed it. A find-and-replace would miss one, and the code would still compile.
  • Nothing elseNo reordering, no reformatting, no moved blank line, no lost comment. The reviewer sees a rename, because it was a rename.

Everything else in the tool depends on this. A suite of small, realistic and deliberately nasty fixtures runs every operation and compares the diff byte by byte, and the codebase has one fixed rule about it: when the fidelity suite fails, the implementation is wrong, not the test. A diff that only says what changed is also what makes a change reviewable when nobody remembers writing it, which I wrote about in We Deleted the Wrong Half of Agile.

The vocabulary

Everything you are allowed to say.

More than a class diagram, less than an IDE. This is the whole language, there is no hidden second part.

Elements

RecordCarries
PersonAn actor with a role: external, internal or user
SoftwareSystemA system, usually a neighbouring one you don't own
ContainerA deployable unit inside a system, with a kind: service, worker, cronjob or db
BoundedContextA DDD context; renders as a lane other elements nest inside
ModuleA unit of cohesion, optionally inside a context
CapabilityA business capability, optionally inside a context
UseCaseA user-visible use case
RiskA design risk, anchored to what it concerns
QuestionAn open design question, anchored the same way
AssumptionA standing assumption everything downstream rests on
ConceptA named box that asserts nothing about a system, for drawing an argument instead of an architecture

Relationships, metadata, views

RecordCarries
DataFlowFrom, to, a payload name shown as the edge label, and a direction
DependencyFrom, to, and a kind: uses, calls, reads, publishes or consumes
RelationFrom, to, and a free label. An arrow that claims nothing more than its label
LifecycleStatus (current, target, to-adapt, to-be-created, deprecated, or your own word), phase, valid-from, valid-until
OwnershipSquad, domain, and RAPID role lists: recommend, agree, perform, input, decide
Tag.ForAttaches a lifecycle and/or an ownership to any element or any link
ViewA named subset with a base lens (module map, dependency graph or all), stored as its own .cs file
The text layer

Nobody has to open it.

This is the text layer up close. A workspace is three text files with a vocabulary that is written down, so the canvas is one way to edit the model, not the only one. An agent with a shell writes the files, checks them and renders the picture. Nothing to install or connect for this: it is how the tool works from the first commit. It is the same idea as the context repo in Prompt-Driven Architecture: the text is the asset, and the picture is a build artefact.

One workspace, three filesabbreviated
// 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" }  ]}
The model

A box that asserts nothing.

Every other element kind is a claim: this is a bounded context, this is deployable. The boxes in an argument claim nothing, and if you force one into a Module, you write something false into a file other people will copy. Concept and Relation are the way out: a named box and a labelled arrow that mean only what they say.

The view

The view is the question.

A view names the subset one picture is about. A link is drawn only when both ends are in the view. This is the most common reason a diagram looks fine and still misses an arrow, so the validator reports this case by name.

The layout

Describe the arrangement, not the positions.

A language model is good at intent and bad at pixels. So the agent writes the intent, three columns, sixty apart, centred, and Stemma calculates the coordinates. When you move one box by hand, Stemma stores an offset against the constraint instead of replacing it, so the arrangement survives the correction and the offset moves with its slot when the grid is solved again. row, column and stack work the same way.

Annotation

Ink is not architecture.

Swimlane labels, brackets and callouts carry no architectural meaning, so they are free-form shapes in the sidecar, never elements. I write this down because agents break this rule first: if you want an element only to make the picture look better, what you want is a shape.

a shell, no browser opencheck it, draw it, then catch it when it gets stalewrapped to fit
$ stemma validate --workspace .
  info    model/is-a-figure            6 of 6 elements are Concepts, so 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
  • DeterministicThe same input gives the same picture. Every render starts its own short-lived backend, so two renders can run in parallel and neither touches a workspace you have open.
  • CheckableEvery render writes a manifest with a fingerprint of the model, the view, the layout and the profile. --check calculates it again and fails the build when a committed image no longer matches its source.
  • Written downThe sidecar has a published JSON Schema, and every file points to it. The protocol reference in the repo explains the whole chain for someone who never opens 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 blog, the one in The Bottleneck Moved, first made in draw.io. Getting the same picture from a model, with one command and no mouse, transparent and identical on two machines, was the acceptance test for this pipeline. The story behind it is less nice. Before, the diagrams for a later essay were made from a Stemma model by a 190-line Playwright script: it started the app, opened a real browser and dragged every node into place with fake mouse events, in three passes, because dragging was the only thing that stayed. It worked, and it was also a bug report against my own product. stemma render is what deleted it.

The views

One model, a different lens for each room.

A view is a projection, not a copy: a named subset of the same model with its own layout, styling and notes. When you switch a view, you change the lens, not the data.

The Concerns board in Stemma, listing the model's open question, assumption and two risks, each labelled with the element it is about.
The Concerns board Every question, assumption and risk in the model, each one still pointing at its element. Click one and it opens on the map, selected.
Story books

The review meeting is part of the architecture.

This is the visual layer at work. Modelling the system is half of the job. The other half is forty minutes in front of the people who decide the budget, and usually that is a slide deck, rebuilt by hand every quarter, which starts to drift from the model on the same day. And an architecture nobody follows did not happen, which is the point of Adoption Is the Deliverable.

Concepts/view-book.stemma.yamlexcerpt
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 page

A view, plus what you would actually say.

Each page points to a view and has a title and a narrative. Not a caption, but the sentence you say when that picture is on the screen, written down so it still works when you are on holiday.

An audience

One model, one deck per room.

A book has an audience: leadership, engineering, security or operations. The committee that funds the work and the team that builds it get a different order, depth and language, from the same model, with no second copy to maintain.

Present mode

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 below it, and prev/next takes the room through it. Drag pages to reorder them, edit the narrative in place, and export a multi-page PDF for people who want a document.

Why it matters

The deck cannot drift either.

A book stores a reference to a view, never a picture of it. When the architecture changes, next quarter's walkthrough is drawn again from the current model, so the story you told the board and the code you shipped stay one artefact.

Stemma in present mode: a four-page steering review for leadership, with the page strip along the bottom and the narrative for page one below the canvas.
Present mode A four-page steering review for leadership. The strip at the bottom is the story, the canvas above it is the model. Nothing was exported to build this.
The shape of it

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 why I can test the fidelity guarantee on its own.

ClientReact 19 · Vite · React Flow · zustand · Tailwind. Renders the active view, sends operations, applies deltas. It holds no authoritative state, a refresh just fetches the snapshot again.
WebASP.NET Core 10. REST for load, snapshot, layout and views; a SignalR hub for the operation stream. The only place model-provider calls are allowed to live.
EnginePure Roslyn. Workspace, in-memory model, the operation catalogue, DocumentEditor rewrites, the DSL reader and writer, undo, the layout sidecar. No web, no network.
ModelThe record vocabulary your workspace references. Nothing but type definitions.
StorageYour Git working tree. Architecture/*.cs, Views/*.cs, stemma.layout.json. No SQLite, no Postgres, no embedded store.
The lineage of a Stemma workspace Two committed files at the top, Architecture.cs and stemma.layout.json, with descent lines to four derived views: Module Map, Dependency Graph, Concerns board and the HTML report. Model · committed Architecture.cs α Presentation · committed stemma.layout.json β View Module Map A View Dependencies B Board Concerns C Publication Report .html D
A stemma codicum is the family tree philologists draw for a manuscript: every copy traced back to one original. The files here have the same shape. Two committed files, every view comes from them, and nothing has to be kept in sync by hand.
Roadmap

Horizons, not dates.

A side project I build on evenings and weekends, since 4 May 2026. I don't promise quarters, so each item has a status instead, in Stemma's own lifecycle words, the same ones 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
Questions

What people ask first.

Nine questions people ask first, answered here instead of three emails later.

Does the diagram stay in sync with the code?

It cannot drift, because it is the same thing. 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 keep in sync.

Does my whole system have to be written in C#?

No. C# is only the notation of the model, not a limit 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. You need a .NET runtime, 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, and the presentation (positions, styling, routing, notes) is a stemma.layout.json sidecar committed next to it. While a workspace is open the model is in memory; the files are the permanent storage.

How is this different from draw.io, Lucidchart or an EA tool?

A drawing tool gives you a picture that is true on the day you save it. A reverse-engineering tool reads code but cannot edit it. Stemma works in both directions and keeps round-trip fidelity, so a UI edit becomes a diff with only that edit in it. That is what makes the loop safe 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 nothing else: no reordering, no reformatting, no lost comment. The reviewer reads it as the change it was.

How do stakeholders see the model without installing anything?

Two ways, both in the visual layer. A story book is an ordered sequence of pages for one audience that drives the canvas while you present, stored next to the model so it cannot get stale. Or you export one self-contained HTML report: a single file that opens offline, with no server, no login and no seat, with audience modes and comments attached to elements that you can import back.

Can an agent use this without opening the canvas?

Yes, and out of the box: that is the text layer. A workspace is three text files, Architecture/*.cs, Views/*.cs and stemma.layout.json, and all three are documented for someone who will never see the app. An agent writes them, runs stemma validate to catch the string ids nothing else checks, and runs stemma render to get the image with no browser and no mouse. The layout is a constraint (grid, row, column, stack) instead of coordinates, because a model is good at intent and bad at pixels.

What stops a published diagram from going stale?

Every render writes a manifest next to the image, with a fingerprint of the model, the view, the layout and the profile. stemma render --check calculates it again and exits non-zero when the committed image no longer matches its source, so the build fails before a reader sees an old picture. It is the same drift guarantee as the diff, outside the app window.

What licence is it under?

PolyForm Noncommercial 1.0.0. Free for any noncommercial use: personal use, study, hobby projects, and use by charities, schools, public research bodies and government. Any commercial use needs a commercial licence: kontakt@bfrackowiak.pl. It is source-available, not open source, and that is on purpose.

The argument behind all of this is an essay, The Diagram That Cannot Lie, about why a diagram that can drift always does. And on what diagrams are really for: Diagrams Are Conversations, Not Documentation.

Where to find it

The repository.

github.com/batas2/Stemma

The source is there, with the engine and the fidelity suite. You need the .NET 10 SDK and Node 24, no database and no container, and ./run.sh --dev --workspace samples/AuroraRail opens the reference model. What is still in progress is on the roadmap, and what already shipped is dated in the changelog, so you can check the pace yourself.

PolyForm Noncommercial 1.0.0. Free for personal use, study and hobby projects, and for charities, schools, public research and government. It is source-available, not open source: any commercial use needs a commercial licence, and that is a short email, not a procurement process.

If you disagree with something on this page, that is more useful to me than agreement, so write to me. I especially want to hear from architects who lived with diagram drift long enough to stop believing in fixes.

Reading this with a machine? The whole page is also plain markdown, the changelog too (markdown), and the site context file is llms.txt. The repo has a JSON Schema for the layout sidecar and a protocol reference for writing a workspace without the app.

Stemma is my pet project. I build it alone, on evenings and weekends, the source is open to read, 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. The commercial licence exists because a licence has to say something, not because there is a funnel behind it. I built the tool because I wanted it and nobody else had.