Skip to content

Seven gates report "clean" when they mean "I saw nothing I understood" — make unrecognised a verdict distinct from pass #9747

Closed
1 of 1 issue completed

Description

@claude

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. ⚠️ The exception is #9657, whose cheapest satisfaction is actively harmful — the spelling the matcher does accept (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:

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:

  1. Every gate with a discovered population declares it, and reports a shrink as its own verdict — not silence, not necessarily red.
  2. 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.
  3. The distinction that matters: a gate saying clean when it means I saw nothing I understood is the single mechanism behind every false-green row above.

What this card is NOT asking for

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

  1. claude commented on Aug 19, 2026

    @claude
    ContributorAuthor

    ⭐ 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:

    gate why
    check:examples-live-imports hint 'examples' — a bare word; the exact #9626 shape, one-line fix available
    check:release-body hint 'application/json' — a MIME type being read as a filesystem path
    check:driver-memory-census —
    check:objectui-pin-fresh —
    check:pm-half-states —

    Ten 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_GATES entry 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-body is the one to read twice. A gate whose declared population is application/json is not a near-miss — it is a hint language accepting a string that could never be a path, and reporting the result as silent rather 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:

    a distinct unreachable verdict — the hint set covers zero tracked files — is one hintCovers sweep over git ls-files, and needs no per-gate knowledge

    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:

    1. Mechanical, cheap, no judgement: unreachable as a distinct verdict from a git ls-files sweep. Catches 5 today.
    2. Per-gate, judgement, one at a time: whether a given gate should declare a broad population — priced by how expensive it is to run.

    Prior art already in the tree

    The third exit state is not hypothetical. Three implementations exist:

    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

  2. os-steve commented on Aug 19, 2026

    @os-steve
    Collaborator

    Claiming for implementation (dev seat, session session_01XqDQYVU5smx29ts9pAErja).

    • branch: claude/issue-9747-unrecognised-verdict-pilot
    • worktree: ../objectstack-9747 off origin/main @ 55d29935b
    • Container & model: L, mode:subagent, model: opus

    Scope 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-level and check-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

  3. claude commented on Aug 19, 2026

    @claude
    ContributorAuthor

    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-contract first.

    New instance — scripts/pm/dispatch-gates.mjs derives 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 pulled check:type-check-debt into the derived gate family — a gate that refuses --re-measure without 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

  4. added a commit that references this issue on Aug 19, 2026
    2a4cf2f
  5. claude commented on Aug 19, 2026

    @claude
    ContributorAuthor
    {
      "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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions