| title | Forma | ||
|---|---|---|---|
| doc_version | 1.0.1 | ||
| status | active | ||
| last_review | 2026-08-17 | ||
| owner | Luca Dominici | ||
| canonical_id | readme | ||
| tags |
|
||
| related |
|
Present your architecture instead of slides. Forma turns any codebase into an interactive, stack-agnostic C4 explorer — big-picture → drill-down to the leaf — generated from the code and kept true to it by a deterministic drift check. No more slide decks that lie the day after you draw them.
Forma is the companion to arbiter: arbiter governs the process, Forma shows the system and
guarantees the picture matches reality.
The Control Room is dogfooded locally across the owner's active repositories, not published as a
second demo. The explorer is deliberately not this repo. It is haben, a
private 53-package Go application, generated by forma at haben commit 4c9b6880: six domains over
53 packages, 193 edges read from real import blocks, and box text quoted from that repo's own
feature matrix. Its five-checkpoint architecture timeline is governed by haben's current product
boundary: the 63-node AS-IS baseline grows cumulatively to 66 nodes and 196 edges, while the final
operational checkpoint honestly reports no architecture change. A tool proving itself on its own
28 nodes proves nothing you care about.
What you are looking at is checked, not asserted: at that commit node scripts/presentable.mjs
exits 0 on the model — every level under 24 boxes, every box carrying prose rather than a file
count, every level with more than one box drawing at least one arrow — and forma check exits 0
against haben's source at the same commit. The curation that turns 53 flat packages into six
domains and maps the governed boundary to typed timeline patches is one human decision, checked in
at docs/demo/curate.mjs with the commands to rebuild it.
The one thing the demo does not do is regenerate itself: haben is private, so Pages publishes a
snapshot committed from a local run rather than cloning it in CI. Nothing automated will notice
if it goes stale. Forma's own model lives in docs/architecture/ and is drift-checked on every
push.
A hand-drawn architecture diagram is stale the moment code changes. Forma walks your source for the real structure, infers relationships from cross-references to exported symbol names (heuristic, additive), and fails a check when the model and the code disagree. What you present is what actually exists.
Where the language declares its architecture, forma reads the declaration instead of guessing. On
Go the container and the leaf are the package (any directory with a non-test .go, however
deeply nested), _test.go files are not architecture, and every edge comes from an import block —
so the direction is right by construction. Every other stack uses the heuristic above.
npx forma-arch <command> # or: npm i -D forma-arch| Command | What it does |
|---|---|
forma init |
Seed every detected production stack/root into c4-topology.json; record reasoned exclusions and adopt only live document sources |
forma gen |
Walk source leaves + derive container edges; fill box text from docs; group package/feature paths into components → c4-model.json |
forma check |
Deterministic drift check — fails if the model no longer matches the code |
forma doc |
Project the arc42 scaffold (ARCHITECTURE.scaffold.md), or --attach <file> to inject a governed block into an existing doc |
forma serve |
Open the live explorer at http://localhost:4173 |
forma verify |
Refresh status from live GitHub issues through your gh CLI — the only networked command |
forma audit |
Emit an offline issue/claim plan, then validate and apply evidence-anchored agent results |
forma scan |
Find the programmes under a directory and write them into forma.room.json |
forma room |
Compose the Control Room — one briefing over N programmes, in one self-contained HTML |
Box text comes from your docs. gen fills each box with the module's docstring (Python """…""", JS/TS leading block), else the directory README.md, else a mapped arc42 section — so the explorer shows meaning, not a list of symbols. It synthesizes a component layer from package/feature directories, falling back to foo_* filename prefixes for flat modules (--no-cluster to disable; --cluster-min <n> = leaves before a container is clustered, default 8; --group-min <n> = files in a group before it becomes a component, default 3).
Above the leaf, your feature matrix outranks the code. A docstring is the right answer for one file and the wrong one for a whole container: a stakeholder does not ask which docstring the first file inside it has, they ask what that part of the product does for the user — and in a governed repo that sentence is already written in a capability table. forma init finds those tables, resolves their code references against the detected source roots and lists a table under docSources only when every reference is live. gen therefore cannot fail because of a source that init selected. It joins each row to the nodes its code references name, and quotes the row verbatim (descSource: "docmap"). Nothing is composed or paraphrased. A node named by more than three rows is not described by the matrix but merely touched by it, so it yields nothing and the code chain runs instead.
"docSources": ["docs/FEATURE_MATRIX.md",
{ "path": "docs/spec.md", "describe": "capability", "ref": "code_ref", "status": "status" }]Column roles are detected by header name (capability/feature/description…, code_ref/path/module…, status/state) — name them explicitly when yours differ. Auto-detection additionally requires a status column, because "feature + file" is also the shape of a refactor plan and a task line does not belong in a stakeholder's box; a source you list by hand is trusted as written.
Progress can be generated, not only hand-written. Where those rows carry a status, gen derives the node's status2 and completion from them — one of two capabilities shipped is in-progress at 50%, and verify.source names the document and the tally. This is derived, so it is never trusted: forma check re-reads the document and fails if the committed model claims a number the document no longer supports, exactly as it re-walks src/ for structure. A node no document names stays unknown — the honest blank, not a made-up zero.
Programme state is curated, not guessed. Code shows what exists, never how far along it is. Drop a docs/architecture/c4-status.json (--status <path> to move it) and gen decorates nodes by id with status2, completion, statusWord, current, target, verify, issues — never func, which belongs to the docs. gen validates the form (ids resolve, fields known, enums and issue numbers well-shaped) and never the prose; forma check fails if the overlay decorates a node the model no longer has.
{ "nodes": { "engine": { "status2": "in-progress", "completion": 60, "statusWord": "v2 in progress",
"current": "Live on ACA: RAG + citations. Hardening this week.",
"target": "Multi-surface substrate with client-ready output.",
"verify": { "source": "ADR-040 on main" }, "issues": ["#534"] } } }The overlay is the authority: every field it sets wins over anything derived, and check stops re-deriving that field. You do not have to hand-edit that JSON — forma gen --status-apply <file> merges {"nodes":{"<id>":{…}}} into it, validating every patch before it touches disk, so a rejected fill leaves the committed overlay untouched. It is the state counterpart of --enrich-apply, for the fields no document can supply. To describe one box by hand without a table, put the sentence in descriptions, keyed "<containerId>/<node-name-without-extension>" — "core/alpha" for the leaf alpha.js in container core, "core/core" for the container itself, since a container is its own container. It outranks every other source.
Future architecture can be a governed timeline. Add an optional timeline to the curated
c4-topology.json when one binary target is not enough. The generated nodes and edges remain
the code-verified AS-IS; each checkpoint applies a compact typed patch over every checkpoint
before it. Forma never stores a second complete graph, never reads a board count as architecture,
and validates every cumulative state before replacing the last model.
{
"timeline": {
"source": "docs/architecture/FUTURE.md",
"checkpoints": [{
"id": "g1", "label": "G1 · UAT", "badge": "9 board · 1 P0",
"patch": {
"nodes": {
"add": [{ "node": {
"id": "mcp", "level": "container", "parent": "system",
"kind": "container", "name": "MCP channel", "status": "planned"
}, "change": "Add the MCP surface." }],
"update": [{ "id": "engine", "set": {
"current": "Serves HTTP and MCP through one core."
}, "change": "Extend the existing engine." }]
},
"edges": {
"add": [{ "edge": {
"from": "mcp", "to": "engine", "label": "delegates",
"estatus": "to-build"
}, "change": "Connect the new surface to the engine." }]
}
}
}]
}
}Node patches support add, non-structural update, and child-first remove; edge patches support
add, exact-match rewire, and remove. Every operation carries governed change prose; a live
node changed locally exposes it as “Change from previous”, while the graph accents changed nodes
and relations without keeping a second change log or drawing ghosts for removals. IDs, parents,
levels and kinds cannot be edited in place, ambiguous edge selectors fail, and a node cannot
disappear while children or incident relations survive. The source path must exist. AS-IS is
implicit and reserved; the last checkpoint is the target, so target is forbidden inside timeline
patches. With a timeline the viewer replaces CURRENT/TARGET with the checkpoints, supports
?checkpoint=g1, accents only the local patch, and keeps drill, layout, re-verification and export
working on the cumulative graph. A future checkpoint with no local patch says “no architecture
changes”; its display-only badge is not interpreted. Without a timeline, the legacy controls and
model are unchanged.
Curated state, verified against reality. forma verify asks your gh CLI for every issue in the repository. A full result page is retried at a larger limit until a short page proves completeness; any retry or parse failure occurs before either output is written. When a model exists, verify also marks nodes whose cited issues are closed as done and prefixes their current with dated evidence; without a model it still refreshes the snapshot for the Control Room. It touches state, never structure, and re-running it never stacks the evidence. It is opt-in and separate on purpose: gen and check never open a socket. In the served viewer, RE-VERIFY re-reads the model without losing your level, layout or mode.
One source of truth. forma doc --attach docs/architecture/arc42.md injects the generated diagrams/tables between <!-- forma:begin --> / <!-- forma:end --> markers in your existing doc; your prose lives outside them, and forma check fails if that block drifts. Attached files are recorded in source.attachedDocs, so the gate governs every doc you attach — not just the model's docPath; deleting the markers (or the file) from a registered doc fails the check rather than quietly un-governing it. That registry lives in c4-model.json, so commit the model — a lost model takes the registry with it. Where a repo lacks docs, forma gen --enrich can fill the remaining box holes with an LLM — opt-in, cached, never on the deterministic gate:
--enricher |
Use it when | Network |
|---|---|---|
agent |
An agent is driving forma. Writes enrich-plan.json with the holes; the agent writes the sentences (reading the sources if it wants) and gen --enrich-apply <file> applies them with the same cache and provenance. |
none |
anthropic |
Headless / CI, with ANTHROPIC_API_KEY. |
REST |
openai |
Same, with OPENAI_API_KEY. |
REST |
ollama |
Sensitive repos: a local model, nothing leaves the machine. | localhost |
--enricher has no default: forma gen --enrich on its own fails loud and lists what it
accepts — the four above, plus echo, an offline stub the test suite uses and you should not. A
default provider is a silent choice about your network and your API keys — and the old default
(anthropic) meant that anyone without ANTHROPIC_API_KEY exported got a skip line, exit 0 and the
same empty boxes they ran --enrich to fill.
The explorer answers "what is this system". The Control Room answers the other question: across everything you are running, what needs you — and then, per programme, where it stands and why.
forma verify --repo ../haben # snapshot the issues (the only networked step)
forma audit --repo ../haben --today 2026-08-18 --plan ../haben/docs/architecture/audit-plan.json
forma scan --root .. --manifest forma.room.json
forma room --manifest forma.room.json --out control-room.html
The look is a manifest choice: "theme": "dark"|"light" for the default pair, or "skin": "paper"|"blueprint"
for one complete look (a token block the palette audit measures like every other; the map follows). A
skin is added with one CSS block, one SKINS entry, one line in scripts/palette.mjs and one schema value.
Counts are not a briefing. The judgement — where the programme stands in one sentence, the risks
with first-hand evidence, what only a human can decide now, how each invariant is guarded — is
written by an agent into the brief (c4-brief.json) as typed claims: each names its subject
(about), cites evidence that must resolve, and is stamped by Forma (writtenAt, evidenceHash).
Forma refuses what does not anchor and names every refusal (lastApply.rejected); a risk or a
decision must rest on something that can move (an open issue, a workflow/release signal, a milestone,
a recent commit — never a closed issue or a README path alone); a claim goes stale when its subject
or evidence moves; and it is coloured only while an independent counter-verifier — by default a
different engine — holds it, on a date the reader sees. Everything else is grey and says why.
The Executive opens with it. Contract: lib/schema/c4-brief.schema.json.
The whole loop is one ritual, run by any agent from the forma-room-update skill
(adapters/claude/forma-room-update/SKILL.md, same text
for Codex), split at the network boundary. Give each active programme health, findings,
auditPlan, auditFill, counterResults and brief.path in the manifest:
forma room update --manifest forma.room.json # live facts (the only network)
forma audit --plan … # what is missing or stale
<the writing agent fills verdicts, findings, brief → audit-fill.json>
forma audit --apply audit-fill.json --audit-plan … # item by item; refusals named
<the hostile verifier runs $forma-counterverify over the re-generated plan>
forma room update --manifest forma.room.json --skip-verify --fill --counter
forma check --room control-room.html && node scripts/room-presentable.mjs …
The first command refreshes every GitHub fact base and fails without recomposing if any fetch is
incomplete. The last update regenerates every plan, applies fill and counter results one entry at a
time, and only then recomposes; room-presentable refuses to publish a decision nobody held. Forma
never launches an agent itself (D-08 / I2), and today never moves on its own (I12).
One file. A briefing in reading order at #/, and under it six lenses per programme, each
answering exactly one question (ADR-0008). A lens is published only where its backing artifacts
exist: on a programme with no architecture model and no requirements source those routes do not
exist, rather than rendering panels that say "No data available." (I7, I20).
| Route | The one question it answers |
|---|---|
#/ |
Which programme needs me now? |
#/<prog>/verdict |
Can we trust this programme's claims today? — the brief (thesis, risks, decide now, invariants, coloured only under a hostile hold), findings, and what the briefing cannot see |
#/<prog>/plan |
What happens when, and what blocks the date? — milestones, the critical path with its six-field float model, drift against GitHub, and bounded lazy Queue/Kanban evidence |
#/<prog>/architecture |
What is the system, and how complete is our picture? — the map, checkpoints carrying measured completion, and the commits no issue claims |
#/<prog>/traceability |
Is what we promised built, and proven? — requirement → design → verification → issues, with the holes named, and the capability ledger |
#/<prog>/operations |
Can we run it, and survive it failing? — what the repository reports about its own workflows and releases |
#/<prog>/provenance |
Why is it this way, on whose authority? — the canon in full, the document gate, everything else listed with a link |
#/options |
What is in this briefing, and which lens each programme can answer. With --serve, checkboxes that write the manifest |
Every address the earlier five-view IA published stays valid: exec, tech, map, wbs, docs
and the retired auto/kanban all redirect to the lens that inherited the question.
Printing gives you every official view, but summarizes interactive issue archives instead of expanding thousands of historical rows. Screen and print cost therefore scale with views and programmes, not lifetime issue count.
The issues become the WBS, checkably. Declare an rtm block and Forma reads the id columns out
of the documents you already write, joins them to the snapshot, and forma check fails on a
requirement that lands on no work and on open work no requirement claims. Both directions, or the
claim is unfalsifiable (ADR-0006).
forma room init safely seeds the tracked Markdown corpus and a requirements matrix whose filename,
columns and existing RTM parser all agree; blocker, audit and findings state are never inferred.
requireIssuesFrom is the explicit completeness claim: with it, uncited open issues fail
forma check; without it, an RTM discovered by room init is evidence, not a claim that the
document already accounts for the whole backlog.
Everything on screen is re-derivable: forma check recomputes every aggregate from the raw inputs
and fails when the artifact disagrees, and scripts/room-presentable.mjs re-renders it and compares
byte for byte. See docs/SCOPE-room.md for what "finished" means here.
code ──► forma gen ──► c4-model.json ──┬──► c4-viewer.html (present / explore)
(single source) ├──► ARCHITECTURE.md (arc42, via forma doc)
└──► forma check (gate: model == code?)
One source of truth (c4-model.json); two renderings (the interactive viewer and the arc42 doc);
one deterministic check that keeps them honest. The file contract is
lib/schema/c4-model.schema.json.
Working on forma itself? docs/README.md is the reading map. Start with
docs/GLOBAL_INVARIANTS.md: the rules that do not break, each with the file that
enforces it and what turns red when it is violated.
The engine is plain Node — no LLM required. Structure is auto-walked, relationships are derived from
code, the check is deterministic. The only human (or agent) step is curating the topology groupings
and writing the arc42 prose — and any model can do that against the documented JSON/Markdown
contract. The Claude skill in adapters/ is a thin wrapper, not the product.
The viewer is a live C4 map, not a static picture:
- Click any box to read its explanation — what it does, current state/target on legacy models, or the state and local change at the selected checkpoint — at every level, from context down to a leaf.
- Double-click a box (or its
[+] DRILL) to descend into it; BACK, the breadcrumb, orESCclimb back out. - Drag boxes to lay out the view your way; RESET LAYOUT restores the arrangement (your curated hints if the topology has them, the automatic one otherwise). To keep a layout, drag it, pick Export layout JSON, and paste the result under
"layout"in the topology —gencarries it intometa.layoutand the viewer pins those boxes, auto-arranging everything else clear of them. - Arrow labels are painted on the diagram while the level stays readable (≤14 arrows) and turn off above that; LABELS forces them on or off, and hovering an arrow always reveals its label.
- PRINT / EXPORT to SVG or PNG for docs and slides — exported arrows carry their labels.
- The breadcrumb names the C4 level you are on (
C4-L1 · CONTEXT→C4-L3 · COMPONENTS) and, when the level carries curated state, tallies it: a dot per status with its count plus mean completion, so a level reads as a programme board and not just a diagram. - Every level shrink-wraps its own content — a context with four boxes renders zoomed and dense instead of adrift in a fixed canvas.
The viewer ships with swappable skins (holo, blueprint) via a dropdown or ?skin=. Themes are
CSS variables; the engine is decoupled from the look.
Apache-2.0. Not affiliated with C4 or arc42 — see NOTICE.
{ "today": "2026-08-10", // the determinism anchor: nothing calls Date.now() "programs": [{ "id": "haben", "ghRepo": "you/haben", "repo": "../haben", "issues": "../haben/docs/architecture/c4-issues.json", "model": "../haben/docs/architecture/c4-model.json", "topology": "../haben/docs/architecture/c4-topology.json", "blockedBy": { "labels": ["needs-human"] }, // never inferred: repos disagree on what waiting means "rtm": { "docs": [{ "path": "docs/PRD.md", "idPattern": "^R-\\d+$", "role": "requirement" }, { "path": "docs/DESIGN.md", "idPattern": "^D-\\d+$", "role": "design" }], "requireIssuesFrom": ["docs/DESIGN.md"] }, "docs": { "include": ["docs/*.md"], "canon": ["docs/PRD.md", "docs/DESIGN.md"] } }] }