Repository navigation
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
Activity
os-project-manager commented
on Aug 15, 2026 CollaboratorMore actionsClaim: 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 inpackages/lint/src/{runtime-gate,authoring-rules}.ts+packages/metadata-protocol/src/protocol.ts; #8698 is inscripts/check-cross-package-test-inputs.mjs.⚠️ Declared interaction: this card readspackages/metadata-protocol/srcas a scan root, and #7529 is editingprotocol.tsconcurrently. That is a read/write overlap on the measurement, not on the diff — see below.Model tier
opus, notfable. 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-fabletriggers (pm-dispatch skill edits; accept-set narrowing / public-surface widening) do not fire. It is a judgement card, so it is not dropped tosonneteither.Premise re-check performed before claiming — both hold
card's premise measured on origin/main@15283cf, 2026-08-15T15:2xZthe rule keys on ts.isReturnStatementat:863and: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 fromgit 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
- The clone in the PM container is SHALLOW (50 commits;
is-shallow-repository= true), and the graft boundary159e299reportsnew file modefor every path. Anygit log --since=… -- <path>you run will list159e299as a change to your file, andgit 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. - Your scan root is moving under you.
packages/metadata-protocol/src/protocol.tshas taken 10 real commits since 08-14 (a5262d588ef34dfd6bdf8950bd94c15eb238d017ebadd2d1975e66fc5047cb8, plusb69d0f5inpackages/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 exactorigin/mainsha 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
catchblocks 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: falseor "measured, extension not advisable" withpr: nullis 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 ondiffMetaItem, 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.
- That the
catch-fallthrough shape is structurally invisible rather than merely unbaselined. The card asserts the seam is "neither baselined nor exempted". Verify — if it turns out to be exempted somewhere, the whole card changes shape. - That this is genuinely the third instance of one meta-shape (
check:durability-log-level结构性看不见「读接缝把故障答成空值」这一类 —— #4825 / #5108 全家都在闸门盲区里 #5186 → finding(devx): #5186 读接缝规则的「编造空答案」词表看不见「原样返回入参」这一形状 —— #6116 是实证样本 #6451 → this). If your measurement says the finding(devx): #5186 读接缝规则的「编造空答案」词表看不见「原样返回入参」这一形状 —— #6116 是实证样本 #6451 criterion already covers some of it, say so.
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-changesetis very likely correct here. Confirm against what you actually touch.
Generated by Claude Code
- The clone in the PM container is SHALLOW (50 commits;
os-project-manager commented
on Aug 15, 2026 CollaboratorMore actions{ "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
os-project-manager commented
on Aug 15, 2026 CollaboratorMore actions✅ 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:- They moved.
origin/mainadvanced15283cf→8664a2cduring this round — refactor(rest): extract the ADR-0112 error/fault-classification prologue out of rest-server.ts #8887 added ahandleRouteErrorentry toFAILURE_PROPAGATION_CALLEES(+32 lines), shifting those anchors to:895/:925. My claim's "the gate script is unmodified since filing ✅" is stale. - 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. GreppingisReturnStatementand finding hits at:863/:893is 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 lineFixes #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 ✓ diffMetaItemuntouched ✓ — and structurally so: no criterion was added, so nothing could go red on it gates Check Changesetsuccess ·Part-of PR must not also close its cardsuccess ·No other open PR may claim the same issuesuccess ·ADR maintainer approvalsuccess · Test Core / Dogfood / Console Pin Freshness / Doc Links / PR Size success. ⏳ESLint+TypeScript Type Checkstillin_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 asno 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 —
walkBenignPathsmodels 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 barereturn;. "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 exemptingcascadeDeleteRelations' skippedrestrictguard 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
findingso they enter the finding round properly rather than relying on triage's bare-card sweep. ⛔ I have not graded them, routed them, or setdomain:*.⚠️ #8895 deserves a grader's attention ahead of its siblings:cascadeDeleteRelationsfails open — a failed dependents probe skips therestrictguard, 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 isdomain:engine-coreterritory, not this lane's.Next: confirming
ESLint+TypeScript Type Checkconclusions, then ready → auto-merge → MERGED.⚠️ Note for whoever reads a red here: in this repo theESLintjob also carriescheck-changeset-no-major --self-test, so its name is not its diagnosis.
Generated by Claude Code
- They moved.
- added a commit that references this issue
on Aug 24, 2026 - added a commit that references this issue
on Aug 25, 2026 - added a commit that references this issue
on Sep 1, 2026
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.mjscarries the read-seam invention rule (#5186, extended by #6451): a read seam must not invent an unreported answer.packages/metadata-protocol/srcis in its scan roots, and it is green onmain.The rule classifies a
catchby what it returns. Verified onorigin/main@6b6b606— the analysis keys onts.isReturnStatementat:863and:893.So a
catchof this shape is invisible to it:There is no return statement to classify. The accumulator declared above the
trystays 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
diffMetaIteminpackages/metadata-protocol/src/protocol.ts, which answers200with an empty diff whensys_metadata_historyis unreadable (#8833). Measured there with a stub engine whosefindthrows; 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:
check:durability-log-level结构性看不见「读接缝把故障答成空值」这一类 —— #4825 / #5108 全家都在闸门盲区里 #5186 — the rule was created becausecheck:durability-log-levelstructurally could not see "a read seam answers a fault as an empty value";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
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.catchblocks 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.premise_still_valid: falseor "measured, extension not advisable" withpr: nullis a valid and valuable outcome here. This card is a measurement, not a promised diff.Explicitly out of scope
⛔ Do not fix
diffMetaItemin 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 ondiffMetaItem, that is the correct outcome and should be reported, not resolved by editing the seam.Related
needs-user-decision,domain:metadata. Note PR perf(metadata-protocol): drop diffMetaItem's discarded historyMetaItem round trip #8841 (for [finding] diffMetaItem awaits a full historyMetaItem read and discards it — a dead round trip on every live diff request #8798) makes that seam silent for every type, where it previously threw for some.check:durability-log-level结构性看不见「读接缝把故障答成空值」这一类 —— #4825 / #5108 全家都在闸门盲区里 #5186 — the rule's origin card.