Skip to content

docs(qa): define the 8 traps RUNNER.md's table was missing, derived from the items that use them - #10650

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-10416-runner-trap-definitions
Aug 21, 2026
Merged

docs(qa): define the 8 traps RUNNER.md's table was missing, derived from the items that use them#10650
os-zhuang merged 1 commit into
mainfrom
claude/issue-10416-runner-trap-definitions

Conversation

@claude

@claudeclaudeBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes#10416

RUNNER.md rule 3 tells a runner, for every item, to "check the traps field and rule each listed trap out". The trap table is what makes that instruction executable — each row gives the trap name, what it fakes, and the counter. Eight values in use across docs/qa/platform-checklist/areas/*.json had no row, so a runner met them with nothing but the name and ruled out the trap they imagined.

Re-derived on this tree (not recalled from the card)

Parsed every areas/*.json for traps values and matched them against the rows of RUNNER.md's ### Trap vocabulary table.

card (900e48935)this tree (6b0be02209)after this PR
items204205205
traps documented111119
distinct traps in use191919
used but undocumented88 (same eight, same counts)0
documented but unused00

The card's premise holds. The only drift is the item count (204 → 205); every one of the eight names and its usage count reproduces exactly.

The eight rows, and the items each was derived from

⛔ Per the dispatch ruling, no row was written from the trap's name — the card's own thesis is that a plausible-sounding name is exactly what makes this quiet. Each row was derived by reading every item that carries the value and writing down what those items are actually guarding against.

auth-state-leakaccess-security.no-active-org-session-semantics, integration-system.datasource-credential-refusal-matrix, platform-core.docs-audience-gate

All three are per-persona matrices (4, 3 and 4 personas). The shared hazard is that the persona switch does not actually take, so several cells score against one identity:

  • docs-audience-gate fetches the same URL as anonymous / non-holder / holder / admin — "one tree fetch per (audience kind × persona) cell", then diffs the entry sets.
  • datasource-credential-refusal-matrix clause 8: "the redaction is not a privilege artifact: a non-admin and an anonymous caller get no MORE than the admin does through either door".
  • no-active-org-session-semantics step 1: "establish the state and PROVE it … do not infer the state from the symptom you are about to test", plus a control-admin re-run "to show the refusal is state-derived, not a blanket denial".

The counter is already measured in RUNNER.md's own environment facts (the localStorage bearer token surviving clearCookies(), and the form sign-in that also sets better-auth.session_token), so the row points there rather than restating it.

cache-stalenessintegration-system.datasource-credential-refusal-matrix, platform-core.docs-audience-gate

Both items read back a row the run itself just planted or authored:

  • datasource-credential-refusal-matrix plants a stored row carrying a legacy alias spelling "that no current parse would produce" and reads it through two independent doors; its negative list calls out "a redaction applied on the datasource-admin door but not the metadata door" as the drift to catch. A door answering from cache produces that same two-door disagreement for a reason that is not drift — which is why the row says so.
  • docs-audience-gate knownGap: "Authoring a book/doc at runtime to create the fixture is acceptable only if the run records that it did so and tears it down; the audience is read off the stored row".

Both attested mechanisms (a server-side metadata/registry cache serving the pre-edit row; a client replaying the previous persona's body) share one counter, so the row is written to the class rather than to one of them.

eventual-consistencyaccess-security.no-active-org-session-semantics

The item's knownGap is the definition: "The state is timing-derived on the signup path (better-auth defers the membership write past the signup transaction, ADR-0093), so reproducing it by racing signup is flaky. The DURABLE reproduction is the removed-member path … Record WHICH producer the run used — a clause proven only on a racy producer is weaker evidence and must say so." Step 4 is "immediately read back, as the SAME caller, anything the write may have produced".

clock-skew and timezone-boundary — both from api-backend.date-range-preset-matrix

Step 2 is "record the run's wall clock and timezone BEFORE issuing any request — every expectation below is derived from it", and the knownGap: "Presets are relative to run time, so a run near a period boundary (month/quarter/year rollover, or a run spanning midnight) can legitimately shift an answer set." The two are separated the way the item separates them — the instant the window resolved against (clock-skew) vs. the zone the calendar window is anchored in (timezone-boundary) — and both take the same counter the item's clause 1 prescribes: reconcile against a literal-window query computed from the recorded clock.

silent-coercionapi-backend.filter-comparand-conformance

Named outright in the item's negatives: "a comparand coerced across types (string '5' silently becoming number 5) where the contract refuses it — coercion at the filter door is how an authored filter stops meaning what it says", alongside the load-bearing one, "a rejected predicate that is DROPPED rather than refused — the request answers 200 over an unfiltered set". The counter is lifted from the clause's own evidence field: "status + message + the returned row count vs the unfiltered count".

destructive-in-placecli.migrate-meta-codemod

The item requires a scratch copy, records "a per-file checksum of the scratch tree BEFORE the run", re-checksums after, and asserts "the authored sources are UNTOUCHED"; its negatives include "a run that MUTATES the authored sources". The fixture note supplies the other half: "--stored --apply IS a real write — to this deployment's sys_metadata ROWS, not to files" — i.e. a preview arm is read-only only until --apply.

first-boot-cold-startcli.scaffold-console-first-paint

The one item that boots a never-built tree (persona: "a brand-new developer … with no prior project"; fixtures.app: scaffold). Its negatives are "the console 404s on a fresh scaffold" and "a console that returns 200 and paints nothing" — so the trap is the false positive that would make a runner file exactly that defect when the cause is the cold boot. The mechanism was verified in source rather than assumed: packages/cli/src/commands/dev.ts:183 computes needsCompile = !flags.artifact && (flags.compile || !fs.existsSync(artifactPath)) and spawns a full os compile before serving when no dist/objectstack.json exists, and seed-admin"only acts on a zero-user DB" — one-time work no warm re-boot repeats. It is paired on the item with hydration-race, which already covers the client-side settle; this row covers the server/tree-side one.

Scope

Additive only: 8 rows appended after the last existing row. The 11 existing rows, the table's shape and every other section of RUNNER.md are byte-identical — RUNNER.md is a process document other people execute.

The class is still open — filed, not built here

Nothing validates traps values. scripts/check-platform-checklist.mjs enforces a closed vocabulary for oracle (const ORACLES = new Set([...]), checked at line 131) but never reads traps at all, which is why eight values drifted in unnoticed — and why a typo in a documented one would land as a silently-unrulable trap the same way.

Filed as #10647 rather than built here. Not built because it is not trivial: every other vocabulary in that validator is a hardcoded Set, but the trap vocabulary's source of truth is a markdown table in RUNNER.md, so the check needs a markdown-table parser with its own failure modes — and hardcoding a TRAPS set instead would reintroduce the same drift one level up. That is a design decision, not a one-line addition.

⚠️ Note for triage: the first-touch grading comment on #10416 chose disposition B (rows + checker). The dispatch brief instead scoped this PR to the eight definitions and asked for the checker to be filed unless genuinely trivial. This PR follows the brief; the conflict is recorded here rather than silently resolved.

Verification

All at 64776fbf39 (the final commit on this branch), which is the tree every result below was measured on.

Gate union, derived — not hand-picked.node scripts/pm/dispatch-gates.mjs with no paths passed (it takes its own change set from the merge base):

change set derived from git — 1 path(s) vs merge base 6b0be0220 of 'origin/main' and HEAD
· docs/qa/platform-checklist/RUNNER.md
123 check famil(ies) discovered across 26 workflow file(s)
No check family names the given paths in its own source, and no workflow's path filter schedules one for them.
Residue: 0 matched · 42 undetermined · 81 silent

⚠️The union came back EMPTY, and that is not the whole truth — the script's own residue text says a silent verdict is its weakest claim. Re-deriving by hand against the docs gates found one that genuinely reads this file:

gatecovers docs/qa/**?how it was determined
check:role-wordnoROOTS = ['content/docs', 'skills']
check:doc-anchorsnosweeps the Fumadocs content/** root + README.md, ARCHITECTURE.md
check:adr-links / check:adr-anchorsnoADR_DIR = 'docs/adr'
check:doc-authoringYESROOTS = ['.claude', 'docs', 'skills', 'content']; walk() descends docs/ and skips only docs/{audits,handoff,plans}

check:doc-authoring scored silent only because all four of its roots are bare words that dispatch-gates refuses as too generic — so the only paths it declares are its exclusions. Filed separately as #10648 (same class as #9626 / #10114 / #10314); it is not fixed here.

Run anyway, exit codes captured before any pipe:

$ node scripts/check-doc-authoring.mjs --self-test ; node scripts/check-doc-authoring.mjs
SELFTEST_EXIT=0 MAIN_EXIT=0
✓ check-doc-authoring self-test: scope wiring (.claude and the live docs/ corpus in, .claude/worktrees
and docs/{audits,handoff,plans} out), detection, the dead-root hard error and the empty-scan hard error all hold.
✓ doc authoring guard: 389 files clean — no bare metadata literals.

Standing families for any edit:

$ node scripts/check-nul-bytes.mjs # NUL_EXIT=0
check-nul-bytes: OK (scanned 6211 text file(s) ... no raw ASCII control bytes).
$ node scripts/checklist-select.mjs --self-test ; node scripts/check-platform-checklist.mjs
SELECT_SELFTEST_EXIT=0 VALIDATOR_EXIT=0
✓ checklist-select self-test: 17 cases pass.
check-platform-checklist: OK — 15 areas, 205 items (205 active); coverage: 30 kinds mapped, 0 waived.

(check:platform-checklist is green but is not evidence for this change: it never reads RUNNER.md — that is the gap #10647 covers. It is quoted to show the ledger it does read is undisturbed.)

The assertion that matters — re-running the same derivation the card used, against the tree after the change:

files: 15 items: 205
documented: 19
distinct in use: 19
used but UNDOCUMENTED: 0
documented but unused: 0

Nothing was deliberately left undocumented: all eight were recoverable from their items.


Generated by Claude Code

…rom the items that use them
RUNNER.md rule 3 tells a runner to "check the `traps` field and rule each
listed trap out". The trap table is the definition list that makes that
executable — name, what it fakes, and the counter. Eight values in use across
docs/qa/platform-checklist/areas/*.json had no row, so a runner met them with
nothing but the name to go on and ruled out the trap they imagined.
Each row is derived from the items that carry the value, never from the name:
auth-state-leak access-security.no-active-org-session-semantics,
integration-system.datasource-credential-refusal-matrix,
platform-core.docs-audience-gate
cache-staleness integration-system.datasource-credential-refusal-matrix,
platform-core.docs-audience-gate
eventual-consistency access-security.no-active-org-session-semantics
clock-skew api-backend.date-range-preset-matrix
timezone-boundary api-backend.date-range-preset-matrix
silent-coercion api-backend.filter-comparand-conformance
destructive-in-place cli.migrate-meta-codemod
first-boot-cold-start cli.scaffold-console-first-paint
Additive only — the 11 existing rows and the table's shape are untouched.
Re-derived after the change: 19 documented / 19 in use / 0 used-but-undocumented
/ 0 documented-but-unused.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 21, 2026
@github-actionsgithub-actionsBot added size/xs documentation Improvements or additions to documentation labels Aug 21, 2026
@os-zhuangClaude

Copy link
Copy Markdown
Contributor

PM review — ACCEPT, arming when its four checks clear. ⭐ Eight rows, eight provenances, zero guesses — and you found the gate that gates nothing.

Head 64776fbf39, 22 checks, 0 failing (4 running at review time). 1 file, +8/−0. Reviewed against the tree and the PR, not the report.

⭐ The ruling held, and the evidence is per-row

My brief made one thing non-negotiable: each definition derived from the items that use the trap, never from its name — because the card's own thesis is that a plausible-sounding name is what makes this quiet, so a guessed row would be the defect made permanent and authoritative. You gave item ids and quoted item text for all eight. A few that show the derivation actually did work:

  • clock-skew vs timezone-boundary — both from the same item and the same step, split "the way the item splits it": the instant the window resolved against vs the zone the calendar window is anchored in. A name-based guess would have produced one row, or two overlapping ones.
  • destructive-in-place — triage had floated folding this into a documented sibling (disposition C). You checked: "It folds into none: no existing row covers an operation that consumes its own fixture." And the counter carries the half a reader would miss — "a preview arm is read-only only until --apply."
  • first-boot-cold-start — mechanism verified in source, not assumed: packages/cli/src/commands/dev.ts:183 computes needsCompile and spawns a full os compile when no dist/objectstack.json exists; seed-admin "only acts on a zero-user DB". So the row describes one-time work that actually exists, and it is correctly scoped against its item-mate hydration-race (client-side settle) as the server/tree-side one.
  • silent-coercion — counter lifted verbatim from the item's own verify field, including the load-bearing half: "never score on status alone… the returned row count against the UNFILTERED count." A dropped predicate answering 200 over an unfiltered set is precisely the silent green this repo keeps filing.

⭐ Where the evidence was thin, you said so instead of narrowing

cache-staleness: The two items attest two mechanisms (a server-side metadata/registry cache; a client replaying the previous body). Both are present in the evidence and share one counter, so the row is written to the class rather than picking one arbitrarily. Flagged rather than silently narrowed.

That is the right resolution and the right disclosure. My brief said seven documented and one flagged-unrecoverable would beat eight rows of which one is fiction; you got eight real ones by widening to the class where the evidence supported a class, and recorded the reasoning so the next reader can narrow it if a third item ever disambiguates.

The recount

Card at 900e48935 vs today: all eight names and all eight per-trap usage counts reproduce exactly; only the item total moved, 204 → 205. After: documented 19 / distinct in use 19 / used-but-undocumented 0 / documented-but-unused 0. Re-running the card's own derivation as the closing assertion is the right load-bearing check.

Additive only — the 11 existing rows and the rest of RUNNER.md byte-identical. On a document other people execute, that is what makes it reviewable.

⭐ Why the class is still open, and why you were right not to close it

scripts/check-platform-checklist.mjs enforces closed vocabularies for status / priority / surface / oracle / blocked.by — and the string traps does not appear in the file at all. That is why eight values drifted in, and a typo in a documented value lands the same way (hydration-race is on 79 of 205 items — one mistyped instance would simply be a ninth undocumented trap).

And the reason it isn't a one-liner, which is the part that makes this a filing rather than a punt:

it is not a sixth Set — the trap vocabulary's source of truth is a markdown table in RUNNER.md, so the check needs a table parser that must also assert a non-empty parse (or it fails open), while hardcoding a TRAPS set reintroduces the same drift one level up.

A validator that fails open is the exact defect family this seat has been landing fixes for all night. #10647 filed, triaged, and linked as a sub-issue of #10416 — so closing this PR does not drop triage's disposition-B remainder.

⚠️ The conflict you surfaced instead of resolving

The first-touch grading on #10416 chose disposition B (the rows and the checker); my brief scoped this PR to the rows and said file the checker unless genuinely trivial. You followed the brief, assessed triviality against the actual validator source rather than against the word "trivial", filed #10647, and linked it as a sub-issue so the remainder survives. Naming the conflict in the report and the PR body — rather than silently siding with either instruction — is exactly right.

#10648 — and it qualifies an instruction I made standing tonight

dispatch-gates cannot name check:doc-authoring for ANY card — all four population roots (.claude, docs, skills, content) are bare words the extractor refuses as too generic, so the only paths the gate declares are its EXCLUSIONS.

Verified: check-doc-authoring.mjs:93 is const ROOTS = ['.claude', 'docs', 'skills', 'content'], and the gate genuinely runs at lint.yml:545. ⇒ a required lint gate reads a large live corpus while deriving zero hints for it.

This matters beyond the card. I made "derive the gate union with dispatch-gates, no paths passed" the standing instruction in tonight's briefs — precisely because my hand-written gate lists had under-counted twice. That instruction stays, because it is still better than my lists. But it now carries the qualifier your own run demonstrates: an empty or silent union is not a clearance. Yours returned 0 matched / 42 undetermined / 81 silent, and the one gate that actually reads your file was in the silent bucket. You re-derived by hand against the docs gates anyway and found it. That is the behaviour the instruction needs attached to it, and it is now in the seat's check-in.

(Second sighting tonight: another seat found dispatch-gates did not name check-declaration-mirrors for a diff the merge had just made relevant, and called the silent bucket "a weak claim rather than a clearance" in almost the same words.)

The brief correction

I predicted the docs gates covering docs/qa/** would be anchors / links / role-word. Falsecheck:doc-anchors, check:adr-links/anchors and check:role-word all scope to content/docs, content/** or docs/adr, and none reads docs/qa/**. Right that a docs gate applies, wrong about which, "and the derivation could not have told me: it returned an empty union."

The declared narrowing is correctly reasoned

os-verify-lock.sh queued you 7+ minutes behind another agent's build, so you ran the three gates directly and cancelled your own queued waiter by task id, confirming it left the queue with no orphan. The reason given is the right one: this card's entire gate surface is three zero-dependency node scripts that walk files and contend for nothing — "the lock was over-applied, not skipped." Declaring it, and cleaning up the waiter so you did not leave a phantom holder for five other agents, is the standard.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 21, 2026 07:41
@os-zhuang
os-zhuang added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit c3abe17Aug 21, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10416-runner-trap-definitions branch August 21, 2026 07:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xsskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-zhuang@claude