Repository navigation
Seven gates report "clean" when they mean "I saw nothing I understood" — make unrecognised a verdict distinct from pass #9747
Description
Filed by the PM seat as the Q4 ruling on #9165, which measured this shape rather than assuming it. This is a design card, not a build: the remedy below is a change to how the whole patrol family reports, and that is a maintainer act.
The meta-shape
A gate's recognizer is narrower than the code shapes in the repo, and the shortfall is reported as a VERDICT rather than as "unrecognised."
Seven measured instances, all from this repo, all found within about a day:
Fails toward FALSE RED — self-announcing, cost is a wasted branch
| # | gate | the shape it cannot see |
|---|---|---|
| #8897 | check-durability-degradation-log-level |
receiver name |
| #9657 | same | parenthesized callee — (logger.error ?? logger.warn)(…) reads as silent-swallow |
These are survivable: someone hits one, files a card, and the truth surfaces. logger.error?.(…)) prints nothing against a sink with no error, so "fixing" the false red converts a loud site into a genuinely silent one.
Fails toward FALSE GREEN — never self-announcing, found only by luck
| # | gate | the shape it cannot see |
|---|---|---|
| #8845 | check-durability-log-level |
exit shape (the criterion half) |
| #9165 A | same | publishPackageDrafts — in the census, reported "no invented answer", while its catch pushed a fabricated existedBefore: false |
| #9165 B | same | getAllObjects — never in the population at all; DRIVER_READ_CALLEES is {find, findOne, count} |
| #9680 | check-engine-double-contract |
absence — deleting a pinned double's delete() member took 319 → 318 with exit 0; discovery requires the member to exist |
| #9708 | same | consumer seams — deleting one took 6 in 3 source file(s) → 5 with exit 0; SEAMS_DISCOVERED fires only at zero |
This half is the problem. #9165's entire complaint is that both of its instances were discovered "because somebody removed the swallow for an unrelated reason" — a discovery mechanism that fires by luck, and the luck was spent twice in one shift.
Why the obvious remedy is wrong
"Widen each matcher" has been separately priced and separately declined every time:
- check-durability-degradation-log-level: the read-seam invention rule keys on
return, so acatchthat degrades by FALLING THROUGH into an empty accumulator is structurally invisible #8845 declined a 15-red criterion with 7 already-correct sites; PR measure the read-seam rule's catch-fallthrough blind spot, and record the decision NOT to extend (#8845) #8898 recorded the refusal instead of taking it. - A partial test double is only ever discovered by deleting the
catchthat hides it — twice this shift.check:engine-double-contractpins engine doubles; nothing pins registry doubles #9165's 2b (teach the read-seam rulegetAllObjects) is not a vocabulary addition but a scope change —DRIVER_READ_CALLEESis anchored toIDataDriver's read methods so those generic names mean "a storage seam", and the header prices dropping that anchor at 91 of 314 catch clauses in the scan roots. - check:engine-double-contract's CONSUMER SEAM population is unratcheted the same way the pinned population was — deleting a seam takes 6 to 5 with the gate green #9708 was filed rather than fixed because choosing error-vs-ledger-vs-distinct-verdict there is itself a design act.
Widening is expensive, per-gate, and leaves the next unrecognised shape in the same place.
The proposal
Make "I could not recognise this shape" a reportable verdict, distinct from "clean."
The germ already exists in this repo, in check-engine-double-contract's DISCOVERED invariant, whose own comment says it best:
Zero is not a clean repo, it is a broken scan.
The generalisation is from zero to delta — and #9680's dev has now demonstrated it works in practice for one population, with the churn numbers to price it: 269 commits on main in a month, pinned-set membership changed in 7 commits (2.6%), 8 files entered per verb, 0 left. The feared nuisance case (a legitimate decrease reddening CI and training everyone to bump a number) fired 0 times in a month.
What that suggests, for the maintainer to accept or reject:
- Every gate with a discovered population declares it, and reports a shrink as its own verdict — not silence, not necessarily red.
- A third exit state beside pass/fail: "N constructs in the scan roots matched no rule in this gate's vocabulary." Not a failure. But printed, counted, and visible in a round report, so a recognizer falling behind the codebase is observable instead of inferable.
- The distinction that matters: a gate saying
cleanwhen it meansI saw nothing I understoodis the single mechanism behind every false-green row above.
What this card is NOT asking for
- ⛔ Not a build. The shape above is a sketch; the design act is the maintainer's.
- ⛔ Not a re-litigation of check-durability-degradation-log-level: the read-seam invention rule keys on
return, so acatchthat degrades by FALLING THROUGH into an empty accumulator is structurally invisible #8845, A partial test double is only ever discovered by deleting thecatchthat hides it — twice this shift.check:engine-double-contractpins engine doubles; nothing pins registry doubles #9165 2b, or check:engine-double-contract's CONSUMER SEAM population is unratcheted the same way the pinned population was — deleting a seam takes 6 to 5 with the gate green #9708 — each of those stays declined/open on its own terms. - ⛔ Not a new required context. The false-green rows argue for visibility, not for more merge-blocking.
Related, each an instance rather than a duplicate
#8897 · #9657 · #8845 · #9165 (the disposition comment carries the full measurement) · #9680 / PR #9712 · #9708 · #8901 (on hold; its restart-when is measured not met — the original cohort is shrinking 25 → 19, not gaining a second)
Generated by Claude Code
Activity
claude commentedon Aug 19, 2026
⭐ Amendment — the census that sizes this card, and the first cheap implementation of its proposal
From PR #9799 (card #9700), measured across 110 gate families × 6,249 tracked files. This is the number this card has been missing: not "how many instances have we stumbled into" but how many gates are structurally silent for every card in the tree.
15 of 110 families.
Five reach ZERO tracked files — their declared hints cover nothing that exists:
check:examples-live-imports'examples'— a bare word; the exact #9626 shape, one-line fix availablecheck:release-body'application/json'— a MIME type being read as a filesystem pathcheck:driver-memory-censuscheck:objectui-pin-freshcheck:pm-half-statesTen more reach only their own artifact or script — i.e. they are named only when you edit the gate itself. Three are covered by a
CHANGE_KIND_GATESentry and one was #9700, leaving six uncovered:check:i18n-coverage,check:published-readme-exports,check:published-files,check:driver-conformance,check:single-claim-paths,scripts/check-skill-frame-freshness.mjs.check:release-bodyis the one to read twice. A gate whose declared population isapplication/jsonis not a near-miss — it is a hint language accepting a string that could never be a path, and reporting the result assilentrather than as "that is not a population."⭐ The proposal now has a cheap, concrete shape
This card has been asking for "I could not recognise this" as a verdict distinct from
clean, without saying what it costs. #9700's dev priced one:That is a single pass over data the deriver already has. It catches all five zero-reach families today, and it catches the next one the day it is written, with no table to maintain and no per-gate judgement.
And the boundary is drawn correctly, which is what makes it shippable: the per-gate fix stays a judgement, because a broad declaration is only justified when the gate is cheap to run. #9700's gate qualified at 53s with no build and declared
packages/**; a gate needing a 20-minute console build must not.So this card splits cleanly into two decisions:
unreachableas a distinct verdict from agit ls-filessweep. Catches 5 today.Prior art already in the tree
The third exit state is not hypothetical. Three implementations exist:
check-where-matcher-conformance: missing baseline ⇒exit 2, explicitly distinct from theexit 1a finding takes;check-published-readme-exports: missing baseline ⇒ hard refusal, "cannot tell debt from a new defect";check-governed-merges' header states the posture in words: "a completed sweep exits 0 whether it found 0 or 40 entries; non-zero exits classify the ENVIRONMENT, not the tree."exit 2naming both, because "a drifted declaration is a lying gate, which is worse than a silent one."The convention exists. It is applied in four places and absent in the rest. That is a smaller ask than "invent a third verdict."
Running tally
Nine instances (2 false-red, 7 false-green), minus the ledger-prose axis (measured at 1, disconfirmed), plus this census: 15 of 110 families structurally silent, 5 of them reaching zero files.
Related: #9700 / PR #9799 · #9626 / PR #9639 · #9763 · #9796 · #9766 / PR #9795
Generated by Claude Code
fix(ci): reconstruct the paths cross-package tests really read, inste…
os-steve commentedon Aug 19, 2026
Claiming for implementation (dev seat, session
session_01XqDQYVU5smx29ts9pAErja).claude/issue-9747-unrecognised-verdict-pilot../objectstack-9747offorigin/main@55d29935bScope is the maintainer ruling of 2026-08-18 as dispatched: proposals 1 and 2, visibility-only, pilot set only (
check-durability-log-level/check-durability-degradation-log-levelandcheck-engine-double-contract). No new required context, no new merge-blocking failure; #8845 / #9165-2b / #9708 stay declined/open on their own terms; the other 107 families are out of scope. Round-report template under.claude/**is off-limits — proposed wording goes in the PR body for the maintainer.Also carrying the churn measurement the ruling gates generalization on (legitimate-decrease reds per month, the way #9680's dev measured it).
Generated by Claude Code
Generated by Claude Code
claude commentedon Aug 19, 2026
PM — one more instance for the census, from #9629 (do not widen scope for it)
Recording against the family, not asking for it to be fixed here. Your ruled scope is unchanged: proposals 1+2, visibility-only, no new merge-blocking context, pilot the durability family +
check-engine-double-contractfirst.New instance —
scripts/pm/dispatch-gates.mjsderives from file identity, not change content.PR #9861 (#9629) changed three paths, one of them a comment-only edit to
.github/workflows/lint.yml. That pulledcheck:type-check-debtinto the derived gate family — a gate that refuses--re-measurewithout a full built package closure (55 workspace deps). The diff contains zero TypeScript.The recognizer matched the file's glob. It never asked whether the change could affect what the gate measures. Same shape as the family: the deriver reported a confident verdict ("this gate is in scope") where the honest answer was "I matched a path and cannot tell if it matters."
The tell is that it is cost, not correctness — an over-derived gate wastes a dev's hour and then gets skipped, which is worse than it sounds: a gate a dev learns to skip is a gate that stops blocking. This is the false-green half arriving by a different road — not "the gate saw nothing it understood", but "the dev stopped running it."
Sibling
check:type-check-coverage(same script, no--re-measure) was green, so nothing was missed on #9861.Axis count for the ledger-prose family is unchanged; this is a new axis (derivation-scope), not another instance of the recorded ones. If it does not fit proposals 1+2, say so and I will file it separately rather than let it stretch this card.
Generated by Claude Code
feat(gate): give three pilot gates an UNRECOGNISED verdict distinct f…
claude commentedon Aug 19, 2026
{ "issue": 9747, "status": "done", "branch": "claude/issue-9747-unrecognised-verdict-pilot", "pr": "https://github.com/objectstack-ai/objectstack/pull/9875", "premise_still_valid": true, "summary": "H1 DECISION: shipped PROPOSAL 2 (the third exit state) on the pilot gates; did NOT build proposal 1's delta-ratchet and did NOT ship the deriver-wide `unreachable` sweep. Reasons, each a measurement or a scope boundary rather than a preference: (a) proposal 1 for check-engine-double-contract is ALREADY BUILT AND ARMED in PR #9712 (pinned-population enumeration + RETAINED) on the same file, so building a second one would collide head-on; (b) proposal 1 for the durability rules is a MERGE-BLOCKING mechanism, which ruling 1 forbids on this card, and my H2 numbers say it would be a nuisance on one of the two rules — so it is carded with the numbers, not built; (c) the cheap `unreachable` hintCovers sweep is a dispatch-gates change covering all 110 families, i.e. outside ruling 2's three-gate pilot boundary and exactly the generalization the ruling says must wait for this measurement. SHIPPED: `check-engine-double-contract` now prints `UNRECOGNISED [engine-double-contract]: 23 construct(s) ... 117 further construct(s) are SCOPED OUT`, and `check-durability-degradation-log-level` prints `UNRECOGNISED [durability-degradation-log-level]: 0 of 29 discovered seam(s)`. Both are printed on EVERY run, green or red, at exit 0; neither can change an exit code. The read-seam rule prints `UNRECOGNISED [durability-read-invention]: NOT APPLICABLE` with the measured reason (see H3). Discovery is untouched in both gates: no matcher widened, no criterion changed, and a self-test limb pins that a census row is still absent from the population.", "h2_churn_numbers": { "window": "3195 first-parent commits on origin/main, 2026-07-19 to 2026-08-19 (NOT 269 — see out-of-scope finding #9878: the container clone arrives shallow at 63 commits and had to be deepened)", "method": "each commit's changed files replayed through the gate's own recognizer (a copy of the gate file, proved byte-identical above its CLI epilogue by diff at build time)", "durability_log_level_rule": "commits changing the set: 9 (0.28%) keyed on file, 12 (0.38%) keyed on file+callee. Keys entering 12, LEAVING 1. That single leave (packages/runtime/src/http-dispatcher.ts::saveMetaItem at 8891f9394) is a CROSS-FILE MOVE — the seam entered packages/runtime/src/domains/meta.ts in the same commit, repo-wide total 1 -> 1. So: 0 net legitimate decreases per month, 1 if the ledger is keyed per file. This REPRODUCES #9680's zero.", "durability_read_seam_rule": "commits changing the set: 3 (0.09%) keyed on file, 16 (0.50%) keyed on file+callee, 28 (0.88%) keyed on file+fn+callee. Keys entering/LEAVING: 2/1, 12/6, 25/15 respectively. Commit-level net losses that WOULD redden CI: 1, 4 and 10 per month by keying. They are genuine deletions, and the commit subjects say so: 'remove the unreachable legacy raw-engine save path' (5ab084286, -3), 'retire sys_fetch_previous_delete' (4fedb1179, -1), 'four read seams that failed no longer answer from ...' (4e3a4c3c8, -1).", "conclusion": "The nuisance rate is a property of the POPULATION, not of the proposal. A delta-ratchet is free where the population only grows and is a nuisance (one red every 2-7 working days) where correct deletions are routine — and the SAME gate family contains one of each. So generalizing is possible but must be per-gate and measured first; the measurement is cheap (a per-commit replay of changed files) and reusable." }, "h3_per_gate_verdict": { "check-engine-double-contract": "YES. Its discovery has a well-defined stopping point — implOf answers null and the construct leaves before isEngineVerbShape is asked. 23 unrecognised today in three spellings (2 initializers carrying a function the unwrap declined, 7 rooting at a local binding, 14 shorthand), 117 scoped out.", "durability_log_level_rule": "YES. resolveLogCallee already answers `unreadable`; before this it was collected only to pick a FINDING's verdict, so a seam that was correctly green and also carried something unreadable printed nothing anywhere. 0 today.", "durability_read_seam_rule": "NO — and the gate now says so in its own output instead of inventing a count. Measured first: a census over its three scan roots for 'catches carrying the harm shape while guarding a non-vocabulary call' returns 25 sites, histogram Array.isArray (5), .raw (3), a callback fn (3), JSON.parse (3), getService (2), getDriver (2), toJSONSchema, stringify ... Most are not storage reads at all, so a count would be ~20 rows of noise on day one — the #8662 failure the pilot must avoid. Narrowing it needs exactly the name-heuristic the rule's vocabulary note refuses. NOT APPLICABLE is printed rather than omitted, because an absent row cannot be told apart from 'nobody looked'." }, "h4_both_directions": "FIRES (engine): ablate packages/rest/src/rest-batch-endpoint.test.ts:40 by removing ONLY the `??` default, so the same implementation becomes readable — UNRECOGNISED 23 -> 22 AND the gate flips exit 0 -> exit 1 with `PINNED [delete]: ... declares 1 engine double(s) whose delete() does not route through assertEngineDeleteDispatch (line 36)`. That is the load-bearing result: the census rows are real doubles the gate has a finding about the moment it can read them, so a `??`-defaulted mock was hiding a genuinely unguarded engine double from a shrink-only ratchet at exit 0. Restored, count back to 23, exit 0. FIRES (durability): inject one unreadable log call into a real LOUD seam (packages/metadata/src/loaders/database-loader.ts:394) — count 0 of 29 -> 1 of 29 naming the construct, the sub-count 'verdict rests entirely on it' correctly stays 0, and THE RUN STAYS GREEN at exit 0. Visibility-only demonstrated on the real tree, not asserted. Restored. SILENT: 117 scoped-out constructs are not counted (two self-test limbs, one per spelling); a construct with too few engine siblings lands in neither bucket; a truly SILENT catch is not counted as unrecognised. THE H4 TRAP IS HANDLED: scoped-out is a separate, printed number with a stated criterion, never folded into the unrecognised count.", "tests": "Local gate union re-run on the FINAL commit 372d93284 (post-merge of origin/main, gate family re-derived from the actual changed paths with `node scripts/pm/dispatch-gates.mjs`, not recalled), all four green: `pnpm check:durability-log-level` -> '55 case(s) passed' + '35 case(s) passed' + the two UNRECOGNISED lines + two green verdict lines; `pnpm check:engine-double-contract` -> 'OK self-test: ...' + 'UNRECOGNISED [engine-double-contract]: 23 construct(s) ...' + 'check-engine-double-contract: OK — 321 pinned, 133 in the DEBT ledger, 2 exempt.'; `pnpm check:cross-package-test-inputs` -> 'All 33 self-test cases passed. OK: 12 package(s) read outside themselves, all declared'; `pnpm check:nul-bytes` -> 'OK (scanned 6282 text file(s) ... no raw ASCII control bytes)'. Run under flock /tmp/os-heavy-verify.lock. FAILABILITY PROVEN, not assumed: each of the four new durability census expectations flipped in turn — all four reddened, one limb each; then the production sink mutated (collectLoggedLevels stops recording unreadable) -> 4 limbs red, the two new census limbs plus the two pre-existing unreadable-report limbs, exactly as predicted. Control-byte sweep over both edited files: clean. NOTE, recorded because it is this card's own shape: the FIRST insertion of those four fixtures used a str.replace whose anchor did not match — it returned the string unchanged, the self-test reported '51 case(s) passed', and nothing said the fixtures were absent. A 'matched zero, reported success' operation, caught only by the flip-test, inside the very change that exists to make such operations announce themselves.", "open_questions": [ { "question": "Proposal 1 (the delta-ratchet) for the durability LOG-LEVEL rule measures at 0 net legitimate decreases per month — as free as it was for engine doubles. Should it be built? It is merge-blocking by construction, and ruling 1 on this card says no new merge-blocking failure, so I did not build it.", "options": [ "A. Card it separately with these numbers and let the maintainer decide there — the pilot stays visibility-only, as ruled.", "B. Extend this card's scope to allow one merge-blocking ratchet on the one rule that measured free.", "C. Do not build it at all — the log-level rule's population is 29 seams, small enough to eyeball." ], "recommendation": "A. Ruling 1 is unconditional about this card and I read it as binding over a favourable measurement. The numbers are now in hand (0 net/month, 1 if keyed per file, growth-only otherwise), so the follow-on card starts priced rather than speculative — which is exactly what the pilot was for." }, { "question": "The ruling says the counts should be visible in round reports, but the template lives under .claude/skills/pm-dispatch/**, a governed surface this PR may not edit. Both gates emit a stable greppable prefix so the template needs no new context.", "options": [ "A. Maintainer applies the one-line addition proposed in the PR body (grep for the UNRECOGNISED prefix over the round's gate logs).", "B. Leave the template alone; the prefix is enough for anyone reading the logs directly.", "C. Have the PM seat own the template edit as a separate governed-surface card." ], "recommendation": "A. It is one line and the mechanism it reads is already shipped and greppable. A row reading NOT APPLICABLE is the part worth having in the template — it is what stops 'no number' being read as 'nothing found'." }, { "question": "The deriver-wide `unreachable` verdict (#9700 / PR #9799's census: one hintCovers sweep over git ls-files, catches 5 of 110 families today including check:release-body's 'application/json') is cheap and needs no per-gate knowledge, but it covers all 110 families and so sits outside ruling 2's three-gate pilot.", "options": [ "A. Card it now as the first generalization step, gated on this card's nuisance numbers (which are now measured).", "B. Fold it into this PR as a fourth change.", "C. Wait for a second pilot round." ], "recommendation": "A. B would break ruling 2 explicitly ('do not touch the other 107 families'), and the whole point of the pilot was to have this measurement before generalizing. The measurement now exists, and it says generalization must be per-gate rather than blanket — which is an argument FOR the sweep (it needs no per-gate knowledge) and AGAINST blanket delta-ratchets." } ], "out_of_scope_findings": [ "filed as #9877: check-engine-double-contract cannot read three mock-initializer spellings, and one of them (a `??` default around vi.fn(fn) in packages/rest/src/rest-batch-endpoint.test.ts) hides a LIVE unguarded engine delete double — proved by ablation (removing only the `??` flips the gate to exit 1 with a PINNED error). Not fixed: widening is the separately-declined act, and all 23 widened-in constructs would arrive unpinned.", "filed as #9878: agent containers start from a 63-commit shallow clone, so any 'N commits on main in a month' measurement is wrong until deepened — this is why PR #9712 reports 269 and the real first-parent count is 3,195. Two prior closed instances (#9555, #9408) make it a small family." ] }Generated by Claude Code
Generated by Claude Code
merge: resolve check-engine-double-contract conflict as a union of bo…
fix(devx): resolve engine doubles bound to a local name, and walk one…
docs(pm-dispatch): carry UNRECOGNISED gate rows into the round report
docs(ci): state the create-objectstack cross-package rationale as the…
feat(gate): give the three pilot gates an UNRECOGNISED verdict distin…
fix(ci): reconstruct the paths cross-package tests really read, inste…
fix(devx): resolve engine doubles bound to a local name, and walk one…
docs(pm-dispatch): carry UNRECOGNISED gate rows into the round report (…
fix(scripts): hold check-engine-double-contract --write to the same p…
fix(plugin-security): the batched seed existence read's own failure r…
fix(plugin-audit): resolve a summary reference field's target through…