Build Board

48 hours from 2026-08-03. Ordered so that the thing reviewers execute exists early and improves, rather than arriving at the end.

Verified against the tree at commit 1343f57, 2026-08-04 evening. Every status below was checked by reading the module that would implement it, grepping for the caller that would exercise it, and — for anything a reviewer would click — reading the template that would link to it. Three rules were applied: a task is not done because a related one shipped; a task whose mechanism exists but whose behaviour does not is partly, not done; and a thing that is built but that nothing calls or links to is not done. That third rule closed four rows today that would otherwise have read green.

Uncommitted work is not on this board as shipped. The tree carries untracked modules from workstreams that were still running when this check ran: app/jobs/ (a scheduler), app/sources/fetch.py (a docket fetcher), app/state/replay.py (point-in-time replay), and app/state/permissions.py with app/web/views/permissions.py and permissions.html (granular permissions). Five test files come with them. None of it is committed, none of it has been verified here, and the permissions router is not mounted in app/main.py. They are listed as in-flight in T26 and nowhere else. Nothing in the submission may describe them as built.

Read this before trusting state.json. It is generated and therefore cannot lie about what it measured — but it was last generated at commit 2b10bb6, five commits back, and it reports no test count at all because the run it sampled produced no summary line. That is ADR-60 working as designed: it says "not measured" rather than "0". It also predates the real-corpus ingestion, the role templates and the migration. This is the third time this file has been recorded as stale. The fix is one command and nobody runs it, which means the fix is a commit hook, not a command.
The three things still worth doing before anything else.
1. A user conversation (T1). 45 messages out, zero replies, four bounces. Cold outbound has been tried and has failed. What is left is a warm introduction, and it is the only task on this board that moves a quarter of the rubric.
2. Make the repository readable (T24). It is PRIVATE, confirmed with gh repo view at this commit. The history was kept whole and unsquashed because they read it. They cannot read it.
3. Connect what is already built (T27). Four screens no navigation reaches, and a first-login tour with 23 tests that has never rendered for anybody because one include line was never added. All of it is minutes of work and all of it is what a reviewer meets first.

Phase 0 — hours 0-2: the spine

#TaskWhy it is firstStatus, and the evidence
T1Send user-interview requests (LinkedIn, ex-colleagues, utility regulatory affairs). Target 3, expect 2. Script in docs/user-research.htmlExternal latency. Everything else is under your control; this is not. failed, and closed as failed. 45 messages went out — 7 on 2026-08-03, 38 through 2026-08-04, paced and one per employer first. Zero replies. Zero interviews. Four bounces, every one an address read off a certificate of service and labelled filing-verified in our own log, which is where ADR-27 and best-practices principle 28 came from. docs/mrd.html now carries the outreach log, and docs/user-research.html still shows three empty interview rows, which is the correct state for a table with nothing in it. What exists instead: docs/synthetic-user.html and docs/synthetic-interview.html, built from the 102 real filings, labelled SYNTHETIC in their own titles, with a rule written into both that no line may move into user-research.html. That is a rehearsal instrument. It is not this task.
T2make run and make test exist and work on a clean checkoutReviewers execute both before scheduling a panel. If this breaks, nothing else is read. partly, and only the last mile is left. All fifteen routers are mounted in app/main.py, tests/test_app_wiring.py derives its questions from app.routes rather than a hand-written list, and the application is deployed and was walked screen by screen over the internet. make run, make test and make eval each ran at the commit that describes them in the README. Measured here, and it went red and then green inside one hour: the committed suite over this working tree gave 1,379 passed, 2 failed, 2 xfailed, 1 xpassed in 6m16s. Both failures were guards catching the uncommitted work of T26, not defects at HEAD — tests/test_retention.py::test_nothing_in_the_product_calls_any_of_this_yet reported that app/jobs/runner.py now reaches the retention module and that deploy/site/privacy.html must stop saying nothing runs these, and the schema guard found tables carrying no retention window, reached because tests/test_app_wiring.py walks app/web/views/ with pkgutil and imports the permissions view. Both were repaired while this board was being written — privacy.html and test_retention.py were edited and the pair passes now. That is the state of a moving tree, not a result: the whole suite has not been re-run since, and make fresh-check has still not been run at any commit. It clones HEAD into a temporary directory and runs the suite there, which is the only check that says whether HEAD alone is green and the only one that answers "does this work for someone who is not you". Until it runs, the contract is asserted rather than tested.
T3Synthetic corpus: one proceeding, three versions with deliberate material and immaterial edits; plus a company context of ~8 obligations, 3 projects, 4 documentsEvery later task needs data with KNOWN answers, so tests can assert against ground truth. done. data/v1..v3, data/company_context.json, and data/manifest.json carrying 11 change spans and 9 boilerplate spans, computed independently and re-read against the bytes. make eval prints the SHA-256 of each version it scored, so a verdict is bound to the bytes that produced it. Per ADR-40 this stays the eval corpus and does not become the demonstration corpus — see T26a.
On the corpus: the deliberate edits are the demo and the test fixtures at once — one material, one cosmetic, one appearing only in the final version, one moving a deadline.

Delivered, plus one that turned out to matter most: a section restructure (Section 6 becoming subsection 5.4) that broke the diff's confidence scoring on first contact, and one sentence repeated nine times, which is what makes the wrong-occurrence citation failure testable rather than theoretical.

The limit found later: the manifest labels five changes, not every change. That is enough for recall counts and not enough for precision, because there are no negative labels. make eval refuses to print a precision figure rather than building a denominator by hand.

Phase 1 — hours 2-12: the hard bet, working

#TaskNotesStatus, and the evidence
T4Ingestion + passage segmentation with stable char offsets; SHA of source retainedOffsets are the foundation of citation verification. done, and it now carries provenance. app/ingestion/ingest.py, source_sha256 and source_url on DocumentVersion, so a claim drawn from a real filing links back to the commission's own copy. tests/test_ingestion.py asserts against the manifest.
T5Citation verifier: given (version, start, end, quote), re-read source and confirm. Normalisation rules explicit and testedADR-03. The depth area. done, and six defects inside it are closed with guards. Equality after normalization, never similarity; a repeated quote must name its occurrence; _spans_of runs over a normalized projection with an index map back to raw offsets (finding 5); a character is protected from folding by what the fold does rather than by its decomposition tag, which closed 150 characters a tag list could not express (finding 6, ADR-26); soft hyphens and hyphenated line breaks handled over all seven Unicode line boundaries (finding 7, ADR-25); the version-aware form loads the version through the tenant chokepoint (finding 8) and now re-checks source_sha256, so a verdict is bound to the bytes and not to an id (ADR-27). Two more found by fuzzing 60,000 generated strings after the suite was green: an empty raw span for a collapsed space, and a span wider than the quote that shifted every later occurrence index (findings 12 and 13). docs/.ai/findings.html now agrees with the code on all of them.
T6Deterministic version diff producing typed changes with passage referencesADR-04. Unit-testable against the corpus's known edits. done, and now exercised on a real filing. app/diff/engine.py; alignment confidence is capped when the section label disagrees, so the restructure escalates instead of scoring 0.944. tests/test_diff.py, 11 tests, plus the diff-completeness metric in make eval. On Kentucky PSC 2025-00113 it finds 144 changes across 1,024,409 characters in 0.78 seconds.
T7Draft-vs-final handling threaded through change recordsADR-05. Separate code paths, separate action vocabulary. done. app/interpretation/action.py holds the vocabulary and the effective-date rule; Change.status is copied from the to-version at write time, so the status a decision was taken under stays beside it. tests/test_draft_final.py, plus the draft-versus-final routing metric in make eval.
T8Multi-tenant isolation CI testADR-07 and ADR-29 promise company_id as a chokepoint, verified by test rather than by inspection. done, and it is the task with the sharpest lesson attached. tests/test_isolation.py passed while passage_refs read passages with no company scope at all, because no test asked that question (finding 1). tests/test_passage_isolation.py closes it; tests/test_screens.py::test_every_route_refuses_another_tenant extends the question to the web layer and tests/test_app_wiring.py::test_the_whole_app_answers_to_one_tenant to the assembled application, where it caught the tenant being answered by two different functions.

Phase 2 — hours 12-24: change to action

#TaskNotesStatus, and the evidence
T9Obligation extraction with evidence links; every extracted obligation carries verified citations or is escalatedExtraction is the model's job; verification is not. partly, and the half that landed is not the half the task names. What exists: an Obligation table in app/state/models.py, ensure_obligation and owner resolution in app/state/routing.py, and eight obligations from data/company_context.json seeded with owners resolved to real accounts. What does not: nothing extracts an obligation from a document. The obligations are read out of a context file a person wrote, and the seeding is done by scripts/seed_demo_gaps.py, which make seed does not call — so a fresh make run has the table and not the rows.
T10Impact mapping: change → affected obligations / projects / documents, with confidenceThe product's core join. not done. map_change_to_obligation() exists and records a mapping, refuses one that crosses a tenant boundary, and insists that a mapping a person made and one a pipeline proposed carry different author kinds. Nothing computes a mapping. The function has no caller in app/ outside its own module, so nothing has a confidence to report, and the seed reads the change-to-obligation mapping straight out of the manifest.
T11Action recommendation + reviewer routing by roleDraft → monitor/comment. Final → comply, with effective date. partly, and it moved a long way today. The vocabulary half is built, tested and scored. The routing half now exists in the library: resolve_change_owner, resolve_escalation_owner, route_escalation and unroute_escalation in app/state/routing.py, with a named failure code per reason and a shared queue that re-derives the reason on every read (ADR-36, ADR-56), plus ten role templates drawn from real filing titles where no template holds both action.propose and action.approve. What is still missing is the connection: no view in app/web/ imports routing except invite_accept.py, and the only production callers of route_escalation are in app/state/invites.py. The assistant can answer who owns an escalation; the review screens cannot show it. Nothing turns a change into a recommended action.
T12Confidence thresholds and the escalation queueADR-06. Below threshold never enters project state. done, with one number stated honestly. Escalation rows carry a reason code for branching and reason text for the analyst, and app/state/claims.py withholds rather than shows. The threshold is configuration per ADR-06 — MIN_CONFIDENCE_BP, read from the environment — and it ships at 0, so nothing is withheld for confidence alone today. Deliberate: there is no evidence for where the line sits, and a number chosen to look rigorous is worse than an honest zero.
T13Project state + audit history; rollback to a prior stateDecide event-fold vs snapshots and record the choice. partly, and the missing half is now named precisely. Project state and the audit chain are built, hash-linked, and carry actor attribution across three digest schemes that verify end to end without ever re-hashing an old row (ADR-17, ADR-53). Reversal is built: revert_event in app/state/rollback.py writes a new row pointing at the row it reverses, reopens the escalation where that applies, and taking back a reversal reinstates nothing (ADR-54). Point-in-time restore is not builtapp/state/replay.py is untracked and unverified and does not count. The event-fold-versus-snapshots question is settled by ADR-16, ADR-17 and ADR-31 and is no longer open.

Phase 3 — hours 24-34: the surface

#TaskNotesStatus, and the evidence
T14Analyst workspace: proceedings, changes with materiality, and the action queueSimple enough to use without a walkthrough. That is the stated bar. partly, on the same count as before. The project list, proceedings list, version timeline and change detail all render and answer 200 on the assembled application. Materiality is still NULL on every change. A model would fill it and nothing on the claim path calls a model, so the screen shows the field's absence — MATERIALITY_UNASSESSED — rather than a default that would read like a judgement. The bar itself is now unmet in a second way: the first-visit walkthrough written to answer "usable without a walkthrough" is not included in base.html. See T27.
T15Citation visible in the UI: click a claim, see the exact source span highlighted; unverified claims held in review with the reason shownThe prep guide requires the hardest technical decision to be visible in the product. This task IS that requirement. done, and checked by starting the real application rather than by reading the suite. /changes/{id} carries the paired demonstration: one claim whose citation verifies and opens the stored source scrolled to the cited offsets, one whose quote was corrupted and never renders as a claim at all. The panel is server-rendered, so it works without a round trip and without a framework. The strongest guard is tests/test_change_view.py asserting a withheld claim's statement string is absent from the response body — not greyed out, absent — because withheld claims render from a dataclass with no statement field. New today: a claim drawn from the real Kentucky filing carries a source link back to the Commission's own PDF, which is the thing a regulatory reader actually wants from a citation.
T16Reviewer view: escalation queue, approve / reject / amend, and the audit trail behind each partly, and the sharper caveat is the one to read. /review and the review centre both answer 200 and no longer collide on a path (ADR-23). Approve and reject each write an audit event whose chain still verifies, refuse an unsigned resolution, refuse a rejection with no reason, and conflict on a double resolve. Amend is not built — ADR-13 permitted that cut in advance. The caveat is answered (ADR-91), and not on this screen. can_approve — the segregation-of-duties control the whole approval model rests on — had no caller in any view, and now has exactly one: app/web/views/actions.py::decide. It does not belong here. Resolving an escalation is the analyst's own job and gates on escalation.resolve; approving the ACTION that follows from a claim is a different decision by a different person, and it needed a screen that did not exist. /actions is that screen. Two attempts to put the gate on this one were reverted, because gate 2 demands action.approve and the analyst does not hold it. tests/test_actions_screen.py proves the refusal end to end; the strict xfail that carried the gap is gone. One more turn of the same screw, found by review. The control was wired to a screen and the state that makes it refuse was reachable from no screen, so four documents told a reviewer to arrange it at /users, which cannot. VERBATIM_DEMO_REFUSAL=1 on the seed arranges it as the system actor and says why nothing else can; tests/test_seeded_refusal.py walks it.

Phase 4 — hours 34-42: evidence it works

#TaskNotesStatus, and the evidence
T17Eval set: labelled obligations over the corpus; report precision and recallNumbers you can defend beat adjectives you cannot. partly, and read the split rather than the headline. The harness exists and gates: make eval scores five metrics against data/manifest.json with no model, no network and no database, and exits 1 on a blocking failure. It now has its own tests, which it shipped without. What the task asked for is still absent: the eval set is over the deterministic spine, not over labelled obligations, because T9's extraction does not exist. Precision is not reported and cannot be — the manifest labels five changes, so there are no negative labels. New, and it is the next real measurement: three real version families carry the filer's own marked-up copy — ground truth nobody on this side wrote — and nothing scores against it. Until something does, "the eval set is self-labelled" stays a live concession.
T18Citation-verification pass rate, and a deliberate-corruption test proving the verifier rejects a fabricated quoteThe single most persuasive demo in the submission. done, including the repair that made the rule real. The corruption tests pass — a fabricated quote at real offsets, offsets past the end, negative and inverted offsets, empty spans, and the ambiguous-occurrence cases. make eval reports both metrics with their denominators. The scorecard once printed "20 of 20 offsets, 100%" where only 10 were distinct strings and 9 were one repeated sentence; the sample count is now derived by de-duplicating on the identity that makes two probes the same evidence, rather than passed in by the caller with an interest in a big number (ADR-39).
T19Failure-mode notes: where retrieval misses, where diffing breaks on restructuring, where the model misreads"Where modern AI fails and how you designed around it" is an explicit evaluation dimension. done, in four places that now agree. docs/tdd.html holds the failure modes and the corruption bar; docs/.ai/findings.html holds thirteen defects with how each was found and seven judgements with their reversal conditions, and no longer calls closed findings open; docs/submission.html holds the defects found since; and make eval prints a caveat naming what its numbers cannot support.

Phase 5 — hours 42-48: the documents and the submission

#TaskNotesStatus, and the evidence
T20Fold user-interview findings into the PRD and the prototype. What they misread, what they ignored, what you changedThe PRD is explicitly read for how user feedback shaped the build. blocked on T1, and T1 has failed. Nothing to fold. docs/user-research.html keeps hypotheses H1 to H3 open and falsifiable, which is the right shape and is not evidence. The trap to avoid in the remaining hours: the synthetic user is articulate and quotable, and moving one of its sentences into the PRD as a finding would do the submission more damage than the empty table does. Both synthetic documents say so in their own text.
T21Finish PRD and TDD; confirm the prototype tells the same story they doReviewers read PRD and TDD first, then verify against the code. partly, and the reconciliation debt has shrunk rather than grown. Of the five documents that disagreed with the code, four are fixed: the README now describes the build it has, findings.html agrees with the verifier, mrd.html carries the real outreach numbers, and the decision log runs to 63 entries with five earlier ones corrected in place. What is left: state.json is stale again, five commits back; ADR-59 still says the real corpus is wired to nothing, which scripts/ingest_real.py made false this morning; and the application footer tells every screen "the corpus is synthetic" while a real Kentucky filing can be loaded behind it.
T22README with the exact run and test commands; verify on a clean cloneDo this on a fresh clone in a temp directory. Not "it works on my machine". partly, and the first half is now good. The README carries its own line count with the command beside it, names what is built, and — the part that matters — names what the code does not do under its own heading: nothing fetches on a schedule, retention is written and not running, there is no point-in-time rollback, zero user interviews. Three earlier claims that had gone false in the flattering direction were corrected. The clean-clone verification has still not been run. That is T2's last mile and this row does not close without it.
T23Fill docs/submission.html: what you built, what you reused, what the AI wrote and what you rewrote or rejected, what broke and how you debugged itKeep this current as you go. partly, and it needs one more pass. Assembled from primary sources and re-verified twice, which the page states rather than claiming it was kept live. Missing since the last pass: the coverage argument in full (refused, overruled, measured, cut), the assistant that shipped disconnected, the migration that would have taken the live site down, and the real-corpus retrieval run with its anti-fabrication rule. Each is a strong row and each is currently only in a commit message.
T24Repo public or reviewer access granted; commit history intact, NOT squashedThey read the commit history. It is part of the submission. blocked, and it is the cheapest blocker on the board. History is intact and unsquashed — 43 commits, each message carrying its reasoning and its cost, which is a real part of the argument. gh repo view at this commit returns "visibility":"PRIVATE". Every hour spent writing those messages is worth nothing until this changes. ADR-43 chose private as the starting state deliberately; nothing has moved it.
T2590-second pitch written and said out loud: who, what, why, what is hard about itRequired for the panel; also a good test of whether the product has a point of view. rewritten, not rehearsed. The pitch in docs/.ai/briefing.html was rewritten at this commit — the routing sentence changed because routing now exists, and the Kentucky pair replaced an abstraction with a number. It has not been said out loud or timed. Ninety seconds is shorter than it feels.
T26Committed 2026-08-04, after the suite proved itHeld out of every document while it was uncommitted, so nothing described it as shipped before it was. committed, with ADRs 66, 67 and 68 covering the three of them. app/jobs/runner.py with scripts/run_jobs.py — the scheduler that open question 5 asks for, and the answer to "nothing runs the overdue sweep". app/sources/fetch.py — a docket fetcher, which is what "nothing fetches on a schedule" is about, and which open question 6 says is a sketch until it exists. app/state/replay.py — point-in-time replay, the missing half of T13. app/state/permissions.py, app/web/views/permissions.py and permissions.html — any permission to any person, custom roles and a conflict report rather than prior restraint. Status of each is the same: untracked and uncommitted. The permissions router was unmounted when this board's check began and was mounted in app/main.py, still uncommitted, before it ended — which is how fast this tree is moving and why nothing here may be described as shipped. Five test files ship with them and are excluded from the counts on this board. They turned the committed suite red for an hour — see T2 — and both failures were guards doing exactly what they were written for: a scheduler that runs retention makes a sentence on the privacy page false, and a new table with no retention window is a table nobody decided how long to keep. Neither was a defect to silence; each was a decision the landing commit had to take, and both were taken.
T26aReal corpus: retrieve, verify, load one pair, label both corporaNot on the original plan. It landed today and it changed what several documents may claim. partly. Done: 102 filings from eight commissions with provenance beside each — source URL, docket, title, filing date, filer, retrieval time, SHA-256 — retrieved under a rule that an empty haul is a successful run and a fabricated one ends the project, then checked by a sceptic that re-fetched, re-hashed and read for the absence of extraction artefacts (ADR-58, ADR-59). All 102 hashes recompute. 62 documents sit in version families. scripts/ingest_real.py loads the Kentucky 2025-00113 pair and diffs it. Not done: make seed does not run it, so a reviewer following the README sees the synthetic corpus only; the second-corpus mapping ADR-40 promised the MRD is unwritten; and nothing measures against the three filer-published redlines.
T27Connect what is already builtThe recurring failure of this repository, and no test catches it. not done, found by this check. The first-login tour has never rendered for anybody. _tour.html names the single line base.html must add and says which include it must follow and why; base.html includes _clerk.html and nothing else. 23 tests pass because they read the template file and the served assets, not a rendered screen. Four screens have no way in: the user register (/users), share administration (/admin/shares), invitations (/admin/invites) and Integrations (/admin/sources) are mounted, and no template a signed-in person can reach through the six-item navigation links to any of them. The approval-route editor is the exception, because workflow_route.html links to it. The guard to write with the fix: tests/test_app_wiring.py derives "is it mounted" from app.routes. Nothing derives "can a person get there", and these are two questions.

Phase 6 — after the technical interview of 2026-08-10

Five gaps an outside reviewer named, worked on five branches at once and merged 2026-08-11. They are I1 to I5 in docs/.ai/gaps.html and they are numbered T28 to T32 here. The eight decisions they produced are ADR-96 to ADR-103, written after the merge because five concurrent worktrees could not share this file — which is ADR-61 working, not a lapse. Read T31 first. It is the only row on this board whose honest status is that the work was built, measured and then taken back out, and the measurement is the deliverable. And read the two new standing risks. Three rounds of review on this batch found published numbers that did not reproduce from the code, five of them in one module. That is this repository's own cardinal sin, committed inside the change that answered an interview about rigour.
#TaskNotesStatus, and the evidence
T28I1 — answer "ten thousand concurrent multi-million-word documents" with a measurement rather than a constraint listThe question a procurement conversation asks first. ADR-96, ADR-97. answered, and nothing is built. docs/scalability.html carries the curve from scripts/bench_diff.py and section 4 designs the pool, the queue and the admission check. The reviewer's premise was wrong on this codebase and that is the useful finding: normalization is 87% of the diff call and the list-level matcher 1.5%, so the bottleneck is a per-character Python loop rather than concurrency. Partitioning was measured and rejected — index partitioning turns the real Kentucky pair's 144 changes into 777. Still open: no pool, no queue, no admission check, and the binding limit is one bad document rather than ten thousand of them. And it turned up a live defect: finding 17, the alignment score taking difflib's junk default on every long passage, one paragraph in 73 mis-flagged after a one-word edit. Held open on purpose by ADR-97, because the fix rewrites a stored column and needs a corpus reload beside it.
T29I2 — a truncation policy, written before the feature that would need itThe interview asked for context management. The code inverted the request: there is no long session to manage. ADR-98, ADR-99, ADR-100. done. app/chat/budget.py holds one ordering — the question, the citation spans and the diff offsets never evicted, turns oldest first and contiguously — and refuses the turn rather than shaving a citation to fit. tests/test_history_needs_a_budget.py walks the syntax tree of app/ and fails the day a value derived from the conversation reaches a model call without going through it, which is the difference between a rule and a preference. Multi-turn is still unbuilt, deliberately. Conceded: characters, not tokens, because no offline tokenizer is a dependency; the tripwire matches by name; and the tool-round budget is a second policy nobody has written.
T30I3 — the eval becomes a gate, and the model path gets scored at allAbsorbs P2-18, which the scorecard had named against itself. ADR-101. done. app/evals/model.py, 26 cases over four metrics, one of them blocking at a threshold of zero. Exit 2 means the model was not evaluated and is never a pass; .github/workflows/ci.yml runs make test and make eval on every push and requires the dry run to exit 2. It is a third harness rather than a sixth metric because make eval opens no socket and must keep not opening one. The limit to say out loud before anybody asks: nobody has run it with --send. Every outcome path is exercised by a deterministic fake. Found by review: the harness could print RELEASE BLOCKER and still tell the workflow the run was clean, because nothing tested what main() returned.
T31I4 — a parsed document tree instead of a section number standing in for structureThe largest of the five. The one that came back as a negative result. ADR-102. built, measured, and not wired in — and that is the finding. app/diff/structure.py parses a filing's own numbering where it can and refuses with a reason where it cannot: 27 of 102 real filings yield a tree. The proxy is as bad as feared — the leading integer fires on 42,546 of 107,122 passages and 112 of 421 modified changes escalate because a printed gutter number shifted by one. Then the signal was wired in and measured. The two branches it was built for fired zero times each. All 112 movements came from a branch nobody authorised: the parser failing to read both documents, and that failure withholding the restructure cap — absence licensing certainty, on the one product that exists to refuse it. It lifted a real Missouri stipulation's two paragraphs from 0.50 to 0.9877 on evidence taken entirely from the other document in the pair. The wiring came out, the parser and the measurement stayed, and tests/test_structure.py holds that nothing under app/ imports it. The cost, stated: 1,200 lines of application-shaped code with no caller in the application, and 112 alarms still in the queue that this work established were the wrong instrument to remove.
T32I5 — a reviewer can answer the low-confidence alignment flag, and the answer is keptHalf existed: the caution reached a screen and no decision. ADR-103. done. Measured first — 16 of the 27 seeded changes carry the caution and 8 sit at exactly the ceiling, so the product asked for judgement on more than half its own corpus and kept none of it. Three verdicts, filed as audit rows under their own action codes, never a column and never a correction of the number; two of the four states keep the caution and two lose it, decided by one predicate the screen and the chat clerk share. Answering makes the answerer an author, so they can no longer approve what follows. Found by review before merge: a pairing somebody had confirmed still printed "read the pairing above as a guess" four lines above the badge carrying that person's name, while the clerk said nothing about the pairing at all — one record, two surfaces, two accounts of it. Left open on purpose: no control is drawn on a pairing the diff was confident about, which is exactly the unlabelled renumbering the cap cannot see.

Where the hours went, against the plan

Phases 0 and 1 — the corpus and the deterministic engine — came in close to plan and are done, with ten defects closed inside them and the last two found by fuzzing code that already looked finished. Phase 2 split, and the split moved today: the state and audit machinery landed early, the model-dependent half (T9, T10) still has not, and T11's routing arrived as a library that no screen calls. Phase 3 was rebuilt wider than planned after the reference notes landed, and the bill for that width is now legible — 21 screens, four of them unreachable. Phase 4 gates. Phase 5 is where the debt sits and it is no longer mostly documents: the two open items are a private repository and a clean-clone run.

The cheapest remaining work, in order: make the repository readable (T24, one click); add the _tour.html include and put the four admin screens behind a link (T27, minutes each); run make fresh-check (T2, T22); run make status; correct the three prose claims listed in T21. Only T27 is engineering. All of it changes what a reviewer sees before they read a line of code.

Cut list — if time runs short

Sacrifice in this order, and say so in the submission rather than hiding it: multi-jurisdiction taxonomy, point-in-time restore (keep audit history and reversal, drop the replay), the reviewer role model, UI polish. Never cut: citation verification, the deterministic diff, draft-versus-final, or the user interviews. Those four are the submission's argument.

Cuts actually taken: amend in the review queue is cut (T16), which ADR-13 permitted in advance. Multi-jurisdiction taxonomy was never started. The 99% line-coverage target was cut on cost after being asked for, refused, and then granted — ADR-38 carries all four moves, and what survives is a measured 92% reported as a fact rather than adopted as a standard. Two cuts came back: the evals were on this list and the harness shipped anyway; rollback was cut and reversal shipped. A cut that reverses quietly is the same failure as a decision nobody wrote down, so both are recorded here. The user interviews are on the never-cut list and no interview happened. Forty-five messages went out, which is an input and not a result. That is the one cut nobody decided to take, which is the worst way to take one.

Standing risks

RiskMitigationHow it played out
No user interviews land in timeAsk on the first night; offer 20 minutes at their convenience; if only one lands, say so honestly and report what it changed.Materialised, and it is now final rather than pending. Nothing was asked on the first night — the mitigation was written and not executed. 45 messages later there are no replies and four bounces. Cold outbound is exhausted; only a warm introduction is left, and the clock has roughly a day in it.
Scope creep into a platformThe cut list above. One user, one workflow, one depth area.Materialised, deliberately, and the bill is now countable. 21 screens, 69 application modules, 99,099 lines. The rule that makes breadth defensible holds — every surface that synthesises states what it withheld at equal weight. The cost is T27: four screens nothing links to, and a walkthrough nobody has seen.
Prototype does not start on a reviewer's machineT2 early, a clean clone, no service dependencies (ADR-07).Materialised three times, and the third was found off the test path entirely. First the app served only /healthz; then it 404ed on its own navigation; then the deploy would have started against yesterday's schema and answered no such column: document_versions.source_url on every screen. No test could have caught the third — tests build their schema from the current models every time — so app/state/migrate.py derives the difference between what the models declare and what the database has, refuses a NOT NULL column with no default, and exits non-zero so a deploy stops rather than starting broken.
Docs written last, contradicting the codePRD and TDD updated as decisions are made. The ADR log is the running record.Materialised in both directions now. For a day the documents understated the build. Then the real corpus landed and three pages that said "every document here is invented" went false in one commit — the same defect pointing the other way. Four of five contradictions are fixed; the three named in T21 are not. The pattern worth keeping: the application cannot lie about itself, because tests/test_app_wiring.py derives from app.routes. Prose has no equivalent, and every prose correction today was found by a person reading.
A passing suite that never asked the questionAdversarial review and fuzzing of the wiring and the data inventory, not only of the code under test.Five instances now. The unscoped passage read; the unmounted routers; the same one layer up; two defects a fuzzer found in a green module; and today the tour with 23 tests that has never appeared on a screen. The lesson has not changed and neither has the fix: derive the question from the program, never from a list a person maintains.
Built and not connectedAsk of every finished feature: what calls it, and what links to it.The dominant failure of this build. Today alone: the assistant included nowhere, then wired to an engine under a different name; .env never loaded, so every model path announced its fallback while holding a valid key; migrate_audit_schema exported with no production caller; revoke_all_for_user with no caller; can_approve with no caller (ADR-91 gave it one, and it needed a whole screen rather than a line); the tour; four admin screens. None of them failed a test, because a test proves a thing works, not that anything uses it.
A fix that depends on somebody rememberingTurn the command into a hook. If the correction is "run X", X belongs in the commit path, not in a document.state.json has now been recorded as stale three times, and the fix has been "one command" every time. It is currently five commits behind and reports no test count at all.
A published number that does not reproduce from the codePut every published figure in one constant, re-derive the constant from the data in a test, and let prose name the key rather than repeat the value. Where the figure is "what this rule bought", measure it by deleting the rule from the shipped source, not by re-implementing it with the condition flipped.The dominant failure of the interview-gap work, 2026-08-10 to 11. Four consecutive reviews of one module found one each: a candidate count published as 1,164 that measures 1,212, a gutter fraction published as 0.20 that measures 0.35, a run published as 27 that measures 21, a heading count published as 113 that measures 98, and an offset the parser never produces. Elsewhere in the same batch, three comments quoted a millisecond figure for one database read that a reviewer got as 2.3188 and a third machine as 2.7059, and an eval's span floor claimed a derivation that did not reproduce. Every one was checkable in seconds by anybody who ran the thing, every one had been copied from a code comment into a test comment and an HTML page so one wrong number became three, and every fix corrected the sentence it was handed and left the rest standing. Closed as a class rather than as instances: app.diff.structure.MEASURED holds 91 figures, four guards in tests/test_structure.py fail if the page, the dict or the module's own comments disagree, the millisecond figures were deleted in favour of scripts/measure_audit_subject_read.py printing the query plans that the argument actually rested on, and the span floor's claim was deleted rather than reworded. This is the repository's cardinal sin and it happened repeatedly inside a change answering an interview about rigour.
A guard that would pass without the thing it guardsBreak the rule and watch the test go red before the test is believed. Every new guard gets a mutation run, and the harness is committed so the run is a number a reader can reproduce rather than a promise in a docstring.Three instances in one batch, and one of them lied about being measured. The model-path scorecard's main() could be replaced with return 0 and the whole file stayed green — a harness printing RELEASE BLOCKER and reporting a pass to the workflow reading it — and its only mitigation for a run where almost nothing answered could be deleted from the page. The context tripwire claimed every clause of its rule was reached by a case; the claim was false three times running, six clauses, then eight, then three. The mutation pass that "proved" it had read stale bytecode: CPython validates __pycache__ on mtime and size, mtime has one-second granularity, and a same-length edit applied and reverted inside a second reuses the previous run's bytecode. scripts/mutate_context_budget.py is committed, clears every cache around every run, and ADR-100 makes its printed line the only coverage claim a docstring in that area may make.