Appearance
Journal — 2026-09-08
13:32 — Stage 2 /grill-with-docs, round 0
Actor: Proposer (claude-opus-5, session 01BXCqRu) Prompt: owner asked whether a cloud LLM with RAG could augment the product's features as a chatbot, then asked for a grill and for the result to land in CONTEXT.md and the spec before implementation. Artifact: docs/adr/0031-conversational-routing-with-a-cloud-query-tier.md, CONTEXT.md, .scratch/chemical-safety-assistant/PRD.mdDefects raised: none against existing artifacts — the grill resolved an open shape, it did not correct a written one. Coverage check: pass for this work. The repo-wide count was 67 before the session and 67 after; every one of those is R-46..R-112 from the two stack-conventions notes, which predicted their own RED and are stage-1's debt, not this session's.
What was actually asked, and the first thing found
The owner's proposal — cloud LLM plus RAG, chatbot over the features — is the option ADR-0002 rejected by name: "Generative with RAG grounding … the residual failure lands on a person's hands in a spill." That was surfaced before any design work, with the split stated plainly: retrieval was always permitted, generation never was, and D-40 already put an online-only chatbot service on the server.
Eight questions, eight owner rulings
- Router, not generator. Model decides where to point; the span is displayed verbatim. Generator and hybrid both declined, the second on ADR-0012's "verbatim text serving as decoration."
- Two-tier. Client-side Routing Floor always present; cloud Query Intent tier is an upgrade. Server-only declined because it puts a network dependency on a route into First Aid at the moment the problem statement says signal is frequently absent.
- Not Stated below a threshold, with the SDS Section nameable but its content not assertable. Threshold fixed at Curation.
- Routing never designates a Chemical Record — delegates to D-46's chooser. The acetone case in ADR-0031 is the reason: one supplier's sheet recommends nitrile gloves, another forbids storing with nitrile rubber.
- Query-only egress. No Corpus text reaches the provider, which also removes a licensing question nobody on this effort has authority to answer (D-01).
- Client-side Escalation Trigger ahead of any network call, and Routing never pre-fills a spill branch or an exposure route.
- Seam 5, a Routing Fixture of exact per-case outcomes with mandatory negative cases. No percentages, so D-30 holds.
- Full landing — ADR plus glossary plus spec, rather than deferring the spec.
Notes worth keeping
A correction made mid-session. The frontier stage was taken from the handoff ("frontier stage US") rather than from the checker. init.sh's fixture output prints frontier stage: T, which is the fixture's own sandbox and not this repo. Reading coverage_check.py's frontier computation directly confirmed US — only R, D and US ids are declared inside the scanned paths, and the one W-01 in the tree lives in docs/agents/templates/, which is not scanned. Had the handoff been trusted and been wrong, the seven new D ids would have needed no downstream row and the PRD Coverage rows added for them would have been exactly the nominal-mapping failure the handoff's own Do-NOT list names.
ADR-0031 supersedes nothing, and says so in its Status line. ADR-0002 and ADR-0012 stand in full. The ADR carries an explicit abuse warning in the same shape ADR-0016 uses for "proxy corpus": "We added RAG" is exactly the phrase that will later be used to reopen ADR-0002.
Three new Curation artifacts, none per-document — Routing Threshold, Escalation Trigger, Routing Fixture. Recorded in Declared Gaps because D-58's half-day-to-a-day measurement did not include them and they do not scale with corpus size.
Blocked, unchanged: the cross-model grill on the spec. It is now owed against a revised spec — §2, §9, four user stories, Seam 5, three gaps and seven Coverage rows moved this session. Stage 3 still does not start until it clears.
14:05 — Stage 2 /grill-with-docs, round 1 ("with the decision open")
Actor: Proposer (claude-opus-5, session 01BXCqRu) Prompt: owner asked to grill the decisions left open by round 0. Artifact: docs/adr/0032-corpus-unavailable-is-not-not-stated.md, docs/adr/0033-gemini-flash-paid-tier-and-a-capped-endpoint.md, docs/adr/0034-the-corpus-is-sized-to-the-study-area.md, CONTEXT.md, .scratch/chemical-safety-assistant/PRD.mdDefects raised: two, both in artifacts written the same day — see below. Coverage check: 67 before, 67 after. Nine new D ids declared and all nine landed in the PRD Coverage table in the same pass.
The open set was four, not three
Round 0's closing summary listed three open owner decisions. Re-reading PRD Risks turned up a fourth, and it was the only safety-shaped one: "decide what the app does when it starts and finds its Corpus missing or partial. That path is unspecified here, and it is the realistic failure." Contradictions 1-4 are all CLOSED; Risks 1 and 2 are not. Taking the summary at face value would have grilled the budget and skipped the hazard.
Six rulings
- Corpus Unavailable is a state distinct from Not Stated (ADR-0032). The defect it prevents: an evicted IndexedDB still starts, still renders, and answers Not Stated — telling a Handler the supplier was silent on gloves when the truth is the document is gone. ADR-0002's failure mode arriving through storage instead of through a model, invisible because the vocabulary had no word for the second case.
- Verifiably complete or absent, never partial, and Escalation survives an empty Corpus — 1669 is not a Corpus read.
gemini-3.8-flash, paid tier (ADR-0033). Owner chose the provider; theclaude-apiskill was loaded first so the Claude prices offered were fetched rather than recalled, and the Gemini prices were fetched from Google for the same reason.- D-69's zero-retention half was withdrawn, not quietly dropped. See below.
- Server-side cap plus per-address limit, because D-60 left the endpoint unauthenticated by construction and there is no per-user identity to limit against.
- The Corpus is sized to a Study Area, not a document count (ADR-0034).
The defect the price check found
D-69 was written yesterday requiring "zero retention and no training on submitted text." Checking it against a real provider showed it was never meetable: the free tier trains on submitted content, the paid tier does not train but retains briefly for safety and compliance, and true zero retention exists only under enterprise contractual terms — a company-only resource under D-01.
The requirement was written without being checked against anything. It sounded like diligence and was actually an assumption. ADR-0033 narrows it to the half that is true, states the residual exposure in words rather than gesturing at it, and records that the gap is a limit of what was available rather than a convenience.
Why the corpus-size answer is not a number
ADR-0028 framed corpus size as a budget lever and the spec repeated it, which left the decision as a number to haggle down. A count is the wrong unit. Twelve documents chosen for availability, put in front of a participant who handles thirty chemicals, produce a study that measures emptiness. The same twelve chosen because they are every chemical on that participant's line produce a study where a miss is a finding. Identical effort, and only one of them answers R-04.2.
D-80 is the cheap half nobody had separated out: the area's chemical list needs no document handover, can be answered by a supervisor in an afternoon, and is a better test of access than continuing to wait for the binder.
Two corrections carried from round 0's landing
Both were found by review, not by a gate, and neither has a gate that could have caught them — the glossary-term check in init.sh is still one of the commented-out optional ones.
- "Online tier" had become a second name for the Query Intent tier in seven places, which is exactly what the glossary's
_Avoid_lists exist to prevent. - The PRD Vocabulary note said no term was coined for the spec and then said six were. The first sentence was inherited and had stopped being true.
Notes worth keeping
amends ADR-NNNN, which stands is safe with the status gate. adr_status_check.py only reads supersedes claims from the Status line, so amending needs no status change on the amended ADR. Five ADRs already use the pattern.
A near-miss on redeclaration. Writing ### D-69 amended… as a heading in ADR-0033 would have fired the coverage checker's redeclaration rule — ids are unique and permanent. The amendment is referenced in prose and the new ruling carries its own id (D-76).
Blocked, and now frozen: the cross-model grill. The spec has moved twice in one day, and every improvement pushes the grill further back. The owner ruled a freeze: no further spec additions until the grill clears. That is the actual decision; the timing follows from it.
15:40 — Docs site, VitePress over the decisions
Actor: Proposer (claude-opus-5, session 01BXCqRu) + three general-purpose subagents Artifact: .vitepress/config.mts, index.md, docs/guide/*.md, scripts/gen-decision-index.mjs, scripts/check-mermaid.mjsCoverage check: 67 before, 67 after. The site declares no ids.
Grilled to five rulings
Docs site only, not the application — "implement it" attaches to the VitePress documents, and the other reading would have started stage 3 and spent the owed grill. Then: guide pages link and never restate; the repository root is the VitePress source so the glossary and spec are gated too; topics derived from this product rather than copied from the sibling; scaffold first, then three agents.
The argument for link-don't-restate came from the template itself. The work-permit repo carries guide/doc-drift.md — "Where the normative files no longer match production" — a page whose whole job is tracking its own docs drifting from its normative files. That is the cost of restating, already visible on the shelf this repo was told to copy from.
Two defects the tooling found on its first run
The spec was never being rendered. .scratch/ is a dot directory and VitePress globs with dotfiles excluded, so every link to the PRD was dead. Found by ignoreDeadLinks: false on the first build — the gate working before a single guide page existed. Fixed with a tracked spec symlink, which gives the same file a non-dot path without moving it, copying it, or editing the three governance documents that name .scratch by path.
A bad mermaid diagram does not fail the build. The plugin renders client-side, so a syntax error ships as a blank box and the build stays green. The sibling repo had already paid for this and carries check-mermaid.mjs; it is now ported, with the JSDOM shim that fixes the DOMPurify.addHook is not a function failure two of the three agents independently hit and correctly diagnosed as environmental. 6/6 diagrams parse.
That one matters more here than in the sibling. The Routing flowchart renders D-68 geometrically — its arrow goes from Routing into manual search, never into a Chemical Record — so the page never has to write "Routing never designates a Chemical Record" as prose. A diagram that silently fails to render takes the ruling off the page and leaves nothing in its place.
A second numbering hole, found by arithmetic
An agent noticed session-handoff.md said 80 decisions while the generated index said 75, refused to paper over the difference, and traced it. D-47 and D-48 are declared nowhere — not in docs/, not in .scratch/, not in CONTEXT.md, and not anywhere in git history. ADR-0021 tombstones D-23/D-24/D-25 and does not mention these.
Same defect class as ADR-0021, found the same way and by the same behaviour that ADR-0021's own Context praises: an agent reporting a discrepancy rather than quietly closing it. The tombstone was deliberately not written — a new D- id needs a PRD Coverage row, which is a spec addition the freeze forbids. It is recorded as a blocker for the grill instead.
Notes worth keeping
The agents did not need correcting on the rule. All three returned zero gap: comments and zero dead links, and one of them corrected a claim before publishing it — it nearly wrote that no file in either app repo names a domain term, grepped, and found src/router/index.ts:27 const DEFAULT_TITLE = 'AI Chemical Safety Assistant' // TODO: Change this.
One agent refused a merge that would have invented a third source. ADR-0020's D-38 and PRD §1 list the stack differently (§1 adds Playwright, @iconify/vue, OpenAPI/CORS). It quoted D-38 alone rather than combining them into a list neither document contains.
The freeze held. Nine hundred lines of documentation landed without one id declared, one ADR edited, or one line of the spec changed.
16:20 — The cross-model grill, finally run
Actor: three adversarial reviewers (claude-fable-5-1), driven by the Proposer (claude-opus-5, session 01BXCqRu) Artifact: .scratch/chemical-safety-assistant/grill-findings-2026-09-08.mdDefects raised: 3 blockers, 5 serious, 6 unruled safety holes. Nothing fixed. Coverage check: 67 before, 67 after. The findings file declares no ids.
The model substitution, stated plainly
The handoff required claude --model claude-opus-4-8 in a second terminal. ListAgents showed sixteen peers and none of them was it — all other projects. Messaging one would have hijacked somebody's working session. The grill ran on Claude Fable 5.1 instead: a different model family from the proposer, no shared context, three reviewers on three attack surfaces. The requirement the ruling protected was fresh eyes that are not this model's; that was met. If the owner wants Opus 4.8 specifically, it still needs a terminal.
The finding that justifies the whole exercise
Almost every high-severity defect is in ADR-0031 through ADR-0034 — the four documents written in the last two days by the session that then reviewed its own work and pronounced them sound. The older ADRs, which have survived prior adversarial passes, came back clean.
Three of the confirmed defects were introduced by this session. One of them was introduced hours earlier, by the commit that claimed to be fixing staleness: annotating Risks item 2 as CLOSED deleted the very sentence the annotation says is "left above as the record", and left ADR-0032 quoting a sentence that no longer exists anywhere. The commit message for that edit described finding and fixing three inherited sentences that had stopped being true. It created a fourth.
The three blockers
- Not Stated means two incompatible things.
CONTEXT.md:43says it is what the app answers when the Corpus lacks an answer; ADR-0032 says it is a statement about a consulted document. For a chemical not in the Corpus no SDS was consulted. Seam 5 mandates the glossary's reading; US-62 promises the opposite. Root cause: ADR-0032 added Corpus Unavailable beside the Not Stated entry without re-reading it. There are three facts and two words. - Routing has no Corpus Unavailable outcome. The glossary gives Routing three outcomes and the Routing Fixture tests those three. A Floor over an evicted store returns Not Stated by ADR-0031's own threshold rule — the exact answer ADR-0032 forbids.
- Term explanation has no legal transport. §2 says the backing service receives Query Intent only and returns structured intent; §9 says term explanation is generation and is online-only. Generated prose is not structured intent.
Two failures of the harness itself
The gate cannot see a dead upstream. Three Coverage rows in the newest ADRs land on R-03, R-19 and R-32 — the first two superseded on 2026-09-02. coverage_check.py verifies an upstream id was declared; it never checks whether it is still live. The handoff already said "presence is not truthfulness". It is not currency either.
A deleted study objective was cited twice. ADR-0018 recorded that R-24 removed R-04.2, and ADR-0017 had already corrected ADR-0009 for citing it. ADR-0031 and the spec's Declared Gaps both cite it again, six days later. A previously-corrected error recurring is worth more than a new one: it says the correction was recorded somewhere nobody re-reads.
What was clean
The proxy-corpus rulings, Reconciliation, never-merge, §5, §7, §8, Seams 1-3, the four superseded ADRs' dead-status lines, and all eight glossary terms coined this week. Eleven of the sixteen new spec Coverage rows verified accurate against their targets.
Note for whoever fixes these
Fixes are new ADRs that supersede or amend — never edits to accepted ones. ADR-0034 carries a "What each amendment touches" section; ADR-0031 and ADR-0033 do not, and finding 8 is partly about that absence. Write the section.
17:10 — The gate gap, closed
Actor: Proposer (claude-opus-5, session 01BXCqRu) Artifact: scripts/id_status_check.py, scripts/id-status-check.sh, init.sh, docs/adr/0035-findings-carry-their-own-supersession-marker.md, three research markers, four Coverage Note cells Coverage check: 67 before, 71 after declaring D-81..D-84, 67 again once they landed in the spec. New gate: IDSTATUS: PASS.
What the grill actually found
Not "the gate is incomplete". The repository had already diagnosed this exact defect one level up and written adr_status_check.py for it, whose docstring says a one-sided supersession leaves "nothing to warn a reader who opened the dead one directly". Findings had the identical shape and no gate at all: poster-v2.md said "Supersedes R-03", and R-03's own entry said nothing.
Worse, the correction already existed in prose, twice. ADR-0018 D-34 recorded on 2026-09-02 that ADR-0005 cited R-03's superseded settings — and ADR-0018 fixed ADR-0005's prose while leaving ADR-0005's Coverage row standing. Six days later the same session cited R-03 in two new ADRs and cited R-04.2, an objective ADR-0018 D-33 records as deleted and ADR-0017 had already corrected another ADR for citing.
That is the argument for a gate over another correction document. A prose correction is read once, by the person writing it.
Verification found a fourth row the grill missed
The grill named three. Enumerating rows that would fail turned up ADR-0001's R-04 row — the foundational ADR, stale since 2026-09-02, missed by every prior pass including the cross-model one.
The ruling that proved itself the same hour
Q3 chose two states — Superseded by for dead, Amended by for partly replaced — over treating every replacement as wholesale. Then the row enumeration showed three rows cite R-19, including ADR-0001's and ADR-0017's. Under the wholesale reading the new gate's first act would have been failing the two oldest, most-reviewed ADRs in the repo. A gate whose debut is a false positive teaches everyone its failures are noise.
Two bugs the checker found in itself
- The first
--demorun failed asserting rule 1 fires for an unmarked target. The fixture was wrong, not the code: nothing in it claimed to supersede R-02, so rule 1 correctly stayed silent. - Rule 1 originally also required the claimant to appear in the target's marker. ADR-0014's prose reports "R-24 supersedes R-04" as evidence without being the superseder, and the enclosing heading there is a D-id — the check would have failed correct text. Dropped; rule 2 still validates what a marker names.
- Superseder ids were harvested from the whole marker line, so R-03's read "R-22, R-23, R-22". Now taken from the head only, before the em dash, where the explanatory prose starts.
Notes worth keeping
Not folded into coverage_check.py, deliberately. That gate is red for 67 stage-1 reasons and a new defect would have printed as problem 68. init.sh's own header records what that costs in a sibling repo: four lint errors kept a typecheck gate from executing for two weeks, "because the run was red for a reason everyone had learned to ignore".
IDSTATUS is in GATES, not just in a run line. init.sh warns that a gate missing from the variable "runs but is never counted and a failure exits 0" — a gate that cannot fail.
The new checker's --demo runs under the FIXTURE gate, like coverage_check.py's, so a checker loosened into always-passing is caught by something other than memory.
A narrow new rule, stated out loud (D-83). Coverage Note cells in Accepted ADRs are now editable for bookkeeping. Without saying so, the four Note edits here would read as licence to edit anything.
What this does not fix, and cannot. IDSTATUS gates currency, not correctness. Two rows the grill found — ADR-0031's R-32 and ADR-0033's R-19 — cite live findings for claims those findings do not make. No currency gate reaches that. Both stay open, and the two R-03 rows carry the same second defect with their Notes saying so.
18:05 — The three blockers, closed
Actor: Proposer (claude-opus-5, session 01BXCqRu) Artifact: docs/adr/0036-four-answers-for-no-answer.md, docs/adr/0037-term-explanation-is-curated-not-generated.md, CONTEXT.md, the spec Coverage check: 67 before, 74 after declaring D-85..D-91, 67 again once they landed. IDSTATUS: PASS throughout — every Coverage row written today cites a live upstream, which is more than could be said for two of the four ADRs written yesterday.
The grill said three facts. It was four.
The grill found Not Stated defined incompatibly in two places and counted three facts sharing two words. Walking the branch found a fourth: a Chemical Record ships only the spans a curator selected (D-50), so a question whose answer sits in an unselected span renders as "the SDS does not say" when the SDS says it three pages in.
That fourth one does not get a state. Its dangerous half is already ruled out — ADR-0015 forbids a selection omitting a Restriction, D-44 requires whole-document Reconciliation, Seam 1 asserts it — and the residue is a Curation quality problem the runtime cannot distinguish from fact 1 without per-Section bookkeeping. That bookkeeping was rejected on a cost argument that matters: it is paid per document, on top of a figure D-59 already calls a floor, and ADR-0028 rules that corpus size is the schedule. Buying it shrinks the Study Area, and a smaller Corpus produces more Not In Corpus answers than the Outside The Record answers the bookkeeping would buy. It makes the common failure worse to fix the rare one.
Four states, and the sentinel that makes two of them distinguishable
Not Stated (a document was read and is silent), Not In Corpus (no record), Outside The Record (a record, but not that field), Corpus Unavailable (no data on this device).
The last two would have been indistinguishable at runtime, because an evicted store makes every read return nothing — so Not In Corpus would have inherited exactly the lie Not Stated just shed. D-88 resolves it with one O(1) read of the snapshot-version record: present and the chemical absent means Not In Corpus; gone means the store was evicted. That closed a HARM-rated safety hole as a side effect — the grill's startup-only verification finding — without any machinery written for it.
The blocker-3 fix bought a property worth more than the fix
Term explanation had no legal transport: §2 says the service returns structured intent, §9 said explanation is generated prose. The tempting fix is a second request type. The chosen fix makes explanation extractive — a Curated Explanation written at Curation, the same object as a Curated Translation pointed at a word instead of a passage.
It closes the transport blocker, closes the safety review's ไนไตร hole (explaining a glove material is inseparable from a suitability claim), makes explanation work offline, and yields D-91: no text the app displays is generated by a model. ADR-0002's line stops being "extractive with one styled exception" and becomes absolute. That is a far easier claim to defend to a Safety Officer, and it is mechanically testable — every rendered string traces to a Source Span or a Curated Explanation.
Notes worth keeping
ADR-0036 has an empty Coverage table, deliberately. No source finding drives it; it came from adversarial review of the repo's own vocabulary. ADR-0021 set that precedent and it is stated in the table's place. The alternative was stretching an upstream to look rigorous — which is precisely what the grill caught in ADR-0032 and ADR-0033 the day before.
Both ADRs carry "What each amendment touches". Five accepted decisions across four documents are amended, and grill finding 8 was partly about ADR-0031 and ADR-0033 lacking that section.
The Vocabulary note went stale a third time and was caught in the same pass. It listed eight coined terms; there are now eleven plus one rewritten. Two earlier instances of this exact staleness were fixed on 2026-09-08 and a third appeared within hours — the note is a running total, and a running total in prose goes stale every time the thing it counts changes.
A cost nobody has sized. The Curated Explanation glossary is the fourth once-per-Corpus Curation artifact after the Routing Threshold, the Escalation Trigger and the Routing Fixture. None was in D-58's per-document measurement and none scales with corpus size, so ADR-0028's "corpus size is the schedule" no longer covers the whole cost. Recorded as a blocker.
19:30 — First application code, and a gate that had never worked
Actor: Proposer (claude-opus-5) + two general-purpose subagents Artifact: app/chemical-safety-assistant-api @ 3f993d2, app/chemical-safety-assistant-frontend @ 86e8f28, gitlinks advanced Coverage check: 67 before, 67 after. IDSTATUS: PASS.
Scope was cut before spawning, not after
The owner asked for the application. Three open map tickets govern application behaviour — the offline contract is undecided (US-31 and US-56 promise opposite things), escalation text has no provenance ruling, the Escalation Trigger has no destination. So the Conversational surface, Routing, First Aid and Spill Response were excluded and only what accepted ADRs already fix was built. Nothing built needs a Corpus, so the unchosen Study Area blocked none of it.
The frontend's TYPECHECK gate had never checked anything
init.sh ran vue-tsc --noEmit against a solution-style root tsconfig.json — "files": [] plus references — so tsc had no files and exited 0 in milliseconds. Every prior TYPECHECK: PASS in that repo was meaningless.
Falsified rather than believed: a deliberate type error left --noEmit at exit 0 and produced TS2322 under --build --force. Now --build --force, with --force so a stale .tsbuildinfo cannot green it either.
This is the same class of defect this repo's FIXTURE gate exists to catch — a checker loosened into always-passing — found in a sibling harness where nothing was watching. The API's tsc --noEmit was tested the same way and is genuine. Both work-permit frontends carry solution-style tsconfigs and are worth the same check.
A security regression I introduced, found by the agent that introduced it
Removing the auth furniture was correct: D-60 cuts accounts, so a guard requiring an unobtainable session was dead code. But three template routes sat behind it — POST /upload, GET /file, DELETE /file — each taking an object-storage path from the request. Closed-but-dead before, open after.
The agent reported it rather than guessing at a fix, which was right. Verified it directly: the delete route is mounted in app.module.ts, takes a path from the query string, no guard. Unmounted both modules — the smallest reversal — and left the modules on disk with the reason written where the next person looks. Mounting them again needs a ruling about who may call them, not a guard, because D-60 leaves no session to guard with.
What each agent did that was better than asked
Frontend: made state-collapse a type error three ways — literal discriminants, disjoint required payloads (Not In Corpus structurally cannot name a record; Corpus Unavailable carries neither record nor version), and no shared renderable field, so a generic renderer has nothing to reach for without narrowing. It also proved the guard has teeth: adding a fifth variant fails at the dispatcher's v-else. Its sharpest test is evictSentinelOnly() — the natural shortcut of checking the sentinel only when the record is missing passes every other test and answers Not Stated for a vanished Corpus.
It flagged that persist: false on the store is load-bearing: persisting the snapshot version would give an evicted IndexedDB a surviving copy still claiming "verified", reintroducing ADR-0032's failure through the state layer.
API: made the central rule structural rather than conventional — no model holds safety text in a scalar column, so a safety field is a row with a required sourceSpanId and the span chain to its SDS Section is required. A non-span safety field has no shape to take. It also proved its schema test has teeth by breaking the schema four ways and watching four failures.
Both stated their own limits rather than being caught at them
The frontend's test drives a fake gateway because fake-indexeddb is not a dependency, so the real IndexedDB path is covered by typecheck and review only. The API's two migrations were produced by prisma migrate diff with no database reachable and are unverified against live Postgres. Eleven TODO(spec): markers between them, none of them a guess.