Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
309 changes: 49 additions & 260 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,295 +2,84 @@

<img src="docs/images/writ-mark-200.png" alt="writ" width="120" align="left" hspace="16" vspace="4">

**Designing system architectures by exhaustion — a systems-engineering layer
over [writ](https://github.com/writ-lang/writ).**
**Design system architectures by exhaustion, with
[writ](https://github.com/writ-lang/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.
An architecture brief is usually answered by judgement: someone reads it,
decides what it must have meant, and draws a diagram. writ-arch does it
mechanically. A catalogue of candidate components and the brief's requirements
become a writ model; `writ check` enumerates **every** architecture that fits;
and five small tools turn the result into what a team needs.

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.
<br clear="left">

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.

## The workflow
## The loop

```
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
catalogue/*.md ──writ-bank──▶ model.writ ──writ check──▶ every valid design
│
┌───────────────┬───────────────┬───────────────────────┤
writ-assume writ-interview writ-c4 writ-adr
facts to questions for C4 diagrams decision records,
confirm the brief's author (D2 / SVG) 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.

## The five tools

| | reads | writes |
| Tool | Turns | Into |
|---|---|---|
| **`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 |

### `writ-bank` — because a form cannot map over a roster

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:

```console
$ writ-bank catalogue/corpus.md example/corpus.extras.writ > example/corpus.writ
```

**Why 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.
| `writ-bank` | a Markdown catalogue of parts | the writ model |
| `writ-assume` | the catalogue's unconfirmed (`?`) cells | the few that actually change an answer |
| `writ-interview` | `writ check` findings | questions for whoever wrote the brief |
| `writ-c4` | one chosen design | a C4 diagram where every box is derived from the model |
| `writ-adr` | the derivations | decision records: what was chosen, what was rejected, on which requirement |

### `writ-assume` — the answer to "who could possibly verify this catalogue?"
All the meaning lives in writ; these are generators and formatters over its
output, in POSIX `sh` and `awk`.

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.
## What it finds

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:

```console
$ 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 LOST
```

It 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.sh` asserts
> it.

### `writ-interview` — the step that pays for the exercise
On the worked example — 50 TB of PDFs to classify, feed to AI and surface in a
CRM — 96 of 2,916 component combinations satisfy the brief. More useful are
the two questions nobody had asked:

```console
$ 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)?
counterexample: hold-object-store -> enumerate-event-stream
-> extract-llm-vision -> classify-rules-engine
```

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` — diagrams that cannot lie

```console
$ writ-c4 example/corpus.writ example/corpus.rules 183 --level container --svg out.svg
```

Every 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.

### `writ-adr` — records that re-derive

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.
Half of the 96 designs meet every *stated* requirement yet discard extracted
text, so "re-run the classification" would mean reprocessing the whole corpus.

## `lib/arch.lib.writ` — and one distinction that matters
And `writ-assume` turns "verify 33 catalogue claims" into "confirm these four":
it flips each unconfirmed cell and reports only those that change a verdict.

The vocabulary splits its fields in two, and confusing them is how a diagramming
need silently becomes a design constraint:
## Run it

- **Constraint fields** (`bulk`, `scans`, `rerunnable`, `persists`, `couples`)
are read by `fits`. They decide which parts may fill which stage, so they
**shrink** the search space.
- **Presentation fields** (`level`, `tech`, `external`, `within`) are read by
`writ-c4` and by nothing in the library. Every one is `fixed`, 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.

## Running it

Needs [`writ`](https://github.com/writ-lang/writ) on `PATH`; `d2` only for SVG.
Needs `writ` on `PATH` (and `d2` for SVG):

```sh
./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 tests
./demo.sh # the whole pipeline on the worked example
./run-tests.sh # the tools' tests
./install.sh # -> ~/.local
```

`demo.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.

## What this does not claim

**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.
## Limits

`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.

## Related

| | |
|---|---|
| **[writ](https://github.com/writ-lang/writ)** | the language, the engine, the CLI |
| **[writ-problems](https://github.com/writ-lang/writ-problems)** | worked models, including `arch/` — the same search without the C4 vocabulary, kept as the teaching version |
| **[writ-vscode](https://github.com/writ-lang/writ-vscode)** | the editor client |
The catalogue is the weakest link: writ proves what follows from it, right or
wrong. Keep it small — the eight to twelve candidates a team would really
consider for one decision, written by the people who will run them — and use
`writ-assume` to find the cells worth checking. writ has no arithmetic, so
capacity and cost belong in a dedicated solver, with results imported as facts.

## License

Copyright (C) 2026 Alex Kunich. **GNU Affero General Public License, version 3
or later** ([LICENSE](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`](LICENSE.exception), which also covers the models, diagrams
and decision records the five tools generate.

Patches are welcome — see [`CONTRIBUTING.md`](CONTRIBUTING.md).
Copyright (C) 2026 Alex Kunich. [AGPL-3.0-or-later](LICENSE), like writ. Your
catalogues and models, and everything the tools generate from them, are yours:
see [LICENSE.exception](LICENSE.exception). Patches welcome —
[CONTRIBUTING.md](CONTRIBUTING.md).
Loading