Repository navigation
[finding][devx] 8 traps values are used across areas/*.json but absent from RUNNER.md's trap table — the runner is told to rule out traps it has no definition for #10416
Description
Activity
First-touch grading (triage seat): promoted to
pm:queue, Task, disposition B (document the 8 from the items that use them, and havecheck:platform-checklistverify everytrapsvalue has a RUNNER.md row — theoracleenum check is the in-file precedent, and it closes the typo case too). Per-trap fold-ins in the spirit of C (destructive-in-placeinto a documented sibling) are the dev's judgment while writing the rows. Note the validator stays on the manual cadence by maintainer decision — this adds a check to it, not CI wiring. Co-location note: #10418 edits the same checklist area; the dispatching seat may fold or serialize. Size/model suggestion: S–M, sonnet.
Generated by Claude Code
Claim:
/pm-dispatchdomain:devx execution seat.
Session:session_01DdCnBGcHeufjrq7drTD3wt
Branch:claude/issue-10416-runner-trap-definitions
Worktree:objectstack-issue-10416
Domain:domain:devx
File surface:docs/qa/platform-checklist/RUNNER.md(the trap table) — and only that, unless the measurement says otherwise⭐ PM ruling in the brief: ⛔ the eight missing definitions must be derived from the items that use them, never guessed from the trap name. The card's own thesis is that a plausible-sounding name is what makes this quiet — "a runner who guesses wrong rules out the trap they imagined" — so a guessed definition would be the defect made permanent in the playbook.
⛔ Governed surface out of scope (
docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md,content/docs/releases/**) and the #9465 epic fence (.changeset/tooling,cut-rc.yml,release.yml, rootpackage.json).
⚠️ This repo has no formatter of record (noformat/prettierscript, prettier not in devDeps, no.prettierrc/.editorconfig) — ⛔ do not runprettier --write(#10622).
Generated by Claude Code
{ "issue": 10416, "status": "done", "branch": "claude/issue-10416-runner-trap-definitions", "pr": "https://github.com/objectstack-ai/objectstack/pull/10650", "premise_still_valid": true, "summary": "Appended 8 rows to RUNNER.md's `### Trap vocabulary` table, each giving what the trap fakes and the counter, matching the 11 existing rows' shape. Every row was derived by reading the items that carry the value, never from the trap's name; no row was guessed and none was left undocumented. Additive only: the 11 existing rows and the rest of RUNNER.md are byte-identical. Re-derived on today's tree the card's premise holds exactly (same 8 names, same counts); only the item count moved, 204 -> 205. Two findings filed: #10647 (nothing validates `traps` values -- the reason this drifted, and the remainder of triage's disposition B) and #10648 (a required lint gate that is invisible to dispatch-gates, found while deriving this card's gate union).", "recount": { "card_at_900e48935": { "items": 204, "documented": 11, "distinct_in_use": 19, "used_but_undocumented": 8 }, "this_tree_before_6b0be02209": { "items": 205, "documented": 11, "distinct_in_use": 19, "used_but_undocumented": 8, "documented_but_unused": 0 }, "after_64776fbf39": { "items": 205, "documented": 19, "distinct_in_use": 19, "used_but_undocumented": 0, "documented_but_unused": 0 }, "note": "All eight names and all eight per-trap usage counts reproduce exactly. The only divergence from the card is items 204 -> 205." }, "definitions_and_provenance": { "auth-state-leak": { "items": ["access-security.no-active-org-session-semantics", "integration-system.datasource-credential-refusal-matrix", "platform-core.docs-audience-gate"], "evidence": "All three are per-persona matrices (4/3/4 personas) that re-issue the SAME request as different callers. docs-audience-gate: 'one tree fetch per (audience kind x persona) cell', then diffs 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'.", "row": "fakes: a per-persona matrix scored against ONE identity -- the previous persona's credentials survived the switch. counter: prove the identity server-side before each cell (session row / GET /auth/get-session), never from the gesture meant to switch it; a session per persona. Points at RUNNER.md's OWN environment fact (localStorage bearer surviving clearCookies(); form sign-in also setting better-auth.session_token) rather than restating it." }, "cache-staleness": { "items": ["integration-system.datasource-credential-refusal-matrix", "platform-core.docs-audience-gate"], "evidence": "Both read back a row the run itself just planted or authored. datasource item plants a stored row with a legacy alias spelling 'that no current parse would produce' and reads it through two independent doors; its negatives call out 'a redaction applied on the datasource-admin door but not the metadata door' as the drift to catch -- a cached door produces that same disagreement for a reason that is not drift. 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'.", "row": "fakes: a read answered from a cache instead of re-resolved -- the planted/edited row reads back unchanged, or one persona's body is replayed for the next. counter: confirm the response carries something this run set; a client of its own per persona; two doors disagreeing may be one door's cache rather than the drift the item hunts.", "confidence_note": "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." }, "eventual-consistency": { "items": ["access-security.no-active-org-session-semantics"], "evidence": "The knownGap is effectively 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: 'immediately read back, as the SAME caller, anything the write may have produced'.", "row": "fakes: a deferred write read before it settles -- an effect not yet landed reads as a refusal, a state the reconciler is about to fill reads as durable. counter: prove the state from its own record, never from the symptom; prefer the durable producer over the racy one and record which the verdict rests on; re-read after a settle window before writing an absence down." }, "clock-skew": { "items": ["api-backend.date-range-preset-matrix"], "evidence": "Step 2: 'record the run's wall clock and timezone BEFORE issuing any request -- every expectation below is derived from it'. 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.' Clause 1 verify: 'preset query answer set === literal-window query answer set'.", "row": "fakes: a relative window resolved against a different instant than the expectation was computed from. counter: record the wall clock BEFORE the first request, derive every expectation from that one instant, reconcile against a literal-window query from the same clock, re-run any window whose boundary the run crossed." }, "timezone-boundary": { "items": ["api-backend.date-range-preset-matrix"], "evidence": "Same item and same step, which names the ZONE distinctly from the instant ('wall clock AND timezone'). Split the way the item splits it: the instant the window resolved against (clock-skew) vs the zone the calendar window is anchored in (timezone-boundary).", "row": "fakes: rows near midnight or a period edge landing on the other side of a calendar window anchored in a different zone than assumed. counter: record the zone with the clock, compute the expected window in the zone the door resolves in, score against the literal-window query, never against an intuition about 'today'." }, "silent-coercion": { "items": ["api-backend.filter-comparand-conformance"], "evidence": "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 negative 'a rejected predicate that is DROPPED rather than refused -- the request answers 200 over an unfiltered set'. Counter lifted verbatim from clause 2's evidence field: 'status + message + the returned row count vs the unfiltered count'.", "row": "fakes: a 200 that reads as the contract holding while the input was converted, or the predicate dropped entirely. counter: never score on status alone -- compare what came back against what was sent, and the returned row count against the UNFILTERED count." }, "destructive-in-place": { "items": ["cli.migrate-meta-codemod"], "evidence": "Fixture requires 'a scratch copy of an authored source tree'; step 3 records 'a per-file checksum of the scratch tree BEFORE the run'; step 6 re-checksums; clause 2 asserts 'the authored sources are UNTOUCHED' with before/after checksums as its verify; negatives include 'a run that MUTATES the authored sources'. The other half from the fixture note: '--stored --apply IS a real write -- to this deployment's sys_metadata ROWS, not to files'.", "row": "fakes: a clean re-run -- the first run already rewrote the input it is judged against. counter: scratch copy / a DB you own; checksum or snapshot before and after and cite the pair; check which arm actually writes before replaying -- a preview arm is read-only only until --apply.", "note": "Triage floated folding this into a documented sibling (disposition C). It folds into none: no existing row covers an operation that consumes its own fixture. Defined rather than folded." }, "first-boot-cold-start": { "items": ["cli.scaffold-console-first-paint"], "evidence": "The only 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 makes a runner file exactly that defect when the cause is the cold boot. Paired on the item with hydration-race, which already covers the CLIENT-side settle; this row covers the server/tree-side one. Mechanism verified in source, not 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; seed-admin 'only acts on a zero-user DB' -- one-time work no warm re-boot repeats.", "row": "fakes: a 404 / empty body / white screen from a first boot still doing its one-time work, read as the first-run-only defect the item hunts. counter: read the boot log and wait for the server's own serving line before the first probe; do not write a first-run defect down before re-probing a warm boot." } }, "left_undocumented": "None. All eight were recoverable from their items -- every one had explicit step/knownGap/negative/verify text naming the hazard, and the two thinnest (cache-staleness, first-boot-cold-start) are recorded above with how the residual ambiguity was resolved rather than papered over.", "gates_traps_values": "NO. scripts/check-platform-checklist.mjs enforces closed vocabularies for status/priority/surface/oracle/blocked.by (`if (!ORACLES.has(c.oracle))`, line 131) but the string `traps` does not appear in the file at all. That is why the 8 drifted in, and a TYPO in a documented value (hydration-race is on 79 of 205 items) lands the same way. Filed as #10647 and linked as a sub-issue of #10416. NOT built here per the brief: 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. That is a design decision, not a one-liner.", "tests": "All at 64776fbf39, the final commit on the branch. GATE UNION derived with `node scripts/pm/dispatch-gates.mjs` and NO paths passed: EXIT=0, change set 'docs/qa/platform-checklist/RUNNER.md' vs merge base 6b0be0220, 123 families across 26 workflows, verdict line '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 EMPTY union is not the whole truth (the script says `silent` is its weakest claim), so I re-derived by hand against the docs gates: check:role-word ROOTS=['content/docs','skills'] no; check:doc-anchors sweeps content/** + README.md + ARCHITECTURE.md no; check:adr-links/check:adr-anchors ADR_DIR='docs/adr' no; check:doc-authoring ROOTS=['.claude','docs','skills','content'] with walk() descending docs/ and skipping only docs/{audits,handoff,plans} -- YES, it reads this file. Ran it: exit codes captured BEFORE any pipe, SELFTEST_EXIT=0 MAIN_EXIT=0, gate's own verdict lines 'check-doc-authoring self-test: scope wiring ... all hold.' and 'doc authoring guard: 389 files clean -- no bare metadata literals.' Standing families: check-nul-bytes.mjs NUL_EXIT=0, 'check-nul-bytes: OK (scanned 6211 text file(s) ... no raw ASCII control bytes).' -- re-run at the final commit, plus a manual grep -naP for control bytes over the edited file. checklist-select --self-test EXIT=0 ('17 cases pass'), check-platform-checklist.mjs EXIT=0 ('OK -- 15 areas, 205 items (205 active); coverage: 30 kinds mapped, 0 waived') -- quoted only to show the ledger is undisturbed; it never reads RUNNER.md, which is the gap #10647 covers. THE LOAD-BEARING ASSERTION, re-running the card's own derivation after the change: documented 19 / distinct in use 19 / used-but-undocumented 0 / documented-but-unused 0. No ablation applicable (docs-only, no build artifact and no test to mutate). DECLARED NARROWING: I routed check:doc-authoring through scripts/pm/os-verify-lock.sh first; it queued behind another agent's example-showcase build and a vitest run for 7+ minutes, so no VERDICT line was obtained. I ran the gate directly instead and cancelled my own queued waiter by task id (confirmed gone from the queue; no orphan). Reason: this card has NO heavy work -- its entire gate surface is three zero-dependency node scripts that walk files and contend for nothing, so the lock was over-applied, not skipped.", "open_questions": [], "out_of_scope_findings": [ "filed as #10647 (sub-issue of #10416): check:platform-checklist validates `oracle` against a closed vocabulary but never reads `traps` -- the drift that let the 8 in is still open, and a typo in a documented trap lands the same way. This is the remainder of triage's disposition B.", "filed as #10648: dispatch-gates cannot name check:doc-authoring for ANY card -- all four of its 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 (.claude/worktrees, docs/{audits,handoff,plans}). A required lint.yml gate reads a large live corpus while deriving zero hints for it; same class as #9626 / #10114 / #10314, and the fix pattern is already documented in check-doc-anchors.mjs." ], "brief_corrections": [ "The brief predicted 'there are docs gates in this repo (anchors, links, role-word) that may cover docs/qa/**'. FALSE as stated: check:doc-anchors, check:adr-links/anchors and check:role-word all scope to content/docs, content/** or docs/adr and none reads docs/qa/**. The one gate that DOES read it is check:doc-authoring, which dispatch-gates cannot name -- so the prediction was right that a docs gate applies, but wrong about which, and the derivation could not have told me: it returned an empty union. Hence #10648.", "The card's counts are reproduced exactly except items 204 -> 205 (one item added since 900e48935). All 8 trap names and all 8 usage counts are unchanged.", "CONFLICT, not silently resolved: the first-touch grading comment on #10416 chose disposition B (the 8 rows AND the checker). The dispatch brief scoped this PR to the 8 definitions and said to file the checker unless genuinely trivial. I followed the brief, assessed triviality against the actual validator source (it is not trivial -- see gates_traps_values), filed #10647 and linked it as a sub-issue of #10416 so closing this PR does not drop the B remainder. Noted in the PR body too." ] }
Generated by Claude Code
- added a commit that references this issue
on Aug 21, 2026 - added a commit that references this issue
on Aug 23, 2026
Found while rewriting
cli.migrate-meta-codemodfor #9733 (PR #10412). Filed unassigned, out of that card's scope and not touched there.The finding
docs/qa/platform-checklist/RUNNER.mdrule 3 instructs a runner, for every item, to:That only works if every value an item can carry in
trapsis defined somewhere. RUNNER.md's trap table is the definition list — each row gives the trap name, what it fakes, and the counter the runner applies to rule it out. Eight values in use have no row.Measured on
main(900e48935), by parsing everydocs/qa/platform-checklist/areas/*.jsonfortrapsvalues and matching them against the trap table's rows:absence-inference,automation-input,dispatcher-vs-hono-route,hydration-race,seed-data-thin,shared-browser-tab,single-datapoint,stale-console-bundle,stale-dist,wrong-panel,wrong-persona.auth-state-leakcache-stalenesssilent-coercionclock-skewtimezone-boundaryeventual-consistencydestructive-in-placefirst-boot-cold-startMost are guessable from the name, which is exactly what makes this quiet: a runner who guesses wrong rules out the trap they imagined rather than the one the item's author meant, and the verdict still reads
pass.silent-coercionandeventual-consistencyin particular imply specific counters (compare the stored value against what was sent; re-read after a settle window) that a name alone does not supply.scripts/check-platform-checklist.mjsdoes not validatetrapsagainst any vocabulary, so nothing catches a new undocumented value — or a typo in a documented one, which lands as a silently-unrulable trap the same way.Suggested dispositions (triage's call)
check:platform-checklistverify everytrapsvalue has a RUNNER.md row, so the table and the ledger cannot drift again. This is the same shape as the existingoracleenum check (which the validator does enforce) and would have caught all 8. Note the validator is not CI-wired by maintainer decision, so this gate fires on the manual cadence.destructive-in-placeis used by exactly one item and overlaps nothing today).Refs:
docs/qa/platform-checklist/RUNNER.md(rule 3 + the trap table) ·scripts/check-platform-checklist.mjs(theoracleenum check is the precedent for B) · #9733 / #10412 (where this surfaced).Generated by Claude Code