Repository navigation
Description
Observation class — recording only, no verdict asserted, no pm:queue. Measured during the #9165 option-B site triage (PR-less measurement pass, triage table); filed by the devx PM seat because the dev that found it could not complete the duplicate search (see Provenance below).
The defect
isReadCall() (scripts/check-durability-degradation-log-level.mjs:1880) decides whether a call is a storage read by matching the callee name against DRIVER_READ_CALLEES, with no receiver check, and then recurses into same-file wrappers to MAX_READ_WRAPPER_DEPTH = 2:
function isReadCall(node, functionBodies, seen = new Set(), depth = 0) {
const name = calleeName(node);
if (!name) return false;
if (DRIVER_READ_CALLEES.has(name)) return true;
if (depth >= MAX_READ_WRAPPER_DEPTH || seen.has(name)) return false;
…find is in the read vocabulary. It is also Array.prototype.find. So a pure in-memory helper matches, and the wrapper recursion then attributes the match to that helper's caller.
Live instance, verified at source on origin/main:
mergePackageAwareOverlay(packages/metadata-protocol/src/protocol.ts:1256) — a pure helper — containslist.find((c) => c.pkg === packageId)at:1290, on a plain local array.- That pulls its caller's
try/catch(getMetaItems,protocol.ts:6254-6329) into the read-seam census. - The
tryit guards contains exactly oneawait—metadataService.list(request.type)— and **nofind/findOne/countof its own.
Measured: 1 of today's 66 seams.
Why it matters, stated honestly
Nothing is broken and no verdict is currently wrong: the site passes. The cost is to every measurement quoted against the census. The read-seam population is the denominator for #5186, #6451, #9165's landed criterion, #8845's chain and #8901's scope warning — and it currently contains one member that is not a read seam at all. Any future criterion over that population inherits the imprecision, and the cheap disposition (baseline the site) would book a recognizer imprecision as if it were reviewed code, which the baseline's own header explicitly forbids: "an entry that says a human read it, not a rule that guesses."
⛔ Two constraints for whoever picks this up
- Fixing it edits
scripts/check-durability-degradation-log-level.mjs, which engages two standing couplings: The read-seam invention rule has no declared failure-propagation vocabulary, so "the catch reported the failure" is uncheckable — the blocker measured under #8845 #8901's trigger-file clause (any PR touching this file must name that card) and check-durability-degradation-log-level:collectLoggedLevelsonly recognises a logger named logger/log/console, so a catch that reports through an injected logger reads as silent to BOTH rules #8897's hold record ("must be decided in the same change whenever this file is edited"). Neither is optional. - It moves the 66. Every number on 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, The read-seam invention rule has no declared failure-propagation vocabulary, so "the catch reported the failure" is uncheckable — the blocker measured under #8845 #8901 and check-durability-degradation-log-level: the read-seam invention rule keys onreturn, so acatchthat degrades by FALLING THROUGH into an empty accumulator is structurally invisible #8845 is quoted against a 66-seam census. A fix must restate the affected figures rather than silently invalidating them, and ⛔ must not ride along with any other change.
Possible shapes, for triage rather than as a recommendation
- Require a receiver that is not a plain array or local binding before
findcounts. - Exclude a wrapper hop whose match is a callback-taking
find(anArray.prototype.findcall always takes a function argument; a driverfinddoes not). - Leave it and record the imprecision — nothing depends on it while option B is parked.
⛔ No recommendation offered between them. (1) and (2) differ in what else they would exclude, and neither was measured against the full census here.
Provenance, and why this was filed by the PM rather than the finder
The finding is the dev's; the filing is mine. They declined to file because the required duplicate search could not be trusted: search_issues returned total_count 0 on a positive control that must match #9165 itself, and list_issues answered API rate limit already exceeded. Refusing to file off a zero from an instrument that just failed its own control is correct, and is why this is a filing rather than a duplicate.
I re-ran both. search_issues still returns 0 on the same control — consistent with open finding #11835 (both documented list channels down at once) — but list_issues recovered and returned 99 open finding issues, a non-empty control. Scanning those 99: nothing addresses isReadCall, the read-seam population, or DRIVER_READ_CALLEES. The nearest neighbours are different gates (#11487 cross-package descent counting, #11681 check:slot-lookup typed-not-complete). So the dedupe rests on the instrument that worked, not the one that did not.
Activity
claude commentedon Aug 25, 2026
Triage routing:
domain:devx— lands inscripts/check-durability-log-level.mjs(gate read-seam). Awaits first-touch grading.Generated by Claude Code
claude commentedon Aug 25, 2026
Concentrated triage batch (grading the earlier routing):
finding→pm:queue, Task, S (domain:devxstands) —isReadCall()matches callee NAME with no receiver check, soArray.prototype.find(and any other collection method sharing a name withDRIVER_READ_CALLEES) pulls unrelated try/catches into the read-seam population. Fix: add a receiver/import-provenance check (or exclude known collection-method receivers), re-baseline honestly, self-test both directions.Generated by Claude Code
yinlianghui commentedon Aug 25, 2026
Claim: PM seat
domain:devx @ objectstack, sessionsession_01UjM2ia8Av1v5NqfqQEQmC6, round R4.claude/issue-11921-read-seam-receiver-check../objectstack-11921(dedicated)domain:devx— landing repo objectstackscripts/check-durability-degradation-log-level.mjs+ its baseline, plus a declared increment (below)I checked both before claiming, and they do not hold equally. Recording the difference so the dev inherits a verified constraint and not an inherited assumption.
Coupling 1 — #8897: real, verified, and it fires on this dispatch
#8897's body opens with a machine-readable restart line, quoted verbatim from the card:
⇒ Touching this file is the restart condition. So this PR must decide #8897's options 1/2/3 (leave it + header note · follow a same-file injected-logger parameter · declare additional receiver names) as a declared increment, per the on-hold rider discipline. #8897 records a weak preference for (1)+(2)'s reasoning; that is an input, not a ruling.
⭐ This is what makes the card not-S: it is a recognizer fix plus an adjudication of a parked design question, in one change, by construction.
Coupling 2 — #8901: the card's characterisation does NOT match #8901's own restart line
This card's body says "#8901's trigger-file clause (any PR touching this file must name that card)". #8901's actual
Restart-whenis:That is a conjunction of three closures plus a census finding. "A PR touches the file" is not in it. I did not find a trigger-file clause in #8901's body; it may exist in one of its 10 comments, which I did not exhaustively read. ⇒ Treated as unverified, not as binding. ⛔ The dev must not inherit it as a constraint on my say-so — if it turns up while working, honour it and say so; if it does not, #8901 needs nothing from this PR beyond not re-opening the #8845 decision (which #8901 states as its own ⛔).
The other standing constraint, which IS load-bearing
It moves the 66. Every figure on #9165, #8901 and #8845 is quoted against a 66-seam read census, and this fix removes at least one non-member. The card is explicit: a fix must restate the affected figures rather than silently invalidating them, and ⛔ must not ride along with any other change.
⛔ And the cheap disposition is forbidden by the baseline's own header: baselining the site would "book a recognizer imprecision as if it were reviewed code", against a ledger that says an entry means "a human read it, not a rule that guesses."
Serial constraints. No open PR holds
scripts/check-durability-degradation-log-level.mjs— checked against the open-PR list. Fold-or-serial: nothing to fold; #8897 is a rider on this PR, not a separate queued card.pm:on-holdand its restart has now fired. Its label needs updating when this lands — noted so it is not left as a card whose stated restart condition happened while it still reads as parked.Generated by Claude Code
fix(gate): read-seam recognizer matched a callee NAME with no shape c…
test(gate): de-vacuum the #11921 wrapper fixture — it stayed green un…
yinlianghui commentedon Aug 25, 2026
{ "issue": 11921, "status": "done", "branch": "claude/issue-11921-read-seam-receiver-check", "pr": "https://github.com/objectstack-ai/objectstack/pull/12137", "premise_still_valid": true, "summary": "isReadCall() now consults contradictsDriverReadShape(): a call whose FIRST argument is a function literal is not an IDataDriver read, because every read on that contract takes the object NAME first while Array.prototype.find's first argument is always a predicate. Chosen on measurement across the whole census, not on taste — the card's receiver-based shape (1) would have dropped 14 of 66 seams, 12 of them REAL driver reads (history-cleanup, build-probes, dangling-reference-audit, checkGovernance), because receiver provenance is not syntactically available; shape (3) fixes nothing. The card measured 1 spurious seam of 66; the true figure is 2, and the second (resolveDeferredUpdates, whose try guards a WRITE) was unknown when the card was filed. Census restated rather than invalidated: 66 -> 64, re-measured with the corrected recognizer over the #8845 anchor tree 8664a2c as well as today's, with the old recognizer first reproducing 66 at that anchor as calibration. #8897 decided in the same change (option 1) per its Restart-when clause; #8901 named per its trigger-file clause, which DOES exist — see the Zone 2 note below. No baseline entry added.", "tests": "Gate union re-run at the FINAL commit 9154242e8, exit codes captured before any pipe, all 0: check:agent-test-spelling, check:cross-package-test-inputs, check:durability-log-level, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs (the 8 families derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` in the worktree, re-derived on the final HEAD and unchanged), plus check:ratchet-remedy-authority, scripts/pm/bare-root-worklist.mjs --self-test and check-nul-bytes.mjs. The gate's own verdict line at 9154242e8: '✓ read-seam invention (#5186 + #6451 + #9165, 3 package roots, vocabulary find/findOne/count): 64 read seam(s), none invents an unreported answer (8 answer on a type-discriminated benign branch) (1 pass an input through, reported) (1 answer by writing a pre-`try` variable, reported or discriminated) (1 baselined)' — every parenthetical byte-identical to the pre-fix line, only 66 -> 64 moves. Self-test: '✓ self-test (read-seam invention rule): 45 case(s) passed'. Repo-wide `eslint . --no-inline-config --format json` run under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0, held 51s): 5099 files, 0 errors, 0 warnings — NOT narrowed, so no narrowing declaration is owed. ABLATION, two legs, no build/dist involved (the gate is a .mjs run directly, so there is nothing to rebuild); every leg confirmed on disk by grepping BOTH the injected and the deleted text before and after, and every leg restored by a `trap '<restore>' EXIT INT TERM`. Leg 1, under-fire (revert the shape test): disk counts new=1->0 old=0->1; result RED as predicted — 2 self-test cases failed and the census returned to 66. Leg 2, over-fire (force contradictsDriverReadShape to `return true`): disk counts marker=0->1 original=1->0; result RED — the 3 `flags` cases failed (31 total), which is what makes them non-vacuous, since leg 1 alone leaves them green. ⚠️ Leg 1 also caught a fixture of MINE that asserted nothing: the wrapper-hop case spelled its array find inside a records.map(...) callback, and isReadCall's wrapper recursion walks the SAME TICK only, so the call was unreachable in both directions and the case passed pre-fix and post-fix alike. Respelled as a same-tick `for` loop (the live instance's shape) in its own commit 9154242e8, and leg 1 re-run afterwards showed it going red. CENSUS METHOD: an instrumented COPY of the gate (additive tracing only, deleted before commit) reproduced the gate's own 66 exactly and traced all 90 matched reads; the anchor comparison ran both recognizers over a second worktree at 8664a2c via a ROOT override, the old one answering 66 — the number the script header itself tells the reader to check.", "open_questions": [], "out_of_scope_findings": [ "filed as #12138: the same recognizer's OPPOSITE error — isReadCall's wrapper recursion uses walkSameTickInclusive, so a driver read reached through a synchronously-invoked callback inside a same-file wrapper is invisible to the census; a walkAll probe moves 64 -> 72, stated as an UPPER bound because walkAll also descends into genuinely deferred callbacks. Under-count direction, no verdict wrong today, unfixed here per #11921's ⛔ on riding along." ] }Zone 2 items that came out against the dispatching seat's reading
Zone 2.1 — #8901's trigger-file clause EXISTS, and is binding. Not in its
Restart-when:line (the seat checked that correctly) but in the maintainer hold record of 2026-08-16, comment 5306092540, verbatim: "Trigger file (opportunistic-restart clause): any PR touchingscripts/check-durability-degradation-log-level.mjs— dispatches whose file surface intersects it must name this card; note #8897 (held) must be decided in the same change whenever this file is edited." Reaffirmed 2026-08-17. Honoured: named in the PR body and recorded on #8901. ⇒ This card's body was right on that point; only its attribution (to #8901's own restart line) was off. No correction comment owed.Zone 2.2 — the live-instance count is 2, not 1. Five spurious matched reads across four call sites in two files; two seams leave the census. The restatement got bigger, as the dispatch warned it might.
Zone 2.3 — the warning about shape (2) came out the other way. Shape (2) is sound (no genuine driver read takes a function literal argument); shape (1) is the one that only measurement exposes, at 14 seams / 12 real reads. And shape (2) is not "tuned until only the known instance changes" — it moved two seams, one of them unknown to the card. One correction to the card's phrasing: shape (2) was written as "exclude a wrapper hop", but two of the four spurious sites are direct depth-0 matches in the
tryblock, so a wrapper-only test would have been half a fix.Zone 2.5 — neither landing obligation fires, verified not trusted.
bare-root-worklist.mjs: 0 untriaged rows (and--self-testgreen).check-ratchet-remedy-authority --liststill classifies this file as[anchor: shrink], sweep green with its control corpus set-equality audited both ways — theOR :offer text was not touched.Zone 2.6 —⚠️ Declared deviation: the additive
skip-changesetconfirmed and applied. One file under rootscripts/; nothing published changes.POST /issues/:n/labelschannel the protocol prescribes is not reachable from this seat —curlwith$GITHUB_TOKENreturns "GitHub access is not enabled for this session" andghis absent (the condition already filed as #12123). Fell back to read → union → whole-set write via MCPissue_write, then read back: labels are["size/m", "skip-changeset"]. The same unavailability forced the finding's dedupe through one bounded MCPlist_issuescall (24 openfindingissues, non-empty control) instead of the prescribed REST list endpoint — declared on #12138 too.pm:on-holdand its stated restart has now fired and been answered; its label wants updating when #12137 lands. ⛔ No labels touched on #8897 or #8901.Generated by Claude Code
Generated by Claude Code
yinlianghui commentedon Aug 25, 2026
PM review — ACCEPT (landing held until CI converges)
Reviewed PR #12137 against the dispatch. This is the most measurement-heavy PR of the shift and it earns the ACCEPT on the measurements, not on the diff being small.
What the review checked, and what it found
1. The card undercounted the defect, and the dev found the miss — not me.
The card measured 1 of 66 spurious read-seams. The truth on the anchor tree is 2 of 66. The second one,
resolveDeferredUpdates(packages/metadata-protocol/src/seed-loader.ts), is the worse of the pair: itstryguards a write, and it entered the read-seam population only becauseallResults.find(...)updates an in-memory stats row. A gate that mislabels a write-guarding seam as a read seam is a bigger lie than one that mislabels a read.2. All three candidate shapes were measured across the whole census, not argued from examples.
90 matched reads / 66 seams. Card shape (1) is dangerous: it removes 14 seams, of which 12 are real driver reads. The dev's sentence is the reason this PR is accepted as-is:
That is the correct disqualification of a syntactic fix for a semantic distinction, and it is exactly the failure mode the lane rule 「门禁的自述必须为真 —— 理由为假的门禁比没有门禁更坏」 exists to prevent.
3. The chosen shape is anchored to a declared contract, not to a heuristic.
Every
IDataDriverread takes the object name first;Array.prototype.find's first argument is always a function. So the predicate can only decline to exclude — it cannot remove a real driver read. That is a soundness argument, not a sample.4. Census restated honestly. 66 → 64 on the same anchor tree (
8664a2c), with the old recognizer reproducing 66 there as calibration. Restating a number without pinning the tree and re-running the old instrument is how ratchets drift; this one did both.5. The ablation caught a dead fixture of its own. A test asserting nothing — array
findinside a.map(...)callback, never reached becauseisReadCall's recursion walks the same tick only. Removing a fixture that asserts nothing is a strict improvement in what the suite claims about itself.The open item in my dispatch is now resolved — and I was wrong about where it lives, not whether it exists
My dispatch said, verbatim:
The dev checked. I then re-read #8901's comments myself rather than take the answer on trust. Both readings agree:
5315739733(2026-08-17) names the trigger files: "trigger files unchanged (scripts/check-durability-degradation-log-level.mjs+ the check-durability-degradation-log-level:collectLoggedLevelsonly recognises a logger named logger/log/console, so a catch that reports through an injected logger reads as silent to BOTH rules #8897 coupling clause)" — reaffirming the 2026-08-16 hold record.Restart-when:line. That line readsclosed #8895 AND closed #8896 AND closed #8906, then a census re-run…and says nothing about the coupling.Restart-when:line. The clause survives; the citation does not. Recorded here rather than as a separate correction comment, since this card closes with the PR.Method note for the next seat: the caution was right, the attribution I flagged was the part that failed. "I have not verified this, do not inherit it" is cheap to write and it is the only reason the wrong pointer got caught instead of propagating into #8897's decision.
#8897
Decided option 1. The reasoning in the PR is not the filer's, and the difference matters: options 2 and 3 widen an exemption. Widening an exemption is the unsafe direction for a gate whose whole value is that its self-description is true. Option 1 is accepted on that ground.
Follow-up owed on landing: #8897 carries
pm:on-hold; itsRestart-when:has now fired and been answered. I will update its label in the same pass that lands this PR — a hold whose restart has already been answered is a lie in the state machine.Landing condition
Held. At review time #12137 shows 0 failures with 5 jobs still
in_progress(Lint & Repo Gates,Type Check · workspace,Type Check · consumer gates,Type Check · debt ledger,Test Core 1–2 of 6). Per standing rule:in_progressis not a pass, and the aggregate status read is not a substitute for reading the named gate jobs. Flip-ready happens only when every named gate job reportscompleted+success.Generated by Claude Code
yinlianghui commentedon Aug 25, 2026
os-dev-report
{ "issue": 11921, "status": "done", "branch": "claude/issue-11921-read-seam-receiver-check", "pr": "https://github.com/objectstack-ai/objectstack/pull/12137", "premise_still_valid": true, "summary": "isReadCall() now consults contradictsDriverReadShape(): a call whose FIRST argument is a function literal is not an IDataDriver read, because every read on that contract takes the object NAME first while Array.prototype.find's first argument is always a predicate. Chosen on measurement across the whole census, not on taste — the card's receiver-based shape (1) would have dropped 14 of 66 seams, 12 of them REAL driver reads (history-cleanup, build-probes, dangling-reference-audit, checkGovernance), because receiver provenance is not syntactically available; shape (3) fixes nothing. The card measured 1 spurious seam of 66; the true figure is 2, and the second (resolveDeferredUpdates, whose try guards a WRITE) was unknown when the card was filed. Census restated rather than invalidated: 66 -> 64, re-measured with the corrected recognizer over the #8845 anchor tree 8664a2c as well as today's, with the old recognizer first reproducing 66 at that anchor as calibration. #8897 decided in the same change (option 1) per its Restart-when clause; #8901 named per its trigger-file clause, which DOES exist — it is in the maintainer hold record comment 5306092540 of 2026-08-16, not in the card's Restart-when line, so the dispatching seat's UNVERIFIED reading resolves to EXISTS-AND-BINDING. No baseline entry added.", "tests": "Gate union re-run at the FINAL commit 9154242e8, exit codes captured before any pipe, all 0: check:agent-test-spelling, check:cross-package-test-inputs, check:durability-log-level, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs (the 8 families derived by 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack' in the worktree, re-derived on the final HEAD and unchanged), plus check:ratchet-remedy-authority, scripts/pm/bare-root-worklist.mjs --self-test and check-nul-bytes.mjs. The gate's own verdict line at 9154242e8: 'read-seam invention (#5186 + #6451 + #9165, 3 package roots, vocabulary find/findOne/count): 64 read seam(s), none invents an unreported answer (8 answer on a type-discriminated benign branch) (1 pass an input through, reported) (1 answer by writing a pre-try variable, reported or discriminated) (1 baselined)' — every parenthetical byte-identical to the pre-fix line, only 66 -> 64 moves. Self-test: 'self-test (read-seam invention rule): 45 case(s) passed'. Repo-wide 'eslint . --no-inline-config --format json' under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0, held 51s): 5099 files, 0 errors, 0 warnings — NOT narrowed, so no narrowing declaration is owed. ABLATION, two legs, no build/dist involved (the gate is a .mjs run directly, nothing to rebuild); every leg confirmed on disk by grepping BOTH the injected and the deleted text before and after, and every leg restored by a trap on EXIT INT TERM. Leg 1, under-fire (revert the shape test): disk counts new=1->0 old=0->1; RED as predicted — 2 self-test cases failed, census back to 66. Leg 2, over-fire (force the predicate to return true): disk counts marker=0->1 original=1->0; RED — the 3 flags cases failed (31 total), which is what makes them non-vacuous, since leg 1 alone leaves them green. Leg 1 also caught a fixture of MINE that asserted nothing: the wrapper-hop case spelled its array find inside a records.map(...) callback, and isReadCall's wrapper recursion walks the SAME TICK only, so the call was unreachable in both directions and the case passed pre-fix and post-fix alike. Respelled as a same-tick for loop (the live instance's shape) in its own commit 9154242e8, and leg 1 re-run afterwards showed it going red. CENSUS METHOD: an instrumented COPY of the gate (additive tracing only, deleted before commit) reproduced the gate's own 66 exactly and traced all 90 matched reads; the anchor comparison ran both recognizers over a second worktree at 8664a2c via a ROOT override, the old one answering 66 — the number the script header itself tells the reader to check.", "open_questions": [], "out_of_scope_findings": [ "filed as #12138: the same recognizer's OPPOSITE error — isReadCall's wrapper recursion uses walkSameTickInclusive, so a driver read reached through a synchronously-invoked callback inside a same-file wrapper is invisible to the census; a walkAll probe moves 64 -> 72, stated as an UPPER bound because walkAll also descends into genuinely deferred callbacks. Under-count direction, no verdict wrong today, unfixed here per #11921's constraint against riding along." ] }Generated by Claude Code
Generated by Claude Code
docs(gate): record the read-seam wrapper recursion's callback refusal…
fix(devx): the read-seam wrapper hop gets the shape check the vocabul…
fix(gate): read-seam recognizer matched a callee NAME with no shape c…
docs(gate): record the read-seam wrapper recursion's callback refusal…
fix(devx): the read-seam wrapper hop gets the shape check the vocabul…