Worked models for writ. The language, the checker and the CLI live there; this repository is the problems and their answers.
A sequence of end-to-end tests that use the writ tooling to solve real
problems and check the answers. Each problem is written as a Writ model
(.writ) with its questions kept next door (.claims), exactly as the spec
prescribes; the test runner calls writ check (and, for oversight, writ compare) and asserts the verdicts.
The first batch is puzzles: the two from the spec's Prologue (Appendices C
and D), faithful to the spec, plus eight queens, which is not from the spec
but is the first thing the language could state once differ existed, and a
blocking job shop, whose deadlock is the trap detector at work on a real
scheduling question. Each
move is given a name so writ prints a legible solution path (see Notes on the
solution path below). The second batch is the three §3 scenarios — institutional architecture, a regulated
workflow, and access & privilege — which exercise equation laws, accept
acknowledgments and writ compare.
The third is one scenario that turns the tool around. Every model above
checks something already designed; arch/ designs it — a component
bank, a brief, and every architecture the constraints permit, enumerated. Its
most useful output is not the answer but the question it hands back: the one
thing the brief forgot to say.
The fourth is a pair. calculation/ holds two models of one world that
differ in a single conjunct, and one claims file put to both: the questions are
held fixed so that the difference in the verdicts is attributable to the
difference in the models, and to nothing else.
The sixth turns the tool the other way round again. Every model above is
written to be walked; timetable/ is handed a finished artifact — a school
week a CP-SAT solver decided — and asked whether it is any good. Its schema is
fixed throughout, so the space is one situation and the whole run is spent on
the questions.
The seventh is the only one whose subject is a change: db-migration-problems/rename-a-column/ gates
an expand/contract column rename, proving a deploy plan safe at every instant —
including the instants when a rolling deploy has two releases serving at once —
and refusing the plan that reads before it backfills.
The fifth is the smallest file here and the only one whose subject is a
claim. gotha/ states a proposition that forbids something, in Popper's
sense, and asks writ for a situation it forbids. There is one, three moves
away.
A farmer must ferry a wolf, a goat and a cabbage across a river; left unattended with its prey, the wolf eats the goat and the goat eats the cabbage.
writ check answers:
holds solvable— yes, everything can reach the far bank intact, andwritprints the crossing as the witness (the solution the question asked for):cross-wolf-LR → cross-empty-RL → cross-goat-LR → ….fails no-blunders— but not every path stays safe: there is a reachable arrangement from which the crossing can no longer succeed. The witness is the blunder —cross-empty-LR → wolf-eats-goat-L— andstuck at:showsgoat.at=∅, the goat eaten.
Knights always tell the truth, knaves always lie. Reading a native's recorded claim assigns a consistent kind — but "I am a knave" is consistent with neither kind.
writ check answers:
gaps: 1— the rules run out exactly once: reading cal (who said "I am a knave") is a declared hole ("the island's rules are silent").fails census-completable— so not everyone can be classified.holds abe-can-be-knight/holds bea-can-be-knight/fails cal-can-be-knight— abe and bea (who each said "knight") could each be a knight; cal could be nothing at all. That is the paradox, made mechanical.
Each writ check exits 1 — not because anything is broken, but because it
has a finding to report (a possible blunder; an unclassifiable native). The
tests assert exit 1 as the correct outcome.
Eight queens on a board, none attacking another.
writ check answers holds solvable, and the witness is a solution —
the eight moves that place the queens. All 92 complete boards appear as dead
ends, a full board having no move left.
It earns its place for two reasons beyond the puzzle. It is the first thing
Writ can state that it could not before: a row clash is
(differ q1.row q2.row), which had no spelling until §8.6 let a law hold a
guard and differ joined the standard library. And it carries the clearest
measurement here of which optimisation matters — the same puzzle without
one ordering conjunct has 118 969 situations instead of 2 057 and does not
finish in ten minutes, while an engine-level change that looked obviously
right measured to nothing. Both are written up in
queens/README.md.
Diagonals cannot be computed — |Δrow| = |Δcol| is arithmetic and Writ has
none — but they need not be enumerated either: rows are entities on a
ladder with next/prev, so the square a queen d columns away attacks
is R.next walked d times, and a form names that test once per distance.
Twenty literal conjuncts per move became nine that read. The models are still
generated, there being one move per (column, row), but that is the move set's
doing rather than the arithmetic's.
Three jobs, three machines, routed in a cycle, with no buffers: a job holds the machine it is on until it has acquired the next one.
A shop has two different questions, and they need two different models.
jobshop-possible/ — does a schedule exist? writ check answers holds all-finish, then fails never-stuck, naming a three-move circular
deadlock in full: a-enters, b-enters, c-enters, after which each job holds
the machine the next one waits for. No time in this model at all — and it does
not need any, because the shop seizes on the shape of the routings and would
seize identically whether an operation took a minute or a week. 51 situations.
jobshop-best/ — which schedule is shortest? The same shop with a clock:
ticks as a small named scale (Appendix G's own escape clause), a ladder of
entities walked by next, so "finishes within 5 ticks" is an ordinary
possible. done-by-4 fails and done-by-5 holds, which pins the
optimum from both sides, and the witness is the optimal schedule. Optimisation
by repeated feasibility rather than a cost function. 1314 situations — time is
expensive, which is why the untimed model stays.
The pair is the lesson. The optimum overlaps two jobs and holds the third back,
and the other model is what proves it must: full utilisation is the deadlock.
Neither answers that alone. See
jobshop-possible/README.md and
jobshop-best/README.md.
Both are 19 and 20 lines, because the shop itself is not in either of them:
libraries/scheduling.lib.writ is a domain
library holding the
machines, the routings, the blocking rule and a clock, and the two models load
it and then differ only in their moves — which is the one thing a pair exists
to compare. Their .claims and .rules files load it too, so a model, a
question and a derivation share one vocabulary without sharing a file.
libraries/chess.lib.writ does the same for the
board. Both live in libraries/, which explains what
makes a file a library and what grouping them cost.
Three institutional models, each exercising machinery the Prologue puzzles do
not: equation laws + accept acknowledgments (§8.6, §15, §16.3) and
writ compare (§17).
The kernel-spec's own running example: two oversight agencies, one case
(docket), a possibly-vacant judge, and the structural law same-agency (the
investigating and prosecuting agencies must stand on equal footing). Schema,
instance, equation and moves are transcribed from §4; a single guard token is
changed (see the honest note below).
writ check answers:
equation same-agency/can be broken by: capture-watchdog, restore-watchdog— the tool's guard-and-effect analysis names exactly which of our own powers can violate our own declared law (both writewatchdog.independence, an arrow both routes of the law traverse). The.claimsfileaccepts both, so neither is reportedunadmitted; accepting a non-breaker (conclude,assign-judge) would be reportedstale. Reachable violations are still listed.holds conviction-possible— the docket can conclude, andwritprints the concluding move as the witness.holds accountability—(live (is docket.stage concluded)): from every reachable situation the case can still conclude, becauserestore-watchdogcan always undo a capture.
oversight-repeal.writ is the same model with restore-watchdog deleted —
an amendment that looks procedural. writ compare oversight.writ oversight-repeal.writ reports its true cost:
properties: accountability LOST witness: 1. capture-watchdog
and exits 1. One lawful capture now permanently destroys accountability: the
watchdog can never be restored, the investigation stalls, and the docket can
never conclude from that situation. same-agency and conviction-possible stay
preserved — the loss is precisely accountability.
Honest note. In §4 as printed,
concludeguards on the prosecutor's independence while the capture power targets the watchdog (investigator) — so the two are decoupled and deletingrestore-watchdogcosts nothing (verified: the compare stayspreserved, exit 0). To make the amendment's cost real — and keep thesame-agencybreaker list exactly{capture-watchdog, restore-watchdog}—concludehere guards ondocket.investigator.independence(the bureau the capture power actually targets). That is the only departure from §4; capturing the prosecutor instead would add two more breakers.
A case wired (fixed) to a reviewing officer and its unit of record, a mutable
approving officer, and a vacatable assignee slot. The law officer-in-unit
((= case.officer.unit case.unit)) demands the approving officer belong to the
unit of record.
writ check answers:
gaps: 1—escalate— the automated process settles a case that has an assignee; where there is none it ends at a declaredgap("escalated to a human"). That is the boundary where automation stops and a person takes over.can be broken by: reassign-to-fraud, reassign-to-kyc— the reassignment moves are the law's breakers (both writecase.officer); the.claimsaccepts both, so nothing isunadmitted.holds settle-able—(live (is case.stage settled)): no case ever gets stuck. Even in the escalation state (no assignee) the case can be assigned and then settled, so a settled situation stays reachable from everywhere.
Accounts with a role (user | admin), a sponsor (who vouches for them), and a
fixed source of authority. The law traces-to-root
((= account.sponsor.source account.source)) demands each account inherit its
authority from its sponsor — the chain back to the security root.
writ check answers:
can be broken by: delegate-alice-to-mallory— the one grant that rewires asponsorto an externally-sourced account breaks the root-tracing law; the.claimsaccepts it (the role grants writerole, off the law's route, so accepting them would bestale).fails revocation-possible—(live (not (some (a account) (is a.role admin)))): revocation is not always possible.grant-breakglass-adminis a designed latch — an emergency admin with no revoke move — so once it fires,malloryis admin forever and a "nobody is admin" situation can never be reached again.writprints the stuck state and the single latching move as the witness (1. grant-breakglass-admin). §3 asks the model to say whether an irreversible grant is a latch or a defect; here the model declares it a latch.query admins— the accounts holding admin now (empty at the baseline).
A coordinator and two participants agreeing to commit or agreeing to abort. A participant that votes yes is prepared: it has promised, so it may no longer decide for itself, and it holds its locks until told. The coordinator may crash at any moment, including after deciding and after telling one participant but not the other. It is the scenario that uses all four modalities, and the only one that asks a question under an assumption.
writ check answers:
holds atomic—(never (and (some … committed) (some … aborted))): the parties finish together or not at all, over all 84 situations. A holdingneveris a census, not a search that came up empty.fails can-decide—(live …): a prepared participant can be left unable to decide, and the counterexample is two moves:p1-yes,c-crash. This is the textbook criticism of the protocol, found rather than recalled.fails must-decide—(inevitable …): nor is any run obliged to decide.inevitableis the stronger of the two, so nothing passes it and failslive; where they come apart is the next model.fails must-decide-if-applied— the same question under(fair p1-commit …), and it still fails, because no assumption about scheduling rescues a protocol that has stopped. The verdict prints what it assumed, so it cannot be quoted as the unconditional one.
two-phase-commit-lossy.writ has no crash at all — the network simply drops a
message and the coordinator sends it again, which is a correct protocol. There
can-decide holds and must-decide fails: deciding is always still
available, and a run can decline it for ever. Assume a participant that has been
told the decision eventually applies it, and must-decide-if-applied holds. That
gap is the whole reason for the fourth modality, and for letting a question carry
an assumption the model does not.
two-phase-commit-timeout.writ is the tempting cure — let a stranded participant
give up — and writ compare prices it in one table: atomic LOST, with the
seven-move run that breaks it, and every liveness property gained. Safety
sold for termination, stated rather than argued.
50TB of files, mostly PDFs. Classify them, configurably and re-runnably. Feed what they contain to AI. Surface it in the CRM that already exists.
The scenarios above check a design someone wrote. This one produces one:
nineteen components, seven stages, the brief's requirements as guards, and every
architecture the constraints permit walked by exhaustion. It is queens/ one
level up — a queen is placed on a square, a stage is filled by a component, and
the same cursor keeps it to seven moves. The vocabulary is in
libraries/arch.lib.writ.
writ check answers:
dead ends: 96— 96 architectures satisfy the brief, out of 2,916 raw combinations, andholds realisableprints one of them stage by stage.gaps: 1— the brief never says whether the PDFs are digital-native or scanned, and that one unstated fact decides which extract components are admissible.writreports the hole rather than guessing past it. This is the question to put back to whoever wrote the brief, derived rather than intuited.fails rerun-is-affordable— and this is the finding. The witness isobject-store → event-stream → llm-vision → rules-engine: every stated requirement met, butllm-visiondiscards what it extracts, so re-running the classification silently re-processes all 50TB. Half the answer set — 48 of the 96 designs — looks correct and is not. "Be able to re-run" is a sentence about the classify stage that constrains the extract stage two steps away, and nothing in the brief connects them.holds no-dead-end/holds crm-stays-loose— no partial choice strands the build, and the CRM is never coupled at the database.
It is also the repository's first never properties, which is why the
modality cross-check no longer reports that branch as unexercised.
Reading a finished design out is writ derive's job, not a query's — see the
scenario's README for why (is k.chosen c) in a query
silently answers zero rows.
One allotment of steel. Three plants: two that need it, one that would build a statue with it. Only one can have it.
Every scenario above puts questions to one model. This one is a pair,
and the pair is the instrument: market.writ and planned.writ share a schema,
an instance, a law, a move set and every transition name, and differ in one
conjunct — whether the request an allocator reads must be backed by the asking
plant's own situation. The same market.claims is
put to both, and the verdicts are what move. The vocabulary is in
libraries/economy.lib.writ.
writ check answers, of both:
holds need-can-be-met, in each — neither arrangement is incapable; both can end with the steel built where it was needed, andwritprints the three-move route in each.holds no-wasteunder prices,failsunder the plan — a costless request cannot be weighed against anything, so the monument's request buys the steel:ask-monument-high → allocate-monument → build-monument. Under prices the same three moves are written down; the first never becomes available.holds need-always-still-meetableunder prices,failsunder the plan — and this is the finding. The waste is not a delay:stuck at: (… steel.at=monument steel.state=built). Steel welded into a statue is steel no longer, so no future move can put it right.gaps: 1, in the plan only —survey, the move that would find out what the requests do not carry, is a declared hole rather than an invented procedure. That gap is the economic calculation problem in the one word the language has for it.violated in 24 reachable situations— of its 56, against the law both models declare and bothacceptthe same nine breakers for. The priced model reaches no violation of it at all. (56 states against 12, for the same world: a signal that is free to send multiplies the arrangements without adding information.)
And the pair supports the operation a pair exists for:
$ writ compare calculation/market.writ calculation/planned.writ
properties: need-can-be-met preserved
no-waste LOST witness: 1. ask-monument-high 2. allocate-monument 3. build-monument
need-always-still-meetable LOST witness: 1. ask-monument-high 2. allocate-monument 3. build-monumentWhat the finding is and is not — Writ is a possibility engine, so fails means
permitted by the rules, not inevitable — is worked through in the scenario's
README, along with what to write if you want to
overturn it.
One mill; seven situations; twenty-six lines. The smallest scenario here, and the only one whose subject is a claim rather than a system.
A claim earns testing, in Popper's sense, by forbidding something — and a
prohibition is what never is, which makes the shape of this scenario the
tool's own. The claim: once the means of production are held in common, no
surplus goes to a party outside the work, by the decision of a party outside the
work. The model grants the expropriation in full and at once, with no move
back, and every move in it is one the programme itself asks for — the Manifesto's
abolition, Gotha's deductions, the Commune's recall.
writ check answers:
holds expropriation-succeeds— the antecedent is reached: the mill does become common and the surplus does reach the hands that worked it. A model in which the revolution failed would refute nothing.fails no-exploitation— and the witness is the falsifier, three moves long:expropriate → appoint-board → fund-administration. Gotha's own second deduction, "the general costs of administration not belonging to production", is made by a body that does not work the mill, from a mill the claim says cannot be exploited.holds exploitation-endable— and this is in the claims file so the finding cannot be stretched: recall works, so what is exhibited is a permitted condition, not a trap.
The mechanism is in the schema rather than the moves: worked-by,
directed-by and surplus-to are three arrows where speech has one word, and
expropriate moves a fourth, title. See
gotha/README.md for the claim's untestable sibling, why the
criterion is taken at its strongest, and the amended claim the refutation leaves
standing.
Sixty lessons, three groups, five days: every hour the curriculum demands, placed in a period, a room and a teacher's diary. Every hard constraint met. Would a school accept it?
arch/ designs and the rest walk a space; this one is handed a decided
artifact and audits it. The week was produced by CP-SAT — the constraint
solver in Google's OR-Tools, which is given unknowns and the rules they must
obey and searches for values satisfying all of them — running in the sibling
repository writ-scheduling-verification; the schedules here are frozen
fixtures, so the verdicts are exact. The solver never saw the questions, which
is the only reason its answers are worth checking.
writ check answers:
states: 1— every arrow isfixed, because a decided timetable has nothing left to vary, so there is one situation and the run is spent evaluating questions rather than enumerating. Checking is cheap exactly where generating is dear: askingwritto produce a timetable passes the 200 000-state cap at about fifteen lesson-hours.- ten properties hold — the programme is delivered exactly (no hour missing, none invented, none delivered twice), nothing is in two places at once, every room is the right kind and big enough, every teacher qualified and available. These restate independently what the CP model was told; agreement between two statements of one requirement, in two languages, is worth more than either.
fails sport-not-first/fails time-to-change-after-sport— two rules any teacher would say out loud and nobody encodes. The timetable is feasible, optimal, and would be rejected by the first person to read it.gaps: 2— and these are the better half. The curriculum never says whether a group may have a free period between lessons, or whether two hours of one subject may fall on one day, so the solver settled both by accident. Questions to put back to whoever wrote the curriculum.- the same claims file, put to
timetable-strict.writ— the week re-solved with those findings encoded — reportsgaps: noneand exits 0, andwrit comparebetween the two printssport-not-first gainedwith everything elsepreserved. One question suite, two timetables, and the difference read off rather than argued.
Counting is the other thing to look at: five hours of maths is five demand
entities told apart by an ordinal, and three properties make lesson↔demand a
bijection — exact counting with no arithmetic anywhere. See
timetable/README.md.
Three schema changes that look routine, each with a plan that reads correctly
and goes wrong anyway. Each directory holds its own page, written for someone
who has not used writ.
| problem | the mistake | how far in |
|---|---|---|
drop-a-column/ |
dropping a column while a release that still reads it is live | 2 moves |
add-a-required-column/ |
adding NOT NULL before the code that supplies a value ships |
3 moves |
rename-a-column/ |
switching readers to the new column before the backfill finishes | 4 moves |
They start from SQL, because that is what people write. Each step of each
migration is a .sql file, read by writ sql. That alone catches one mistake:
adding the new column as NOT NULL during an expand. The good and bad DDL
differ by a single character in the generated model — text? against text —
and because each file also carries a row that predates the column, the bad one
is not merely different but refused, naming the row that cannot exist.
What the DDL cannot say is the order: when to run each step relative to each deploy, and what the code reads and writes meanwhile. That is written down as a state machine and checked at every moment, including the moments mid-rollout when two releases are live together and disagree about the table.
Each problem is a pair — a safe plan, and the same file with one word, one conjunct or one condition removed — asked the same questions, so any difference in the answers comes from that change alone. The safe plan exits 0 and prints its runbook as the witness. The shortcut exits 1, names the rule, counts the broken situations and prints the shortest route to one.
Three things hold across all three, and they are the reason the exercise is worth doing:
- The unsafe plan is always faster — two steps against three, three against five, eight against nine. The removed condition is always a wait.
- The unsafe plan still finishes, and never strands you. Every question except "is it safe on the way" answers in its favour.
- The mistake is never in a file. In two of the three, every SQL file and every release is individually correct; what is wrong is the order.
One warning recorded there for anyone wiring this into CI: writ compare of a
safe plan against its shortcut reports everything preserved and exits 0. It
answers a different question — "which guarantees did this version give up?" —
and the shortcut gave none up. It still declares the same rules; it just breaks
one. Use check.
Two thin tests that exercise the last of the §17 command line on the models above (no new model files).
writ control oversight.writ emits the model's move list as an instance of the
standard library's quiver schema: one node, one edge per transition. The
test asserts it is a (of quiver) instance and then wraps and re-checks it,
proving the export is real Writ data that builds with the same machinery — the
seam toward simulation maps between two models' move lists.
The oversight repeal, but as history rather than two files. The test spins up
a throwaway git repo, commits the law, then commits the repeal to the same path,
and runs writ compare --git HEAD~1 HEAD law.writ — reporting accountability LOST across the two commits, exactly as writ compare OLD NEW did on the two
files. (Needs git; the Docker image installs it, and the local runner skips
this one gracefully if git is absent.)
This repository contains no engine. Install
writ first — the runner needs writ on your
PATH, and the models need the standard library the install puts alongside it:
opam install writ # or: make install-writ, from a writ checkout
writ --versionThen:
./run-tests.sh # every scenario
./run-tests.sh river # just one
WRIT=/path/to/writ ./run-tests.sh # a writ that is not on PATH(load "stdlib.writ") resolves with nothing configured: the installed layout
puts the standard library at <prefix>/share/writ/lib, which is where the
resolver looks (design D3). WRIT_TRACE_LOADS=1 prints what each load actually
resolved to, if a model ever surprises you.
Everything above needs writ on your machine. If you would rather install
nothing, the scenarios run in a container built from the image the writ
repository produces:
cd ../writ && make image # tags writ:latest — once, and only when writ changes
cd - # back here
docker compose up # every scenario; exits non-zero if any check fails
docker compose run --rm river # just oneThat image carries writ, the standard library where the resolver looks, and
git — which writ compare --git shells out to, so the gitcompare scenario
needs it at runtime rather than as a build tool.
The base image is named by a build ARG, so the day writ publishes one, this repository needs no change:
docker compose build --build-arg WRIT_IMAGE=ghcr.io/writ-lang/writ:0.1.0Until then, building it yourself is the one step that still wants a writ checkout. Nothing else here does.
- Create
<name>/<name>.writ(the model) and<name>.claims(the questions). Awrit comparescenario also ships a second.writ(seeoversight/oversight-repeal.writ), and where the pair is the point the files may take a stem of their own (calculation/market.writ,.../planned.writ) —writ compareandwrit queryread the claims file as a sibling of the base model. Shared vocabulary goes inlibraries/as a domain library. - Add a
<name>()function torun-tests.sh— runwrit check(andwrit compareif the scenario needs it, on absolute paths under$here) and assert the answers withhas …/near …/lacks …/exit_is …— plus a case in the dispatch and theallrunner at the bottom. - If the scenario re-asks its questions of the rules engine, add a
.rulesfile too, and compare the two answers — a disagreement is a bug in one of them, never a number to adjust. Two ways to run that comparison:modality-cross-check.shwalks the nine scenarios named in it, discovering properties from their claims files; or the scenario carries its owncross-check.sh, one line per property, naming the modality and the polarity outright (calculation/,gotha/). Prefer the second — it is a dozen lines, it reads without a parser, and it can put one rules file to two models.
- A holding
possiblenow prints its witness — the solution.possible Fasks "can this happen"; when it holds,writshows the shortest path to an F-situation, which is the answer (the river's crossing; the reading that makes abe a knight). This matches the spec's Appendix C. - The moves are named. A bare
formproduces unnamed transitions, which §10.1 renders only by position (#0,#8); aformcan't synthesise a name. So each move here takes an explicit name slot (cross-goat-LR,abe-is-knight,read-cal), and the solution path reads as prose — faithful to the named moves Appendix C's own report shows.
