Recall card

Read once, twenty minutes before the call, then shut it. The names and line numbers below were read off the code on 2026-08-07 at commit 65c42d7. Five branches landed on 2026-08-11 and the sections marked with a date of that day were re-checked then; anything still stamped 2026-08-07 is older than the tree. The suite was re-run rather than recalled on 2026-08-11: 2,785 passed, 1 xfailed in 350.73 seconds, exit zero, which is the number docs/.ai/state.json carries.

Check the suite before the call, because one test is a clock and it fired. tests/test_invites.py::test_an_invitation_cannot_land_work_that_rests_on_a_proposed_mapping passed at 08:5x UTC on 2026-08-11 and failed at 09:19 with nothing committed in between. It pins T0 = 2026-08-04 09:00 UTC and mints an invitation with the default seven-day life, so the invitation expired at 2026-08-11 09:00:00 UTC; its last assertion calls shared_queue, which takes no now and re-derives against the real clock, so the queue answers ROUTE_OWNER_INACTIVE where the test wants ROUTE_MAPPING_UNCONFIRMED. If it is still red, say what it is: the product is behaving correctly — a queue must say what is true now — and the test hands one function a different clock from the rest of the fixture. It is a good answer to "what would you fix first", and a bad thing to be surprised by on a share-screen.
What landed on 2026-08-11, in the order it is worth raising. Six things, and every one of them carries a limit that should be said in the same breath. Raise them in this order; the first four are answers to "where is the depth".
  1. The diff path is measureddocs/scalability.html, from a committed script. Linear to 52.6 million characters, quadratic on a run of repeated lines, 3.07–3.14 bytes of heap per source character for the whole pipeline call. The limit: nothing was measured under concurrency at all.
  2. The model path has a scorecardapp/evals/model.py, make model-eval. Fabrication and misses counted apart. The limit: the hardest case rests on three cases, and the harness spends money so it never runs on a push.
  3. There is CI, and there was none before. make test and make eval on every push. The limit: nobody has timed it on a runner and nothing deploys from it.
  4. A document-tree parser was built, measured and withdrawn — a negative result, and the best answer here to "where does modern AI fail". 27 of 102 filings parse; the branches it was built for fired zero times.
  5. A reviewer can answer the diff's doubt — confirm, dispute, or cannot tell, hash-chained, never overwriting the computed number.
  6. A context budget for a feature that does not existapp/chat/budget.py, uncalled on purpose, with an AST tripwire that fails the suite if history reaches a model any other way.
How to use this. Sixty minutes, voice, your repo open. They ask for exact files, functions and commands and reconcile the answers against the repository afterwards. This is a map, not a script — a memorised answer sounds memorised and the hour is built to catch that. If a name will not come, say "let me look" and look. A confident wrong filename is the worst outcome in the hour, and "I do not know" is a positive signal in their own guide.

1. The names

Five files decide everything in this product. Everything else is a screen or a table.

What it decidesFileFunction and line
Is this quote really there?app/verification/verifier.py 262 linesverify_citation — 151
Which claims may be shown?app/state/claims.pyverified_claims — 502
Whose data is this?app/state/queries.py_require_scope — 60; row_for_company — 113
What happened, and can we prove it?app/state/audit.pyrecord_event — 581; verify_chain — 734
What changed between versions?app/diff/engine.py 154 linesdiff — 78

The whole product in one sentence. diff finds what changed; a claim is written about it carrying exact character offsets; on every read verified_claims calls verify_citation, which re-reads the stored source at those offsets, and if the words are not there the claim is withheld and the reason takes its place. Every decision lands in audit.py as a hash-chained row. Every read passes _require_scope first.

The sizes are the answer to "where is the depth". The verifier is 262 lines and the diff engine 154 — the two things the whole argument rests on are the two smallest files here. The big ones are big because they are dull: models.py 3,861 is the schema, invites.py 3,377 invitations, chat/tools.py 2,673 the assistant's tools, state/workflow.py 2,471 the approval route. None decides whether anything is true.

The walkthrough they are most likely to ask for

"Show me what happens when I open a change."

#What happensWhere
1GET /changes/{change_id} arrives at change_detailapp/web/views/changes.py:902
2The tenant comes from the signed-in person, never from a parameter: company_id = Depends(current_company)app/web/deps.py
3change_for_company — another company's change is a 404, not a 403: "no such change" is all the asker is entitled to knowapp/state/claims.py:369
4verified_claimsthe gate, and it runs on the readapp/state/claims.py:502
5Per claim: citation first, confidence floor second. Deliberate — a claim that misquotes should report the misquote, not a confidence numbersame, 555 then 578
6verify_citation re-reads the stored source at the offsets the claim namedapp/verification/verifier.py:151
7Returns (verified, withheld). A withheld statement never enters the HTML — WithheldClaim has no statement field, and _withhold (608) is the only place it is droppedsame
8materiality_for_company asks the model whether the change matters, and its citation passes the same verify_citation. shown_materiality_for_company runs that same gate for every screen that shows the verdict without earning onechanges.py:1014, propose.py:1102, propose.py:1246
9Which obligations it bears on, and who owns themapp/state/mapping.py, app/state/routing.py

The four checks inside verify_citation, by name. "What are your failure modes" is a likely question and these are the answer.

  1. REASON_OUT_OF_RANGE — the offsets fall outside the source.
  2. REASON_EMPTY_SPAN — either side normalises to nothing.
  3. REASON_QUOTE_MISMATCH — the quote does not match at those offsets. The fabricated quote.
  4. REASON_AMBIGUOUS_OCCURRENCE — the words appear more than once and the claim did not say which, or said the wrong one. The check nobody expects; the corpus repeats a sentence on purpose to catch it.

Two more sit above them: REASON_VERSION_UNREADABLE, and REASON_VERSION_CHANGED — which re-checks the stored text against the hash recorded at ingestion, because a fact verified in September is not a fact verified today.

The rest of the names

FileNameWhat it does
app/verification/verifier.py_spans_of, _covers_whole_charactersEvery raw span whose normalized text equals the quote. Refuses a span starting or ending part-way through a character expansion — a ligature, a squared-metre glyph. It re-reads what it refuses and never re-reads what it clears.
app/state/audit.pyDIGEST_V1/V2/V3, _digest_for_row (438)Scheme 1: ten fields, frozen. Scheme 2 adds actor, session and ip — attribution inside the hash, not beside it. Scheme 3 adds reverts_event_id. Current is 2; 3 is used only for a row carrying a reversal pointer. _digest_for_row refuses a v1 or v2 row that carries a pointer: a field the chain does not defend is not evidence.
app/diff/engine.py_alignment_confidence (53)A section-number disagreement caps confidence at 0.5. Text similarity alone runs high exactly when structural identity has changed, which is the dangerous direction.
app/state/search.pysearch_passages (706)SQLite FTS5 with bm25. Four checks before the index is believed, then a fingerprint check on every hit. One disagreement sends the whole answer to the scan rather than being dropped from it.
app/interpretation/propose.pyjudge_materiality (638)The one model call. change is what the model sees; sources is the whole version text the gate reads and the model never sees. It can only cite what it was handed.
app/interpretation/propose.pytransport_from_environment (387)Only ANTHROPIC_API_KEY counts, narrower than the SDK's own search, so "is the model path on" is answerable while rendering a page. Model pinned in code as claude-opus-5.
app/config.pyload_env (43)Loads .env into the process, from app/main.py lines 23-25, above the other imports. See story one.
app/pipeline.pyingest_and_diff (118)Ingest a version, diff against the last, write the change rows. Deterministic end to end.
app/auth/policy.pycan_approve (531)Reads authorship out of the audit chain and refuses self-approval. Called from exactly one place, app/web/views/actions.py::decide (ADR-91), which honours the Verdict and answers 403 with the sentence the gate produced. It had no caller at all until that screen existed.
tests/test_app_wiring.pytest_every_router_in_the_views_package_is_mounted (244)Reads app.routes and compares against every views module declaring a router. The application cannot lie about itself.
tests/test_tenancy_derived.pythree sweepsRead sweep: every public app.state function taking company_id, called with a foreign scope. AST pass: reads the source, so it catches a filter whose deletion no test can feel. HTTP sweep: app.routes on the assembled app, signed in as one company.

Commands, exactly

CommandWhat it does, and what it printed today
make runInstalls, seeds, serves at http://127.0.0.1:8000. No Docker, no database service, no build step. Five seed steps in order, and build_index.py must be last.
make test2,785 passed, 1 xfailed in 350.73 seconds, re-run 2026-08-11. Agrees with docs/.ai/state.json, which is generated by running the suite and is authoritative over every document here.
make evalOffline. Prints "5 of 5 metrics pass", exit 0. No model runs and no network call is made on this path, and the scorecard says so in its own output.
make model-evalThe model path, built and printed, nothing sent. It exits 2 and that is the pass — 2 means the model was not evaluated, which is exactly true of a run that sent nothing. make model-eval-send is the one that spends money, and it is a separate target rather than a flag because env -u ANTHROPIC_API_KEY does not disarm anything: load_env() puts the key back.
make doc-indexRebuilds the index /explain answers from. Needs no key: without OPENAI_API_KEY it writes no vectors, says so on both lines it prints, and retrieval falls back to bm25 and says so in every reply.
CI — .github/workflows/ci.ymlmake test and make eval on every push, plus a step that runs the model harness with nothing sent and requires exit 2. If asked why the documented commands rather than a tighter pipeline: a hand-written pytest -q stays green while make run is broken, and make run is what a reviewer types.
make fresh-checkClones HEAD into a temp directory and runs the suite there. The check that answers "does this work for someone who is not me".
make reset-demo / make reset-demo-dryPuts the demonstration tenant back. Dry run on 2026-08-07: 1,932 rows across 28 tables. It was 536 across 25 when this card was first written, before the seed order was fixed.
make statusRegenerates docs/.ai/state.json from the repo. The one file in docs/.ai/ nobody writes by hand.
verbatim.citelocal.aiLive — marketing site at /, application behind it on the same host. Returned 200 when this was written.

2. Numbers worth having cold

NumberWhat it is, and how to check it
2,730 pass, 1 xfail, 351 seconds.Re-run 2026-08-11 on this branch, straight, exit zero. Say "about two thousand seven hundred" and let make test print the exact one. The number on this card read 2,427 for six days, and it was not drift in the prose alone: state.json had gone stale in the same direction, so the guard comparing the two stayed green over a suite 303 tests larger. That is the one shape that guard cannot see, and it is worth telling as a story about checking a checker.
The decision log, numbered without a gap from ADR-001grep -c 'data-adr=' docs/.ai/decisions.html, run rather than recalled. Also the defects and judgements in findings.html. THE NUMBER USED TO BE PRINTED HERE and it was wrong within a day, twice: this row said ninety-five while the log held one hundred and four, in the same sentence that told a reader to run the command rather than recall it. A count in prose about a file that grows every session cannot be right for long, and being wrong on the panel brief is worse than being absent from it. Run the command.
The diff path: linear to 52.6 million characters, quadratic on repetition.2026-08-11, from scripts/bench_diff.py, which is committed — unlike the retrieval and tenancy figures below. About 0.2 µs per source character; 1.13–1.20 bytes of peak heap per character for diff() alone and 3.07–3.14 for the whole pipeline call. A pair of million-word filings: 2.9 seconds, 38.5 MB. Ten thousand as a backlog is 8.1 CPU-hours; ten thousand concurrent is 376 GB, so you never run ten thousand concurrent. Lead with the finding, not the numbers: the constraint is not SequenceMatcher, which is 1.5% of the call — it is normalization, at 87% — and the obvious fix, partitioning, made it 1.8 times slower and does not touch the quadratic case at all, because a repeated passage yields no anchor. Concede in the same breath: nothing was measured under concurrency, and nothing in the product bounds the pool the arithmetic assumes.
27 of 102 filings parse into a document tree. The tree changed nothing.2026-08-11, app/diff/structure.py::MEASURED. On the eight real version pairs, the two branches the tree was built for fired zero times; 112 confidences moved and every one moved because the parser had failed to read a document. A parse refusal raising a score is absence licensing certainty. So the wiring came out and the measurement is the deliverable. This is the answer to "where does modern AI fail and how did you design around it" — do not apologise for it.
The scaling measurement found a live defect in shipped code, and it was fixed the same day.2026-08-11, section 7 of docs/scalability.html. _similarity takes difflib's autojunk default, so on a passage over 200 characters most of the alphabet is junked; 11 of 800 real paragraphs changed by one word fall under the 0.50 caution line, the worst at 0.444 against a true 0.964. The direction is safe — it can only over-flag — so it fails toward review as ADR-003 requires. It was fixed on 2026-08-11, and the reason it was not a one-line commit is the answer worth giving: the value is stored on every change row, so the fix is a derived-corpus migration under playbook §27 rather than a keyword. scripts/remeasure_alignment.py ran it whole — 333 modified changes read, 25 moved, 2 cautions lifted, 0 added, idempotent on a second run. Volunteer the zero: this entry had argued the defect could only push a score down, and 333 real rows confirmed it rather than the argument confirming itself. ADR-97 carries the decision and a correction to its own figure — an independent sample put the crossing rate at 59 of 800 rather than 11, worst case 0.000 against a true 0.976, and neither number is discarded.
107,257 passages. Retrieval about ten milliseconds indexed against nearly three seconds scanning.Re-measured 2026-08-10 on the live database, six queries, best of five: 8.36–10.62ms indexed, 2,799–2,826ms scanned — 307 times, on the mean. The corpus grew 8.7-fold on 2026-08-10 when the 52 documents nothing had ever loaded were ingested (gap P2-17), so every earlier figure on this card is superseded rather than wrong: 12,311 passages at 0.95–1.51ms against 411–420ms, and before that 8,707 at 0.5–1.1ms against 357–364ms. The ratio barely moved and the absolute numbers moved a lot, which is the actual finding: the scan is linear in the corpus and the index is not, so the gap widens with every filing loaded. Concede the same gap as the tenancy numbers before they ask: no benchmark script is committed. These come from driving search_passages against _hits_by_scan in a throwaway script, so nothing in the repository reproduces them today. If they ask for the harness, that is a fair hit — the same fair hit, twice.
The Kentucky pair: 1,024,409 characters against 1,024,536.127 apart. Kentucky PSC 2025-00113, Lane Kollen's direct testimony filed 2025-09-09 and corrected 2025-09-30. Both counts recompute exactly off data/real/. The strongest single number in the submission.
144 changes, in under half a second.144 is exact and it is in the database: 134 modified, 6 added, 4 removed, across 4,285 passages against 4,287. Timed today at 0.40s. Say "under a second" — several documents print 0.78 seconds and nothing in the code measures it.
102 real filings, 102 provenance records, eight jurisdictions, 19 dockets.Georgia, Indiana, Kentucky, Missouri, North Carolina, Ohio, Utah, Virginia. Counted off the provenance files today. If pressed: the Ohio documents came off the Supreme Court of Ohio's public docket rather than PUCO's own.
70 routes, 19 routers, 26 templates.Read off the assembled application: from app.main import app; len(app.routes).
Tenancy: 101 of 176 filter deletions caught.Real, and easy to say wrong. Read the wording in story two before you use it.
45 messages, 0 replies, 4 bounces, 0 interviews.Zero is undisputed. The 45 is the best available figure and is not audited — an older note says about 40.
94% line coverage. 14,067 statements, 796 missed.Re-measured 2026-08-07 over the whole suite (pytest --cov=app, 2,785 passed, 1 xfailed, 8m26s under coverage). The 93% at 13,073 statements on this card was 2026-08-05 and is superseded. Say "measured", give the date, and do not offer it as a standard — line coverage measures execution, not verification. The 92% in resume.md is yesterday's, at 1,381 tests, and is superseded.
Numbers not to claim.
The glass contrast — and this one is now a story rather than a warning. The figure was wrong four times, and verbatim.css keeps every correction rather than tidying them away. First, 15.84 rising to 16.15, computed against #0f1e24 — an ink colour that appears nowhere in the stylesheet. Second, the fix raised --glass-tint and stopped there; nothing consumes var(--glass-tint), so the token moved, the paragraph was rewritten in the past tense, and the rendered pane did not change by one value. Third, the recomputed 16.73 / 8.26 / 4.57 were announced as "computed from the pixel Chromium drew" when they are ratio() against the flat --glass-solid token. Fourth, measuring the pane on a page with nothing dark behind it is the same error in a fourth disguise.
What is true today, and it is safe to say: 16.73 / 8.26 / 4.57 are ceilings against the flat --glass-solid token, they are pinned to the hundredth in tests/test_glass_contrast.py (which labels them CEILING in as many words), and the translucent pane over real content is lower and nobody has measured it. Say that, including the last clause.
Why it is worth telling: it is the clearest answer available to "how do you know your own numbers". A plausible figure reads as a checked one, nothing asserted either early figure in a test, and each correction was condemned by the next paragraph down. The fix that stuck was not a better number — it was a test that pins the number to the token.
The README line-count table — recounted 2026-08-11, and the recount is the story. It now reads 181,902 tracked lines over 95 files under app/, from git ls-files. The command it carried before was a find over the working tree, and it counted five git worktrees agents had left under .claude/: 1,027,894 lines against a real 181,902. Nothing in the number was a lie and the scope was. If a count comes up, that is the example to give, and the general form is worth saying: a re-derivation is only as trustworthy as its scope, and the scope is the part nobody reads.
Three rounds of adversarial review, and what they kept finding. Not one bug three times — one shape three times: a published number that does not reproduce from committed code.
The scalability page had it twice in a row. A worker was sized from a constant measured on a narrower region than the sentence around it claimed, and it erred low, which is the direction that admits a pair the worker cannot hold. That was corrected — and the correction was itself traced with the source document built outside the timed region while the sentence beside the table said the peak held the source text. Every pipeline figure moved again: 2.55–2.69 bytes per character became 3.07–3.14, the worker pool 575 MB became 693 MB, and the admission check's headroom fell from thirty per cent to eleven. The guard that stuck is not an inequality on the bytes — none would work — it watches where the allocation happens.
The third is the one to volunteer, because it is unfixed: the retrieval and tenancy figures on this card came from throwaway scripts and nothing in the repository reproduces them today. The diff numbers do not have that problem, because scripts/bench_diff.py is committed and guarded. If they ask for the harness behind 101-of-176 or 8.4ms-against-2,800ms, that is a fair hit, and the honest answer is "the script is not in the repository, and the one for the diff is — that is the difference between a measurement and a memory."
Say what the pattern taught rather than just conceding it: a number and the region it was measured over travel together, or the number is worthless. Every correction here was a region, not an arithmetic error.

3. Where the honest answer is uncomfortable

Volunteer these. A limit you concede is discipline; the same limit found by them is a hole.

How many users did you talk to?

None. 45 messages to named people on live dockets. Zero replies. Four bounced — and all four addresses came off a certificate of service and were marked "filing-verified" in my own log, which taught me that a verified fact has a shelf life, and that is now in the product. Why: cold email to people who file for a living, from an address they have never seen, inside a two-day window. What would change it: one warm introduction, through you or through AES. Do not offer the synthetic user as a substitute — offer it as what it is, a hypothesis generator that says SYNTHETIC in its own title and is barred by its own page from supplying a line to the PRD.

What does the gate not check?

The reading. "Verified" means the words exist where the claim says they do. It says nothing about whether the reading of them is right, and a fluent, correctly-cited, wrong reading passes. The one they may not find alone: a wrong-occurrence citation, where exact offsets land on occurrence A while the model reasoned about occurrence B. That is why REASON_AMBIGUOUS_OCCURRENCE exists and why the corpus carries the trap on purpose.

The model judges materiality. Where is that judgement stored, and who audited it?

In seven columns on the change row, and the model audited itself under change.materiality_set. materiality_for_company writes the verdict, the sentence behind it, the citation it rests on, which model said it and when — all seven together or not at all — and appends to the hash chain with actor_kind of model, so nothing there can later read as a person’s judgement.

What is NOT stored is the fact that the citation verified, and that is the whole argument. That fact has no shelf life: it is a statement about bytes, and bytes move. So it is recomputed from the stored source every time the verdict is shown, exactly as claims.py recomputes it for a stored claim. Only the model call is saved. A stored verdict whose quote is no longer at its offsets is withheld on the next render — no job runs, nothing is rebuilt, the page refuses — and a reviewer can watch that happen by editing the cited text and reloading.

The mistake we made getting here. The first version of this gated exactly one reader: the change screen, which is also the one that writes. The project list and the chat tool went on reading the materiality column raw. That had been harmless only because the column was permanently NULL — the moment anything wrote it, a verdict whose bytes had moved was refused on one screen and asserted as fact one link away, on the screen that shows it most often. 2,427 tests stayed green over the hole, because not one of them judged a change and then read a different surface. The fix was not to patch the two call sites: shown_materiality_for_company is now the only way to read that column for display, and a test walks the syntax tree of app/ and fails on any module that touches the attribute directly, so the third reader — the one nobody has written yet — cannot repeat it. The lesson we would state at the panel: a guarantee is worth exactly the number of readers that obey it, and “every read” in a docstring is an aspiration until something counts the readers.

The limits, and one of them closed on 2026-08-11. There is an eval on the model path nowapp/evals/model.py, make model-eval — and it is worth two sentences because its shape is the argument. Fabrication (a citation the harness cannot re-read) is counted twice, as a property of the model and as whether any survived the gate, which must be zero; a miss is counted apart, because it costs an afternoon and blocks nothing. A model that produced ten unverifiable citations and a gate that passed none is the design working, and one averaged score would have called that a failure. Concede in the same breath: the hardest measure, cited-correctly-and-reasoned-wrongly, rests on three cases; the harness spends money so CI runs it with nothing sent and requires the exit code that means "not evaluated"; and make eval still measures the deterministic half only and says so itself. On the real API, be precise, because there are two transports and only one has run. propose.py's AnthropicTransport, the citation-gate path, ran once deliberately on 2026-08-05 at 15:43:37 UTC against claude-opus-5, and the transcript is committed at docs/.ai/live-transport-probe.html; tests/test_live_transport_probe.py now rebuilds that request through the shipped transport and holds it to the exact bytes, so it fires on any drift in the model id, the token ceiling, the schema or the prompt. app/chat/agent.py carries a second AnthropicTransport with a different parameter set — tools, and effort inside output_config — and that one has still never run. Its docstrings still say so.
What one call proves, said before they ask: the wire format, the auth and the parse, once. Not behaviour under load, not retries, not long inputs. And say the footnote yourself rather than let them find it: while that page was being corrected, an agent ran the probe with --send believing the key was absent because it had cleared ANTHROPIC_API_KEY — but load_env() puts back any name from .env that is not already set, which is exactly what clearing creates. A second request almost certainly went out; its output was piped to head -2 and lost, so nothing about it can be reported. The page names it, and the script now warns in its own header that clearing the variable does not disarm it.

You say every material change becomes a routed action. Show me the rows.

Check the count before you answer: sqlite3 verbatim.db "select count(*) from change_obligations".
This morning: zero rows, against 171 changes and 8 obligations. map_change_to_obligation existed in app/state/routing.py and had tests; nothing on the product path called it. The join carrying the whole "change to action" sentence had never written a row — the same defect class as the assistant that shipped disconnected.
On a freshly seeded database today: 60 rows — 58 system, 2 analyst — against 765 changes and 8 obligations. app/state/mapping.py::propose_obligations_for_change proposes candidates by comparing words, with no model call; the screen recomputes the proposal on every render, for the same reason the claims are recomputed; a confirmed mapping is a stored row recording who confirmed it.
Do not memorise either number, and here is the reason, which is itself a good answer. An earlier version of this card said 26 rows against 171 changes. Both were real, and both came from a seed order that was wrong: seed_real_findings is a writer of versions, passages and changes, and it ran after the mapping pass instead of before it. The mapping pass therefore saw 171 changes on one run and 765 on the next, from the same commit. The seed target in the Makefile now fixes the order and says why in a comment above it. This is why the first line of this answer tells you to run the query rather than recall it.
Dating: that landed in the last hour. Say so — the commit timestamp tells them anyway, and a fresh fix honestly dated reads better than an old one implied.
Then follow it with the part that makes it a stronger answer than the question expects. A row existing is not the same as a person being told the work is theirs. resolve_change_owner did not read ChangeObligation.mapped_by_kind, so a mapping the pipeline guessed from a word overlap named somebody exactly as one a person had confirmed. Not theoretical: a Kentucky vegetation-management budget table shares "project" and "budget" with MEP's cost-allocation duty, and the screen printed a named analyst over it. ADR-85 found this, named it, and declined it — closing it properly meant a sixteenth refusal code and a change to a contract every caller depends on. ADR-87 then closed it: ROUTE_MAPPING_UNCONFIRMED. The name the product would have used goes in candidate_user_ids, never user_id, so the invariant every caller already relies on — user_id is set if and only if the code is in ROUTE_OK_CODES — does the enforcing, and no call site needs new discipline. ROUTE_PENDING_ACCEPTANCE converted too, at the cost of nine test_invites failures fixed rather than exempted: a person invited into the product and handed an escalation on the strength of a word overlap is the sharpest version of the bug, not an exception to it. The caveat sentence also moved out of the change screen into routing.py, because that screen was the only surface saying it while the escalation queue and the approval route — the surfaces that actually tell somebody work is theirs — do not render that template.
Why this is worth volunteering: it is the clearest case in the repository of finding the class rather than the line, and of an ADR that declines a fix on stated grounds before a later one pays the cost. The failing test is one sentence: a mapping written AUTHOR_SYSTEM, from the Kentucky fixture, walked to a live named human and answered ROUTE_OK.

You added embeddings. Have you ever called that provider?

Yes. Once, on 2026-08-11, and it is the newest thing on this card. The provider moved from Voyage to OpenAI for one reason: a key for the second is in the team's hands and a key for the first never was, so every constant in app/explain/embedding.py was a documentation reading checked by nothing. /explain retrieves over this project's own PRD, MRD and TDD; with OPENAI_API_KEY set the sections are embedded with text-embedding-3-small and without it the same sections are ranked by bm25 and every reply says so. The corpus embedded whole: 145 chunks, 243,820 characters, two requests, 51,854 tokens by the provider's own usage field, at $0.02 per million tokens — about a tenth of a cent for a full rebuild. The width is 1536, not the 1024 the previous code named. A question then went through end to end on the vector path and all five citations survived the re-read. ADR-104 is the record. Say what one call proves and stop there: the wire format, the auth and the parse, once. The refusal path is still unproven — no bad key and no wrong model name have been put to the endpoint — and nothing is known about load, retries or longer inputs. Every test still drives an injected fake through the same seam propose.py uses and the suite makes no network call.
Volunteer the near-miss, because it is the best thing in this answer: the comment in our own file said the endpoint would refuse an unknown field with an HTTP 400, so input_type had to be dropped. Put to the wire, that is false — the field comes back 200 and is silently ignored, which is the worse of the two outcomes: the request would have carried a word that reads like a parameter, changed nothing, and passed every check forever. It is the exact failure this product exists to catch, found in our own source, and found only by making a call we had spent weeks describing instead of making. The wrong guess is kept in the source beside the measurement.
The older near-miss still stands and is a different one: a placeholder value counts as absent in this code, deliberately, because KEY=CHANGE_ME is the exact shape that made a platform in the peer project report a provider it was not using for months while every vector was a hash. That is playbook section 26, and it is the reason the fallback here announces itself in the answer rather than in a log.

You shipped a module nothing calls. Why is that not dead code?

Two of them, and they are different cases — say which is which before they do.
app/chat/budget.py is a rule written before the feature it governs. Multi-turn chat is not built, and the reason the rule comes first is that the obvious policy is the wrong one: a sliding window over recent turns is what every chat product does, and it evicts the citation offsets that make a claim checkable. Drop one and the claim resting on it stops verifying — the exact failure the verifier exists to prevent, arriving through the door marked "context management". So citation spans, diff offsets and the question are a floor that is never evicted, and when the floor alone will not fit, the module refuses the turn. tests/test_history_needs_a_budget.py walks the syntax tree and fails if a conversation ever reaches a model any other way, so the rule cannot be bypassed by the one-line edit that would otherwise turn this into a multi-turn product with no policy.
app/diff/structure.py is the opposite: a feature that was wired in and taken back out, and the measurement is what shipped. Do not defend it as unfinished work.
The honest limit on both: "written and called by nothing" is the shape this repository has been caught by five times, and two of these are deliberate instances of it. The difference is that each says so in its own first paragraph and a test enforces the claim. If a reviewer thinks that is a distinction without a difference, that is a fair fight to have.

Your demo signs anybody in as an administrator.

Yes, my call, recorded as ADR-84. The administrator holds user.manage, user.invite, workflow.manage and threshold.set, and through the invitation path a visitor can invite an address they control and reach action.approve. Nothing rate-limits it. Why: a reviewer reaches the permission grid and the approval-route editor without asking anybody, and those are two of the strongest screens here. What is not at stake: confidentiality. No customers and no customer documents — one invented proceeding and filings the commissions publish themselves.

One correction to carry. scripts/reset_demo.py and make reset-demo exist, ADR-83 argues for them, and a dry run on 2026-08-07 reports 1,932 rows across 28 tables. ADR-84 says nothing resets the demo, and that is wrong. The true limit is narrower: no reviewer can reset it, nothing resets it automatically, and the reset works by deleting that tenant's audit chain — right for a demonstration, indefensible anywhere else.

Who stops a person approving their own work?

The product does, on one screen, and it did not until today. can_approve reads authorship out of the audit chain: hold action.approve, and you are still refused if any row shows you acting on the claim, the change beneath it or an escalation against it. /actions is the caller (ADR-91) — propose an action there and try to approve it, and the answer is 403 with the audit sequence number that proves you wrote it. Concede the history before they find it: the control was written, tested and reachable by nothing, an independent review found that, and two attempts to wire it into the escalation queue were reverted because that screen belongs to the analyst, who does not hold action.approve. Concede the limit, and concede that this brief carried the wrong version of it until an independent review followed the walkthrough. Approving records the decision and performs nothing. And the refusal cannot be reached from any screen: nobody in the grid holds both halves, and nothing in the product puts a second role on an account that already exists — /users makes new logins, every grant path is ceilinged against the granter, and admin deliberately holds no approval code. The demonstration is VERBATIM_DEMO_REFUSAL=1 python -m app.seed, which arranges it as the system actor and says so; tests/test_seeded_refusal.py drives it end to end. Say the shape of the mistake too, because it is the more useful half: the control was correct, tested and wired, and the sentence telling a reviewer how to watch it work was wrong in four files. That is the same failure as the eight unreachable controls, one level up. Say the second limit in the same breath, and say it precisely, because it changed: every test drives a deterministic fake through an injected transport, so what the suite proves is the refusal, not that the endpoint accepts these parameters. That was once true of both transports and is now true of one. The citation-gate transport in propose.py has run against the real endpoint, once, on 2026-08-05, with the transcript committed and the request pinned to exact bytes by a test. The chat transport in app/chat/agent.py, which sends a different parameter set, has not. Do not say "the first real call will find bugs in the transport" — the first real call was made and is written down; see the limits under story three.

4. The three stories

Tell these in your own words. They carry the interview.

The .env that was never loaded

A valid API key sat in .env on disk and nothing loaded it into the process. The assistant said it was unavailable, the proposer declined to propose, the mail transport declined to send — three modules, three named off-states, each announcing its fallback exactly as designed. Every message was true. The product was broken. The fix is app/config.py::load_env, called above the other imports in app/main.py because policy resolves its mode at import time. The lesson generalises: nothing looks wrong when each component politely reports a limitation it does not have, and a true sentence about a stale state is the hardest kind of wrong to see. There was a sequel the same day — load_env wrote blanks in as empty strings, so os.environ.get(name, default) stopped returning the default and two modules crashed at import.

The guards that were derived rather than listed

tests/test_app_wiring.py asserted five hand-written paths and passed happily while two view modules — projects.py and review_centre.py, 2,021 lines and nine routes between them — were mounted nowhere, with base.html linking to one of them from every page. A list a person maintains cannot catch the module that person forgot. Rewritten to read app.routes, it caught three unmounted routers on its first run, and over the rest of that day three more plus the permissions router — 2,200 lines and 130 passing tests, reachable by nobody.

Say the tenancy numbers this way. They are easy to get wrong. An early audit deleted eighteen tenant filters one at a time; the two hand-written isolation files caught four, and one deletion turned nothing red at all. The derived file then went at all 176 single-line filter deletions in app/. Asking the question once per function caught 78. Asking it once per query took it to 101, at the cost of eleven named exemptions across eight functions. Do not say "101 against 2" — 2 is the number of old test files, not the number they caught. No mutation harness was committed, so those figures live in comments in that file and nothing you can run today reproduces them. If they ask for the harness, that is a fair hit.

Deterministic reflection

The gate calls no model and makes no network request, so it cannot share a blind spot with the thing it checks. It was written before any model was connected, which is why connecting one later could not weaken it. It now refuses the model's own materiality verdict on the same screen where it refuses a stored claim, so a reviewer watches a deterministic checker turn down a model judgement and a seeded claim in the same shape, centimetres apart. A citation whose offsets sit two characters off is withheld rather than nudged into place: snapping an offset to the nearest match would make the verifier a formality checking its own repair. One detail — expected_occurrence is deliberately not passed when checking the model's verdict, since the only value available there would come from the offsets the model gave, and the check would be comparing the model with itself.

5. What you decided, not the AI

The defensible shape is "I chose this because X, and asked the AI to implement it." These are yours; the record is in the commit messages and the ADRs.

DecisionThe shape of it
Cut the 99% coverage targetAn agent refused the target and proposed branch coverage, mutation testing and properties instead. You overruled it — refusing a number while nothing else is measured is not rigour, it is declining to answer. A tool went in and the number stopped being an opinion. Then you cut the target on cost: hours spent raising a number, in a submission whose weakest dimension is user research, are the wrong hours. ADR-38 carries all four moves.
Keep the demo administratorADR-84, cost written out rather than softened — including the sentence in it that is wrong.
Order the film so the refusal comes firstAct one is the analyst's day and spends its longest beat on a claim refusing to assert itself; act two is the administrative half. A reviewer who meets the admin screens first reads this as configuration software; one who meets the refusal first reads the admin screens as the controls around a thing that already refuses.
Build retrieval, refuse searchFTS5 went in and ADR-02 was not reversed. No search box, no screen, no nav item. A search feature hands a person a ranked list and asks them to judge it, which puts a ranking in front of the evidence.
Disclose a conflict of duties rather than forbid itAny permission to any person, conflict disclosed. Refusing the pair does not stop somebody doing both things — it moves them to a spreadsheet where nothing records it.
No field to paste an API key intoWhoever sets the key also chooses the endpoint, and every prompt carries obligation titles, owner names and account ids. A settings form would quietly turn user.manage into "read this company's context off the wire".
Exclude summarisation on purposeAn exclusion in the roadmap, not a gap. The moment the assistant emits a sentence it cannot quote, the central claim acquires an exception — and that exception would immediately be the most-used feature in the product.
Retract a claim of user researchAn ADR said a permissions decision came "because the research showed real workspaces where the administrator is also a superuser". There has been no user research. Corrected in place, original left standing, re-attributed to reading titles and signature blocks in data/real/.
Decline the multi-agent claimThe build used a graph of agents; the product does not. Claiming otherwise, in a submission whose argument is that a claim must carry its evidence, would be the defect the submission is written against.

If they find a hole you have not listed

They will. Do not defend and do not collapse — both read badly.

That is a real gap and I had not thought about it. What it would break is [the specific consequence]. The fix is probably [direction], and the reason I would not have caught it in 48 hours is [the honest reason]. Is that the failure you were pointing at, or is there a worse version of it?

The last question is the move. It turns being caught into working the problem with them, which is what the residency is.