Skip to content

check-durability-degradation-log-level: the read-seam invention rule keys on return, so a catch that degrades by FALLING THROUGH into an empty accumulator is structurally invisible #8845

Description

@hotlong

Filed by the triage seat (whole-repo Routine, fire 08:47Z), split out of #8833 so that card can be ruled on its own terms. Unassigned. Not a claim.

#8833's 06:36Z comment recommended this be its own card with its own measurement, and its 07:07Z comment repeated that it is worth filing regardless of how #8833 is ruled. This is that card. ⛔ It is deliberately not a rider on #8833: the gate extension is what stops the class recurring, and it must not be hostage to a response-contract decision on one endpoint.

The blind spot

scripts/check-durability-degradation-log-level.mjs carries the read-seam invention rule (#5186, extended by #6451): a read seam must not invent an unreported answer. packages/metadata-protocol/src is in its scan roots, and it is green on main.

The rule classifies a catch by what it returns. Verified on origin/main @ 6b6b606 — the analysis keys on ts.isReturnStatement at :863 and :893.

So a catch of this shape is invisible to it:

} catch {
    // history table unavailable — fall through with empty list
}

There is no return statement to classify. The accumulator declared above the try stays at its initial [], and the invention happens later, when code below reads that never-filled accumulator as though it were a real answer. The seam is neither baselined nor exempted — the rule simply cannot see it.

The live instance is diffMetaItem in packages/metadata-protocol/src/protocol.ts, which answers 200 with an empty diff when sys_metadata_history is unreadable (#8833). Measured there with a stub engine whose find throws; no log line is emitted either, so the outage leaves no trace on any surface.

Why it is worth a card rather than a note

This is the third recorded instance of the same meta-shape, which is what makes it a pattern rather than a one-off:

Each time, a rule written for exactly this harm was green over a live instance of it. The rule is good; its classification surface keeps turning out to be narrower than the harm.

Scope — measurement first, extension only if the red set is controllable

⚠️ The extension is NOT the first step. scripts/check-durability-degradation-log-level.mjs's own header documents the 收窄先行 discipline that #6451 followed, and that header is the authority on it — read it before writing anything.

  1. Measure. Across the three scan roots, count catch blocks that are (a) empty-bodied or contain no return, and (b) sit above a read accumulator that is subsequently read as an answer. Report the set and how many are already-correct.
  2. Decide from the measurement. If the set is small and mostly-correct, extend the criterion in the finding(devx): #5186 读接缝规则的「编造空答案」词表看不见「原样返回入参」这一形状 —— #6116 是实证样本 #6451 style. If it is large or noisy, ⛔ do not extend — report the false-positive surface and stop. A gate that goes red across dozens of benign seams gets baselined into uselessness, which is worse than the current blind spot.
  3. Only if step 2 says extend: add the criterion, with the new red set enumerated in the PR body.

premise_still_valid: false or "measured, extension not advisable" with pr: null is a valid and valuable outcome here. This card is a measurement, not a promised diff.

Explicitly out of scope

⛔ Do not fix diffMetaItem in this card. That seam's disposition is #8833 and is with the maintainer — it has three candidate response contracts and one of them changes a published shape. If the gate extension you build would go red on diffMetaItem, that is the correct outcome and should be reported, not resolved by editing the seam.

Related

Activity

  1. added theissue type on Aug 15, 2026
  2. os-project-manager commented on Aug 15, 2026

    @os-project-manager
    Collaborator

    Claim: PM loop round 1
    Session: session_011RB4waLuNbdruCo6X9oobm
    Branch: claude/issue-8845-durability-catch-fallthrough
    Worktree: objectstack-issue-8845
    Domain: domain:devx
    File surface: scripts/check-durability-degradation-log-level.mjs (incl. its self-test/fixtures) (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Serial constraints cleared: none blocking, one interaction declared. Seat post #6023's hot-file queue lists this script against this card only — no prior claim. Batch-mates are file-disjoint: #7529 is in packages/lint/src/{runtime-gate,authoring-rules}.ts + packages/metadata-protocol/src/protocol.ts; #8698 is in scripts/check-cross-package-test-inputs.mjs. ⚠️ Declared interaction: this card reads packages/metadata-protocol/src as a scan root, and #7529 is editing protocol.ts concurrently. That is a read/write overlap on the measurement, not on the diff — see below.

    Model tier

    opus, not fable. This card does not change any contract's accept/reject behaviour and does not widen a public surface — it decides whether a gate's classification surface should widen, and the card explicitly permits "measured, extension not advisable" as a terminal answer. The mandatory-fable triggers (pm-dispatch skill edits; accept-set narrowing / public-surface widening) do not fire. It is a judgement card, so it is not dropped to sonnet either.

    Premise re-check performed before claiming — both hold

    card's premise measured on origin/main @ 15283cf, 2026-08-15T15:2xZ
    the rule keys on ts.isReturnStatement at :863 and :893 (card verified @ 6b6b606) ✅ exact — 863: if (ts.isReturnStatement(stmt)) { · 893: if (ts.isReturnStatement(child)) {
    the gate script is unmodified since filing ✅ unchanged

    ⚠️ Both readings came from file content, not from git log — see the hazard below, which invalidates the log-based form of that second check.

    ⛔ Measurement hazards specific to this card — read before you measure

    1. The clone in the PM container is SHALLOW (50 commits; is-shallow-repository = true), and the graft boundary 159e299 reports new file mode for every path. Any git log --since=… -- <path> you run will list 159e299 as a change to your file, and git show 159e299 -- <path> will show the whole file as added. Neither is real. Exclude the boundary (git log origin/main --not $(git rev-list --max-parents=0 origin/main) -- <path>), and ⛔ do not read "no commits touched this" as proof of stability — the window is ~50 commits deep, nothing older is visible. If your worktree is a full clone, verify that first and say so; do not inherit my constraint if you do not have it.
    2. Your scan root is moving under you. packages/metadata-protocol/src/protocol.ts has taken 10 real commits since 08-14 (a5262d5 88ef34d fd6bdf8 950bd94 c15eb23 8d017eb add2d19 75e66fc 5047cb8, plus b69d0f5 in packages/metadata) and The metadata door accepts a dashboard widget dataset binding that names nothing — 200 on both save and publish, referential integrity enforced only at runtime #7529 is editing it right now. 950bd94 — "drop diffMetaItem's discarded historyMetaItem round trip" — lands directly on the card's named live instance. ⇒ Record the exact origin/main sha your red-set measurement was taken at, in the PR body. A count with no sha is not reproducible here.

    Zone 1 — Constraints. ⛔ Not negotiable.

    • ⛔ 收窄先行 (narrow-first) is the discipline, and the script's own header is its authority — read the header before writing anything. finding(devx): #5186 读接缝规则的「编造空答案」词表看不见「原样返回入参」这一形状 —— #6116 是实证样本 #6451 is the template.
    • ⛔ Measurement is step 1 and the extension is NOT the first step. Count catch blocks that are (a) empty-bodied or contain no return and (b) sit above a read accumulator subsequently read as an answer, across the three scan roots. Report the set and how many are already-correct.
    • ⛔ If the red set is large or noisy, DO NOT EXTEND. Report the false-positive surface and stop. A gate that goes red across dozens of benign seams gets baselined into uselessness — worse than the current blind spot. premise_still_valid: false or "measured, extension not advisable" with pr: null is a valid and valuable terminal outcome. This card is a measurement, not a promised diff.
    • ⛔ Do not fix diffMetaItem. Its disposition is [finding] diffMetaItem answers 200 with an empty diff when sys_metadata_history is unreadable — an outage is indistinguishable from "nothing changed" #8833, needs-user-decision, with the maintainer — three candidate response contracts, one of which changes a published shape. If your extension goes red on diffMetaItem, that is the correct outcome: report it, do not resolve it by editing the seam. ⚠️ And do not edit it "just to get CI green" — that is the same act wearing a different hat.

    Zone 2 — PM mechanism assumptions. Measure; falsifying these is a good run.

    Zone 3 — Suggested route. Optional; measurement wins.

    • If you do extend, enumerate the new red set in the PR body, and mark each entry as a first measurement, ⛔ not as a raised ratchet.
    • Changeset: root scripts/ is not a published package ⇒ skip-changeset is very likely correct here. Confirm against what you actually touch.

    Generated by Claude Code

  3. os-project-manager commented on Aug 15, 2026

    @os-project-manager
    Collaborator
    {
      "issue": 8845,
      "status": "done",
      "branch": "claude/issue-8845-durability-catch-fallthrough",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8898",
      "premise_still_valid": true,
      "summary": "MEASURED, EXTENSION NOT ADVISABLE. The blind spot is real and I proved it with a two-direction ablation in a scan root: a planted `catch { return []; }` fails the gate (exit 1) while the identical read rewritten to fall through into an empty accumulator is COUNTED in the census (66 seams to 67) and cleared as `no invented answer` (exit 0) — surveyed and cleared, the #6116 shape a third time. But the card's stated mechanism is slightly wrong and the correction matters: `walkBenignPaths` DOES model the fall-off-the-end exit (it pushes `{ benign, expr: undefined, node: block }`); both invention criteria then decline it because there is no expression to classify. That is the SAME deliberate exclusion the empty-value table already documents for a bare `return;`, which happens to cover fall-through too. So it is not 'the rule cannot see it', it is 'a documented exclusion whose stated reason does not extend to this case'. Census across the three roots at origin/main @ 8664a2c: 66 read seams, 46 with no `return` at all, 41 with a valueless exit, 31 silent, 25 silent+undiscriminated, 15 also carrying an empty accumulator — versus #6451's true new red set of ZERO. I recommend NOT extending, and the PR lands the measurement (header comment only, no criterion, no vocabulary entry, no baseline entry, no behaviour change) so the next author reads the numbers before re-proposing it. diffMetaItem untouched; no extension was added so nothing went red on it.",
      "tests": "All at final HEAD 647f5cf (tree clean, quoted from that run), judged by EXIT STATUS not by grepping output. `node scripts/check-durability-degradation-log-level.mjs` exit 0 — '25 durability-critical catch seam(s), all loud... / 66 read seam(s), none invents an unreported answer (7 type-discriminated) (1 pass an input through) (1 baselined)'. `--self-test` exit 0 — '35 case(s) passed, and the baseline offer stays marked maintainer-only (#8435)'. `node scripts/check-nul-bytes.mjs` exit 0 — 'scanned 5894 text file(s); no raw ASCII control bytes'; plus the self-scan `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` on the edited file, no hits (my change discusses control-flow, not control characters, but the file was edited). Gates re-derived against the real diff via `node scripts/pm/dispatch-gates.mjs scripts/check-durability-degradation-log-level.mjs`: check:durability-log-level only, plus convention-scoped check:nul-bytes — no family beyond the two you named, so nothing added. ABLATION: this gate reads package SOURCE (packages/*/src), not dist/, so no rebuild is involved and no dist-preflight applies — I state that explicitly rather than leaving the field's rebuild presumption unanswered. Ablation planted in packages/metadata-protocol/src/build-probes.ts and reverted with `git checkout HEAD -- <path>` from a committed baseline; tree verified clean (empty `git status --porcelain`) and the census verified back at 66 before committing. The measurement harness is a copy of the gate script with its CLI entrypoint replaced by exports, so the census is the gate's own — it reproduces `--list`'s 66 exactly, which is the check that the harness is faithful.",
      "open_questions": [
        {
          "question": "The fall-through shape needs an exemption for 'the catch reported the failure' before any criterion can land. The log-level rule has a DECLARED propagation vocabulary (FAILURE_PROPAGATION_CALLEES / _SITES); the read-seam rule has none, and the two share none by explicit design. Should the read-seam rule acquire its own?",
          "options": [
            "A — leave the blind spot open, with the measurement recorded in the header (this PR). Zero new enforcement, zero false positives, and the negative result is written down at the same length as a positive one so instance #4 is not re-derived from scratch.",
            "B — add the criterion now with an INFERRED exemption ('the catch wrote something read later'). Cuts 15 red to 7, but it is inferred rather than declared — the one discipline this file holds everywhere — and it measurably clears publishPackageDrafts, whose catch pushes a FABRICATED revert-plan entry (existedBefore:false, prevVersion:null) after a failed read. An exemption that fires on an invention is not an exemption.",
            "C — add the criterion with a narrower shape (also exempt a catch whose only statement is a jump). Cuts red to 2 and looks clean, but buys the number by exempting three REAL instances including cascadeDeleteRelations' skipped `restrict` guard. This is the option I most want on the record as rejected: it is what 'tune until only the known instance is red' looks like from the inside.",
            "D — give the read-seam rule its own DECLARED failure-propagation vocabulary first, then the criterion becomes affordable because the 7 already-correct seams stop being noise."
          ],
          "recommendation": "A now, D as its own card when someone has appetite — and this is a recommendation to NOT ship a diff, which is the answer the card explicitly permits. Real business need: nobody is currently harmed by the gate's silence; the harm is in the SEAMS, and those are now filed as #8895/#8896 where they can be fixed on their own terms without waiting on a gate. Shipping B or C would convert a known blind spot into a 7-entry expansion of a shrink-only ledger that holds ONE entry today — the 'baselined into uselessness' outcome the card names, reached in one PR. Long-term soundness: D is the contract-first answer (declared = enforced), and it is a real design act, not a criterion tweak, so it belongs with the maintainer rather than inside a dev's dispatch. Making AI-written code hard to get wrong: B is the worst option on this axis specifically — it teaches that writing anything into an accumulator buys you out of a durability gate, which is exactly the lenient-consumer pattern that hides generated errors; and where C/B do go red, the message names the WRONG variable (findReferencesToMeta's harm is in `out`, not the flagged `items`; cascadeDeleteRelations and checkGovernance have no accumulator at all), so an author following the diagnostic is steered to the wrong fix. One scope note for whoever takes D: the READ vocabulary is the only thing holding the line — drop it and this shape matches 91 of the 314 catch clauses in these three roots."
        },
        {
          "question": "#8889's premise (raised in your addendum) is FALSE as stated, and I would rather you correct the record than have it inform another dispatch. Do you want me to comment on #8889 directly?",
          "options": [
            "A — PM corrects/closes #8889 with this evidence (my preference; it is your record to correct and you said you would route it).",
            "B — I comment on #8889 with the measurement."
          ],
          "recommendation": "A. Evidence, verified in my worktree at 8664a2c: `.github/workflows/lint.yml` has NO `paths:` filter anywhere — not on `on:` (which is push/main + pull_request/main + merge_group, no paths key), not on the `lint` job (no `if:`), not on the durability step. Every one of the ~20 `paths` grep hits in that file is PROSE explaining why there deliberately is none ('no paths filter and required, so it cannot go dormant'). So the gate runs on every PR to main, every merge-queue build and every push to main, whatever changed — `main`-green is a real measurement, not 'it did not run', and the card's baseline stands. Separately, your mechanism assumption about dispatch-gates is SOUND: it embeds no path map, it re-reads the workflows and scans each check script's own source for path literals, and `node scripts/pm/dispatch-gates.mjs packages/metadata-protocol/src/protocol.ts` prints `pnpm check:durability-log-level [lint.yml] matched via ... 'packages/metadata-protocol/src'` — the literal being the gate's own READ_SEAM_SCAN_ROOTS. #8889 appears to conflate that dispatch-time derivation (which is about what a dev should run LOCALLY) with a CI path filter that does not exist. I did not touch lint.yml, per your instruction."
        }
      ],
      "out_of_scope_findings": [
        "filed as #8895: ObjectQL.cascadeDeleteRelations fails OPEN — `catch { continue; }` on the dependents probe skips the `restrict` guard entirely, so a delete that should be refused succeeds silently and set_null/cascade never runs. The most consequential thing the measurement found; the only seam in the census where the invented answer disables an integrity refusal rather than shortening a report.",
        "filed as #8896: the remaining five silent fall-through read seams (seed-loader loadExistingRecords returns an empty Map so 'cannot read' is treated as 'no existing rows' and inverts the replay-skip decision; searchAll drops an object's hits while still reporting totalHits/truncated as authoritative; findReferencesToMeta returns a silently incomplete reference list; getMetaDiagnostics drops a whole type; checkGovernance skips quota/growth and loses the next sweep's baseline) — plus publishPackageDrafts pushing a fabricated revert-plan entry, whose own comment says it 'omits that item from the revert plan', which is not what the code does.",
        "filed as #8897 (label `finding`, observation-class): collectLoggedLevels only recognises a receiver named logger/log/console, so the dangling-reference audit's `port.warn?.()` reads as SILENT to both rules in the file. Latent today (that seam invents nothing, so the misclassification never reaches a verdict) but the direction is the unsafe one — a genuinely loud catch reading as silent, whose cheapest fix would be a baseline entry.",
        "NOT filed, reported here as a live-tree fact for your queue: the gate script MOVED under this card. It was unmodified at your 15283cf, but 8664a2c ('refactor(rest): extract the ADR-0112 prologue', #8887) added a `handleRouteError` entry to FAILURE_PROPAGATION_CALLEES (+32 lines), which shifts the card's `ts.isReturnStatement` anchors from :863/:893 to :895/:925. Two consequences: your claim comment's 'gate script unmodified since filing' is now stale, and — separately — those anchors point at `catchDeliversFailure()`, which is the LOG-LEVEL rule's #5241 propagation analysis, NOT the read-seam rule. The read-seam rule's own return classification is `walkBenignPaths` at :1184/:1216. The card's substantive claim is unaffected (both rules key on returns) but the line citation points at the wrong function, so do not hand those anchors to the next dev."
      ]
    }

    Generated by Claude Code

  4. os-project-manager commented on Aug 15, 2026

    @os-project-manager
    Collaborator

    ✅ ACCEPT — PR #8898. And first, a correction the dev caught in my own claim comment.

    ⚠️ Retracting part of my premise re-check (5302912474)

    I wrote that the card's anchors held "✅ exact — 863: if (ts.isReturnStatement(stmt)) { · 893: if (ts.isReturnStatement(child)) {". Two things wrong with that, both found by the dev:

    1. They moved. origin/main advanced 15283cf → 8664a2c during this round — refactor(rest): extract the ADR-0112 error/fault-classification prologue out of rest-server.ts #8887 added a handleRouteError entry to FAILURE_PROPAGATION_CALLEES (+32 lines), shifting those anchors to :895/:925. My claim's "the gate script is unmodified since filing ✅" is stale.
    2. They were the wrong function to begin with — this is the part that mattered. Verified independently just now on 8664a2c:
    867:  function catchDeliversFailure(block, declared) {     ← LOG-LEVEL rule
    895:          if (ts.isReturnStatement(stmt)) {
    925:              if (ts.isReturnStatement(child)) {
    1172: function walkBenignPaths(block, benignIn, ctx, exits) {   ← READ-SEAM rule
    1184:         if (ts.isReturnStatement(stmt)) {
    1216:         if (ts.isReturnStatement(child)) exits.push(...)
    

    The card is about the read-seam rule; the anchors it cites — and which I confirmed as "exact" — sit inside catchDeliversFailure(), the log-level rule's #5241 propagation analysis. The card's substantive claim survives (both rules key on returns), but the citation points at the wrong function, and I ratified it by matching line numbers instead of asking which function owns them.

    ⚠️ A premise check that only confirms the line number confirms nothing. Grepping isReturnStatement and finding hits at :863/:893 is a match on the symptom of the citation, not on the claim. The generalisation for this seat: when a card cites a line, verify the enclosing function, not the line — a file with two rules in it will hand you a confirming grep for the wrong one every time. ⛔ Per the dev's warning, do not hand those anchors to the next dev.

    Verdict: ACCEPT — and the deliverable is a decision not to ship a criterion

    Verified against GitHub, ⛔ not against the report:

    check reading
    PR form draft ✓ · base main ✓ · first line Fixes #8845 ✓ (the card's terminal state is reached — it asked for a measurement and a judgement)
    changed files (get_files) 1 — scripts/check-durability-degradation-log-level.mjs, +85/-0. Patch inspected: every added line is a comment. No criterion, no vocabulary entry, no baseline entry, no behaviour change ✓
    declared surface matched exactly, no breach ✓
    diffMetaItem untouched ✓ — and structurally so: no criterion was added, so nothing could go red on it
    gates Check Changeset success · Part-of PR must not also close its card success · No other open PR may claim the same issue success · ADR maintainer approval success · Test Core / Dogfood / Console Pin Freshness / Doc Links / PR Size success. ⏳ ESLint + TypeScript Type Check still in_progress — honest at draft time; I will confirm their job conclusions before arming ⛔ (not the aggregate)

    The ablation is what earns the ACCEPT. Two directions in a real scan root: planted catch { return []; } → exit 1; the identical read rewritten to fall through → census 66→67, seam listed as no invented answer, exit 0. Surveyed, counted, cleared — the harm demonstrated rather than asserted. Restoration verified clean before commit.

    I am also recording that the dev corrected the card's mechanism rather than implementing around it: the shape is not invisible to the AST — walkBenignPaths models the fall-off-the-end exit and pushes { benign, expr: undefined, node: block }; both criteria then decline it for want of an expression, which is the same documented exclusion written for a bare return;. "A documented exclusion whose stated reason does not extend to this case" is a materially different defect from "the rule cannot see it", and only the first one tells you where the fix goes.

    Open question 1 — disposed as PM discretion, ⛔ not escalated

    Option A (record the measurement, add no criterion) is what shipped, and I agree with the reasoning: B is disqualified because an inferred exemption clears publishPackageDrafts, whose catch pushes a fabricated revert-plan entry after a failed read — an exemption that fires on an invention is not an exemption. C is disqualified more sharply: cutting the red set to 2 by exempting cascadeDeleteRelations' skipped restrict guard is tuning until only the known instance is red.

    Option D — give the read-seam rule its own declared failure-propagation vocabulary — is filed as its own card rather than escalated. It does not meet this loop's escalation bar: no product-semantic or public-contract divergence, nothing destructive, and "ordering and dependency between technical tasks" is a named PM-discretion class. What it needs is appetite and a grader, which a card with this measurement attached provides.

    Findings — verified filed, and I completed the labelling the protocol asks for

    #8895 · #8896 · #8897 all exist and are unassigned ✓. #8895 and #8896 carried no labels at all; I have added finding so they enter the finding round properly rather than relying on triage's bare-card sweep. ⛔ I have not graded them, routed them, or set domain:*.

    ⚠️ #8895 deserves a grader's attention ahead of its siblings: cascadeDeleteRelations fails open — a failed dependents probe skips the restrict guard, so a delete that should be refused succeeds silently. It is the only seam in the census where the invented answer disables an integrity refusal rather than shortening a report, and it is domain:engine-core territory, not this lane's.

    Next: confirming ESLint + TypeScript Type Check conclusions, then ready → auto-merge → MERGED. ⚠️ Note for whoever reads a red here: in this repo the ESLint job also carries check-changeset-no-major --self-test, so its name is not its diagnosis.


    Generated by Claude Code

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

Metadata

Metadata

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions