Designing system architectures by exhaustion — a systems-engineering layer over writ.
An architecture brief is normally answered by judgement: an architect reads a few paragraphs, decides what they must have meant, and draws a diagram. The diagram is then the only record, it agrees with nothing, and the questions the brief failed to answer are discovered during implementation.
This repository does the same job mechanically. A component bank and the
brief's requirements become a Writ model; writ check enumerates every
architecture the constraints permit; and four tools turn the result into the
artifacts a team actually needs — a C4 diagram, a list of questions for whoever
wrote the brief, decision records that cannot drift from the design, and a short
list of the unverified facts the whole thing is standing on.
Nothing here is a new language. Every semantic lives in Writ, and these are
generators and formatters over writ derive output. That is deliberate: a second
language with its own meaning would put a compiler between you and the proof.
a brief, in prose
│
▼
catalogue/*.md ──writ-bank──▶ a .writ model
│ │
│ writ check
│ │
│ ┌──────────┬───────────┼──────────┐
│ ▼ ▼ ▼ ▼
│ writ-assume writ-interview writ-c4 writ-adr
│ │ │ │ │
│ the facts questions C4 diagrams decision
│ to confirm for the (D2/SVG) records
│ │ author with proofs
│ │ │
└───────┴──────────┘ answers sharpen the catalogue, and it runs again
The loop is the point. writ-interview is not a report at the end — it is the
step that sends you back to the brief with things nobody had noticed.
| reads | writes | |
|---|---|---|
writ-bank |
a catalogue of parts, as a Markdown page | the model's datums — rosters, spans, one move per part |
writ-assume |
the catalogue's unconfirmed cells | the few whose truth actually decides the answer |
writ-interview |
writ check findings |
the questions to put back to whoever wrote the brief |
writ-c4 |
writ derive rows |
a C4 diagram in D2, at context or container level |
writ-adr |
writ derive rows and proof trees |
decision records, each with what was rejected and why |
A Writ form is rename-and-paste: no recursion, no computation, and it cannot map
over its &rest. So it cannot turn "component X provides capability Y" into
the named span junction that fact requires, nor emit one pick move per part.
None of that is a defect in Writ — it is what keeps a model a finite
presentation. It is a defect in what a human should be asked to type: a bank
of 150 parts is roughly 4,000 datums, ~750 of them globally unique junction
names invented by hand.
A page of tables becomes the whole instance:
$ writ-bank catalogue/corpus.md example/corpus.extras.writ > example/corpus.writWhy Markdown. A catalogue is a curated knowledge base people argue about,
not a pipeline feed. It wants to render in a pull request, so a reviewer can
challenge "is llm-classifier really not reproducible?" without running
anything — and it wants prose. Tables carry the facts; every heading, paragraph
and <!-- HTML comment --> on the page is ignored, and a notes column
documents a row without entering the model.
Columns are matched by name, not position. Reorder them freely; add columns
the tools do not know about. A required column that goes missing is a fatal
error naming the column and the section, not a quietly wrong model — and
run-tests.sh reverses every column of the components table and asserts the
output is unchanged.
Identifier columns must stay single tokens. Values in name, provides,
tech, within and friends become entity names in the model, and Writ names
cannot contain spaces — so apache-spark, never Apache Spark. Slugifying and
keeping display labels outside the model would break the one property writ-c4
sells: that everything on the diagram is derived. Free text belongs in notes.
run-tests.sh asserts the committed model is byte-identical to what the
catalogue regenerates, so hand-editing generated output fails a test rather than
being discovered much later.
This is the weakest joint in the whole idea, and it deserves stating plainly. Everywhere else in the stack a mistake announces itself: a bad guard changes the state count, a bad rule fails the cross-check, a bad claim fails to hold. A wrong catalogue cell fails nothing. It produces a plausible model that proves wrong things with exactly the confidence it would have had if the cell were right. The catalogue is unfalsifiable input to a falsifying machine.
Asking anyone to verify every cell does not work — a real bank is hundreds of
claims and nobody will check them. But most of those claims do not matter:
flip them and every verdict is identical. So mark unconfirmed cells with a
trailing ?, and let the tool find the few that decide anything:
$ writ-assume catalogue/corpus.md example/corpus.claims example/corpus.extras.writ
ASSUMPTIONS THIS DESIGN STANDS ON
33 unconfirmed claims in the catalogue.
4 of them decide something. 29 do not.
LOAD-BEARING — confirm these before anyone builds from this design
1. object-store.bulk = yes
were it `no` instead:
realisable LOST
no-dead-end LOST
3. llm-vision.persists = no
were it `yes` instead:
rerun-is-affordable gained
4. direct-db.couples = tight
were it `loose` instead:
crm-stays-loose LOSTIt flips each assumed cell, rebuilds, and asks writ compare whether any
property moved. "Verify 33 claims" becomes "confirm these four" — and the
four are computed, not guessed. Presentation columns are excluded by
construction rather than by choice: nothing in the library reads them, so no
flip of one could move a verdict.
Read it as an indictment of the worked example, too. This repository's headline
finding rests on two cells about llm-vision that were invented in seconds, and
the tool says so.
The bug worth knowing about. The first version of this reported "0 of them decide something" — because every comparison had errored, and an error read as "no change". A tool that reports safety because it crashed is worse than no tool. A broken run is now fatal and loud, and
run-tests.shasserts it.
$ writ-interview example/corpus.writ --claims example/corpus.claims
THE MODEL ANSWERED 3 of its questions. It raises 2 for you.
184 situations reachable, 96 dead ends.
OPEN QUESTIONS — the brief is silent, and the model refuses to guess
1. the brief is silent: are the 50TB of PDFs digital-native or scanned?
the extract choice depends on it
declared at: gap `pdf-kind-unknown`, reachable in 2 moves
CONTRADICTIONS — a stated requirement the brief does not actually get
2. re-runnable classification implies persisted text
property: rerun-is-affordable
counterexample: 1. hold-object-store -> 2. enumerate-event-stream
-> 3. extract-llm-vision -> 4. classify-rules-engine
to decide: is the counterexample acceptable (relax the
requirement), or is it a real defect (add a constraint)?Neither question was thought of by anybody. The first is a declared gap — the
model saying its rules genuinely run out, rather than guessing past a hole. The
second is a never property with a witness: a stack meeting every stated
requirement whose extract stage discards its output, so "re-run the
classification" silently means re-processing the whole corpus. 48 of the 96
designs have it — half the answer set looks correct and is not.
It works on any Writ model, not only architecture ones: run it on the knights-and-knaves puzzle and the paradox comes out as an open question.
$ writ-c4 example/corpus.writ example/corpus.rules 183 --level container --svg out.svgEvery box, group, arrow and label is a row out of writ derive. If the diagram
is wrong, the model is wrong — which is the one property a hand-drawn
architecture diagram can never have. Boundaries become D2 containers, external
systems are drawn dashed, technology rides in the box label, actors are people,
and edges carry their protocol.
The edges are runtime data flow, not the build ladder. cap.next is the
order a designer fills stages in; the flow span is the order data moves.
In the worked example the catalogue fans out to both serving and the CRM, while
the ladder is a chain — so drawing the ladder would have produced a diagram that
was merely plausible. The two are never the same thing.
Each decision gets its technology and boundary, the alternatives that were rejected, the requirement each one failed, and the derivation tree as proof:
| rejected | because |
|---|---|
nas |
cannot survive the data volume (needs-bulk) |
pdf-lib |
cannot read scanned pages (needs-scan) |
llm-classifier |
not reproducible across runs (needs-rerun) |
direct-db |
couples too tightly (needs-loose) |
writ derive --why alone will not do this. The proof tree for a chosen part
shows only that it was chosen; what was rejected and on which requirement is a
different derivation.
The vocabulary splits its fields in two, and confusing them is how a diagramming need silently becomes a design constraint:
- Constraint fields (
bulk,scans,rerunnable,persists,couples) are read byfits. They decide which parts may fill which stage, so they shrink the search space. - Presentation fields (
level,tech,external,within) are read bywrit-c4and by nothing in the library. Every one isfixed, so they add no situations at all — a design space is the product of the mutable cells. The worked example scores the same 184 situations with them as without.
Add a presentation field freely. Add a constraint field only when you mean to rule something out.
Needs writ on PATH; d2 only for SVG.
./demo.sh # the whole pipeline end to end, on the worked example
./install.sh # -> ~/.local (bin on PATH, lib on WRIT_LIB)
./run-tests.sh # 74 checks over the five tools, the demo included
./run-tests.sh list # the individual testsdemo.sh is the fastest way to see what this does: it runs a brief through all
six stages and prints each one — catalogue, model, enumeration, questions,
assumptions, diagram, decision records. run-tests.sh runs it too, so the
walkthrough cannot rot while the tools move underneath it.
The tools are POSIX sh and awk — no runtime, no packages, nothing to
install beyond writ itself.
The catalogue is the project, and it is the weakest link. The component
attributes in catalogue/corpus.md were written by hand as a demonstration —
every one is marked ?, because every one is unverified. writ proves what
follows from them exhaustively, and will do so just as faithfully if they are
wrong. The rigour on offer is internal consistency, not truth.
writ-assume narrows that from fatal to manageable — it turns "verify
everything" into "confirm these four" — but it cannot tell you whether the four
are true. Only somebody who operates the system can, and no amount of tooling
substitutes for that.
A catalogue is per-decision, not an encyclopedia. The tempting reading of
writ-bank is that you should build a comprehensive bank of 150 components. Do
not: nobody can author or check that. The version that works is the eight to
twelve candidates a team would genuinely consider for one decision, written by
the people who will operate them. At that size every row is checkable, which is
the only size at which any of this is honest.
Quantities remain out of scope. Writ has no arithmetic. Ordinal scales and
precomputed fixed arrows carry a long way (a latency budget composes fine over
a finite quantale), but capacity planning, cost in currency and availability
arithmetic belong in a solver built for them, with the answers imported here as
facts.
writ-c4 draws one design at a time. The model enumerates 96; choosing among
them is a judgement the tool deliberately does not make.
| writ | the language, the engine, the CLI |
| writ-problems | worked models, including arch/ — the same search without the C4 vocabulary, kept as the teaching version |
| writ-vscode | the editor client |
Copyright (C) 2026 Alex Kunich. GNU Affero General Public License, version 3
or later (LICENSE), matching writ itself. A catalogue or model you
write is your own work, not a derivative of these tools — granted explicitly in
LICENSE.exception, which also covers the models, diagrams
and decision records the five tools generate.
Patches are welcome — see CONTRIBUTING.md.
