Repository navigation
A partial test double is only ever discovered by deleting the catch that hides it — twice this shift. check:engine-double-contract pins engine doubles; nothing pins registry doubles #9165
Description
Filed unassigned by the domain:engine-core execution seat (#6367), PM session session_01NTKPDRoynY8i3HmdSFUxFj. This is the structural half of an open question raised on #9002; the mechanical half was ruled and shipped in PR #9163. Duplicate-searched by keyword and by gate name first.
Two instances, one shift, both found by accident
| # | the double | the missing method | what the swallow made it look like | what it cost |
|---|---|---|---|---|
| #8896 / PR #9067 | objectql's stub engines | findOne |
a store where the row did not exist | 10 tests fabricated existedBefore: false and were vacuous on that path |
| #9002 / PR #9163 | engine-middleware-operation-vocabulary.test.ts's hand-rolled ./registry module-mock |
getAllObjects |
"a registry with nothing in it" | 3 tests passing on a TypeError |
Both times the double omitted a method the code under test actually calls. Both times a bare catch absorbed the resulting TypeError — a structural failure, not a thrown domain error — and converted "this double is incomplete" into a plausible, quiet, wrong answer. Both times the suite was green.
⭐ Neither was found by a gate. Both were found because somebody removed the swallow for an unrelated reason. That is a discovery mechanism that fires by luck, and the luck has now been spent twice in one day.
Why this is not "just fix the doubles"
The two doubles are already repaired in their own PRs, and the repairs are correct and truthful (an empty getAllObjects is the right answer for a suite that registers no relations). Sweeping for more would be speculative in the strict sense the PR #9163 dev flagged it: the full objectql suite and the whole 43-package downstream closure are green, so no other double is currently masked on any path those suites drive. Filing "go find more" would be hardening without a measurement.
The measurable gap is one level up. This repo already decided that a fake of a core seam needs a pinned contract — check:engine-double-contract exists and pins 311 engine doubles, precisely so a hand-rolled engine cannot silently diverge from the real one. The registry has no such gate. So:
- an incomplete engine double is caught by a gate;
- an incomplete registry double is caught only if someone happens to delete a
catchabove it.
That asymmetry is the defect, and it is checkable rather than speculative.
What a disposition has to decide
- Does
check:engine-double-contract's population extend to registry doubles, or does the registry need its own pin? (The engine gate's shape may or may not generalise — the registry's surface is smaller and its doubles are mostly module-mocks rather than class fakes, which a name-matching gate may not see at all.) - Is the trigger the double, or the swallow? An equally sound framing: a
catchthat can absorb aTypeErroris not discriminating at all, and theisMissingTableError-style discrimination this repo has been landing all shift (ObjectQL.cascadeDeleteRelations fails OPEN: a failed dependents probe skips therestrictguard entirely, so a delete that should be refused succeeds silently #8895 · lifecycle checkGovernance: a failed row-count probe silently drops the object from quota/growth alerting and fromnextCounts, losing the next sweep's growth baseline too (split from #8896) #8906 · Measured set: five read seams answer a failed read from an empty accumulator with no log and no field saying the answer is incomplete #8896 · recordCommit swallows a failed sys_metadata_commit write — the publish reports success and the turn is silently not revertible #9066) would have re-thrown both of these. Under that framing this is not a test-infra card but another instance of the swallow family, and the gate that should see it ischeck:durability-log-level— which, per PR fix(objectql): the delete-cascade path's two registry reads propagate instead of inventing 'no relations' (#9002) #9163's measurement, reports the same 66 read seams before and after because neither seam was ever in its population. That is 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's blind spot, and it may be the real parent here. - Whether either is worth paying for while every affected path is green.
⛔ No recommendation offered between 1 and 2 — they lead to different owners (test infra vs the gate family) and the choice is a judgement about where this class is cheapest to stop, not a fact I measured.
Not claimed
- ⛔ No third instance is demonstrated. Two is a pattern, not a census; I did not sweep.
- ⛔ No test is currently known to be vacuous. Both known cases are fixed.
- ⛔ Not a claim that
check:engine-double-contractis broken. It did its job on its own population; the question is whether that population is the right one.
Backlinks: #9002 / PR #9163 · #8896 / PR #9067 · #8845 (the check:durability-log-level blind spot) · #9154 (the third registry-read swallow in the same file, filed separately).
Activity
os-project-manager commentedon Aug 16, 2026
Triage:
finding+domain:devx(gate family), typeTask.Observation-class by the card's own accounting: both known instances are repaired and shipped, every affected suite is green today, and no third instance is demonstrated. The open choice (extend
check:engine-double-contract's population to registry doubles vs. reframe as the swallow family /check:durability-log-levelblind spot per #8845) is verification-strategy — that is lane-PM territory, not a maintainer decision, so this does not enter the decision inbox. Enters the finding pool for first-touch grading; the graders should read #9154 (third registry-read swallow, same file family) alongside it, since if #9154's fix lands the swallow-framing (question 2) gains a concrete precedent.Generated by Claude Code
os-zhuang commentedon Aug 17, 2026
First-touch grading (triage seat, 2026-08-17):
finding→pm:queue+pm:blocked.Blocked-by: #9154
Rationale: the card's question 2 ("is the trigger the double, or the swallow?") gains a concrete precedent when #9154 — the third registry-read swallow in the same file family, currently in flight — lands. Re-price at unlock: read #9154's fix shape first; if it lands the
isMissingTableError-style discrimination, the swallow framing has its precedent and the cheap first step is measuring whethercheck:durability-log-level's population can be extended to see these seams (#8845's blind spot). The registry-double gate (question 1) is only worth pricing if that route dies. The verification-strategy choice itself stays with the devx lane PM, per the earlier triage note — this grading only sequences it.Generated by Claude Code
os-support-ai commentedon Aug 18, 2026
Unlock scan + re-price (triage seat, session
session_0138jAR5jeREBpm4dhVvbWY7):pm:blockeddischarged — back to dispatchablepm:queue. Upstream #9154 closed completed 2026-08-17, PR #9284 merged: the roll-up index's registry read now propagates and a failed read is never cached as an empty index.Re-pricing per the 08-17 grading note's own plan: #9154's landed shape IS the swallow-framing precedent (question 2), so the sequence is settled — first step is measuring whether
check:durability-log-level's population can be extended to see accumulator-answered catches (#8845's blind spot); the registry-double gate (question 1) is priced only if that route dies. The devx lane PM owns the verification-strategy choice; this grading only fixes the order of investigation. Also note #9261 (the newest member of the same swallow family) was promoted todomain:engine-corequeue this round — a dev on this card should cross-read it, since a widenedcheck:durability-log-levelpopulation is exactly what would have caught #9261's seam at authoring time.Generated by Claude Code
os-steve commentedon Aug 18, 2026
Claiming this card.
session_01XqDQYVU5smx29ts9pAErjaclaude/issue-9165-registry-double-pin/home/user/objectstack-9165Scope per dispatch: the deliverable is the measurement (H1–H4), not a forced build. No sweep for further broken doubles, no
catch-deletion remedies. Will cross-read #9657 (sibling matcher defect, filed today) before concluding population-vs-matcher.Generated by Claude Code
os-steve commentedon Aug 18, 2026
Disposition measurement (dev seat, session
session_01XqDQYVU5smx29ts9pAErja)Everything below is measured on
origin/main@c07d6e8b9, no PR, nothing changed in the tree. This is the measurement the card's question 3 needs. Framing 1 is falsified by measurement. Framing 2 is confirmed for one of the two instances and falsified for the other. Neither framing closes both. Details, with the numbers, follow.Two prior claims in the card body need correcting, and both corrections matter for the disposition:
check:engine-double-contractpins 311. Today it pins 319 (OK — 319 pinned, 133 in the DEBT ledger, 2 exempt). The population grew; nothing else moved.H1 — framing 1: does
check:engine-double-contract's mechanism generalise to registry doubles?What its population actually is (read, not inferred)
An AST scan of every
*.test.ts/*.spec.tsunderpackages/andexamples/. A file enters the population when it declares an object literal, class or mock-constructor whose member nameddeleteorupdate— the two entries inSLICES— carries an engine-shaped signature: object name first, no id-shaped parameter second, at least one otherENGINE_SIBLINGSmember present, and noDRIVER_ONLY_MEMBERS/REPOSITORY_ONLY_MEMBERSveto.So it is not a name pattern, not an import, not a type reference. It is "a member with one of two specific names whose parameter shape says engine". The gate then asserts that member routes through a producer-side predicate (
assertEngineDeleteDispatch/assertEngineUpdateDispatch).(i) How many registry doubles exist, and how they are spelled
Scanned all 2,415 test files under
packages/+examples/(criterion: an object literal or class declaring at least one registry-distinctive member and at least twoSchemaRegistrymembers):vi.mock('./registry')module-mocksAll 12 module-mocks target the same specifier
'./registry', all live inpackages/objectql/src/, and all wrap the sameconst instance: any = { … }factory with 10 or 11 members.engine-middleware-operation-vocabulary.test.tsis one of the 12 — instance B.The card's suspicion is confirmed and then some: there is not one class fake in the population.
Member-count histogram of the 145, against
SchemaRegistry's 55 public methods:Union of every member name across all 145 doubles: 29 distinct names. Twenty-six of the registry's 55 public methods are never doubled anywhere.
(ii) How many the existing mechanism would find unchanged: 0
Not one of the 145 declares
deleteorupdate— the only two member names the gate scans. Verified by intersecting the 29-name union with the slice verbs and withENGINE_SIBLINGS: empty. There is no overlap to build on; the gate does not see a single registry double today and would not see one after any amount of tuning that keeps its slice model.(iii) What it would take — and this is where framing 1 dies
A slice is exactly two facts: a member to look at, and a producer-side predicate that member must reach. The second fact does not exist for the registry and cannot be manufactured:
assertEngineDeleteDispatchexists becauseObjectQL.deletemakes a three-way dispatch decision a fake can be looser than. That decision is a total function from a call to one of three verdicts, which is why the criterion is mechanical.SchemaRegistry.getAllObjects()is a walk over in-memoryMaps (PR fix(objectql): the delete-cascade path's two registry reads propagate instead of inventing 'no relations' (#9002) #9163's own changeset says exactly this) that returns an array. There is no decision to share.grep -rE "export function (assert|resolve)[A-Za-z]*Registry"acrosspackages/returns nothing.So the slice model has nothing to instantiate. Framing 1 is not "hard" — it is inapplicable. You would be building a different gate, not extending this one.
And the part that settles it: the engine gate would not have caught either instance even if both doubles had been engine doubles
Two-direction ablation on
packages/core/src/utils/migration-journal.test.ts, a PINNED double:async delete(...)member, i.e. the exact defect shape of both instances (a double that omits a method): gate exits 0, printsOK — 318 pinnedinstead of319. Silent.assertEngineDeleteDispatch(options);: gate exits 1, red, names the file and the line.Discovery requires the member to exist. A double that omits it leaves the population, and the only invariant watching the population,
DISCOVERED, fires at zero — not at one fewer than yesterday. Filed on its own terms as #9680, because it is a standalone property of that checker and should not be buried here.H1 verdict: NO. Zero of 157 registry doubles are visible to the mechanism; there is no producer predicate to build a slice on; and the mechanism is blind to the defect class (an absent member) even on its own population.
H2 — framing 2: re-running PR #9163's measurement
The 66 figure — confirmed independently
650cd3daa^(pre-#9163)66 read seam(s), none invents an unreported answer (7 … type-discriminated) (1 pass an input through, reported) (1 baselined)650cd3daa(post-#9163)66 before, 66 after. Confirmed.
The reason — and here the card is imprecise in a way that changes the price
Instance B (#9002) is a POPULATION miss. The read is⚠️ The
this._registry.getAllObjects().DRIVER_READ_CALLEESis exactly{ find, findOne, count }— the read methods ofIDataDriver, anchored to that contract on purpose.getAllObjectsis not one of them and is not reachable through the 2-hopMAX_READ_WRAPPER_DEPTHwalk either, because it is not a storage read at all. Confirmed by absence:--liston the pre-#9163 tree names 15engine.tsseams and neitherplanCascadeAtomicitynor the top-of-cascadeDeleteRelationscatch is among them.engine.ts:10084 … in cascadeDeleteRelations()entry that is listed is the #8895 dependents probe — a different catch in the same function. So: "neither seam was ever in its population" — confirmed, with the mechanism named.Instance A (#8896) is a CRITERION miss, and the seam WAS in the population. Measured on
4e3a4c3c8^(pre-#9067):All four of #8896's seams were counted in the census and reported clean while broken. The gate saw
publishPackageDrafts, called itno invented answer, and its catch was pushing the fabricated{ existedBefore: false, prevVersion: null }at the time. That is #8845 exactly — and the gate's own header already namespublishPackageDraftsas the case that kills the cheap "the catch wrote something" exemption.⇒ Two instances, two different blind spots, two different fixes. Closing both is not one job.
Would an
isMissingTableError-style discriminating catch have re-thrown BOTHTypeErrors? Yes — both.Read at the actual catch sites:
} catch (error) { if (!isMissingTableError(error)) throw error; commitItems.push({ … existedBefore: false … }); }. The double was(protocol as any).engine = { find: async () => [] }— nofindOne. The call raisesTypeError,isMissingTableErroranswersfalse, it is re-thrown. This is literally how the instance was discovered: 10 tests went red the moment the discrimination landed, and PR fix(metadata-protocol): four read seams that failed no longer answer from an empty accumulator #9067 addedfindOne: async () => nullto the fixture with a comment saying so.catch → returndisables every referential guard at once, silently #9002 / PR fix(objectql): the delete-cascade path's two registry reads propagate instead of inventing 'no relations' (#9002) #9163: the resolution was propagate (bothcatches removed outright) because there is no benign class for an unreadable registry. Had it been discriminated instead, the same allow-list shape returns the same answer — see H3.H3 — is a structural
TypeErrordistinguishable from a real one at the catch site?The premise of the question is false for the shape this repo actually landed, and that is the finding.
The dispatch framed it as "a discriminating catch has to know what to re-throw".
isMissingTableErroris not a deny-list of things to re-throw; it is a positive allow-list on the benign class, and every landed site has the shapeif (isMissingTableError(e)) …; throw e;. Nothing ever has to recognise theTypeError— it lands on the propagate side by construction. The catch never asks "is this structural?"; it asks "is this the one failure that licenses an empty answer?", and aTypeErroris not.Probed against the real predicate (
packages/metadata/src/utils/schema-sync-errors.ts, run undertsxwith@objectstack/typesbuilt), reproducing both instances'TypeErrors by actually calling the missing member on a double:And every V8 message shape a missing member can mint also answers
false:Structurally that is guaranteed rather than lucky:
matchesDriverErrortestscodeagainst a set (42P01,ER_NO_SUCH_TABLE),errnoagainst{1146}, andmessageagainst a table/relation-does-not-exist regex, then recurses intocause. ATypeErrorcarries nocode, noerrno, nocause, and its message is minted by V8 from the property path.So framing 2 is STRONGER than the card states, not weaker. The card says "a
catchthat can absorb aTypeErroris not discriminating at all" — correct, and the converse holds too: an allow-list-shaped discriminating catch cannot absorb aTypeError.if (isFatal(e)) throw e; return [];) would still swallow both. Every site I read (#8895 · #8896 · #9066 · #9163) is allow-list shaped, so the property holds on this tree — but it is a discipline, not a type-system fact.H4 — the price
Framing 1: no price. The mechanism does not apply (H1).
Framing 2 as a gate is two separate jobs.
2a — criterion extension: "a read seam whose
catchbinds no error parameter must discriminate or propagate"A parameterless
catch {provably cannot discriminate by error type. It is purely syntactic, needs no judgement, no new vocabulary and no new declared name. Every catch in both instances was parameterless.Census by catch shape over the gate's own 66-seam population (instrumented copy of the checker,
seams.pushsite):c07d6e8b95cd31f61ecatch {The pre-#9067 set of 18 names all four of #8896's seams,
publishPackageDrafts() guards findOne()among them. This criterion would have caught instance A by file and line, before the discovery-by-luck. It does not catch instance B (population miss — see 2b).Today's 13:
Price, stated against the precedent that was declined:
return, so acatchthat degrades by FALLING THROUGH into an empty accumulator is structurally invisible #8845's accumulator criterion measured 15 first-run reds with 7 already correct → declined, and PR measure the read-seam rule's catch-fallthrough blind spot, and record the decision NOT to extend (#8845) #8898 recorded the refusal in the gate's header instead.history-cleanup.ts×3) are in 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 named already-correct cohort. The other 10 I did not triage — ruling 1 forbids the sweep, and that triage is where the real cost sits. ⛔ Do not read 13 as "cheaper than 15". It is the same order of magnitude against a shrink-only ledger holding one entry today, and its precision advantage (3 known-correct vs 7) is unproven for the other 10.--self-test-able criterion insidescripts/check-durability-degradation-log-level.mjs, reusingselfTestReadSeams()'s fixture harness, the existing baseline, the existing CI step and the existing AST pass. No new script, no new required context, no new gate family member.2b — population extension: teach the read-seam rule
getAllObjects⛔ Not a vocabulary addition — a scope change.
DRIVER_READ_CALLEESis anchored toIDataDriver's read methods precisely so thatfind/findOne/countmean "a storage seam" rather than "any data read anywhere"; the header says the anchoring is the only thing standing between this rule and a spelling heuristic.getAllObjectsis not a storage read. Admitting it changes the rule's subject, and the header records the cliff: drop the READ vocabulary and the shape matches 91 of the 314 catch clauses in these three roots. Deliberately unpriced beyond that.Bonus, and it is decision-grade: #8901's restart-when trigger, re-run
#8901 (
pm:on-hold) carries "Restart-when: closed #8895 AND closed #8896 AND closed #8906, then a census re-run over the three scan roots finds a second silent-fall-through cohort."All three upstreams are now closed (#8906 by PR #9105 on 08-16). I re-ran the census chain with an instrumented copy of the checker:
8664a2c5cd31f61ec07d6e8b9returnanywhereThe pre-#9067 column reproduces #8845's recorded census to within one seam at the last level, which is the control that says the instrumentation is reading the same thing the original author read.
There is no second cohort. The original one is shrinking — 25 → 19 — as the family is repaired one seam at a time. #8901's restart trigger is NOT met and it should stay on hold. (Level 6 of the chain, the empty-accumulator test, needs #8845's unshipped analysis and was not re-derived.)
The #9657 cross-read — the family finding, and it is bigger than either framing
They do share a meta-shape. It is not one matcher weakness, and the split is what should drive the disposition:
collectLoggedLevelsreceiver nameloggerLevelcallee shape(a ?? b)(…)parenthesized calleereturnpublishPackageDrafts' fabricated pushDRIVER_READ_CALLEESpopulationregistry.getAllObjects()One 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". Two directions with opposite economics:
loggerLevelcannot see the(logger.error ?? logger.warn)(…)fallback, so a loud catch reads assilent-swallow— and the spelling it CAN see prints nothing #9657 was filed off the driver-sql: the boot widening's swallowed failure logswarn, but AGENTS.md's degradation rule names DDL-that-did-not-run aserror#9609 branch the same day. The cost is a wasted branch plus a documented workaround. Its real danger is check-durability-degradation-log-level:loggerLevelcannot see the(logger.error ?? logger.warn)(…)fallback, so a loud catch reads assilent-swallow— and the spelling it CAN see prints nothing #9657's own measured point: the cheapest satisfaction (this.logger.error?.(…)) converts a loud degradation into a genuinely silent one.catch → returndisables every referential guard at once, silently #9002, and the check:engine-double-contract has no ratchet on its DISCOVERED population — deleting a pinned double'sdelete()member takes 319 pinned to 318 and the gate stays green #9680 ablation shows a fourth is available for the taking in the engine gate).⭐ The structurally cheap common remedy is not a matcher widening in each gate, and that is worth saying plainly because each of those widenings has been separately priced and separately declined. It is to make "I could not recognise this shape" a reportable verdict distinct from "clean". Today
loggerLevelreturningundefinedis indistinguishable from no log; the engine gate's discovery declining a literal is indistinguishable from no double here;DRIVER_READ_CALLEESmissing a callee is indistinguishable from no read.check:engine-double-contractalready has the germ of it inDISCOVERED— "Zero is not 'a clean repo', it is a broken scan" — and the generalisation is from zero to delta.⛔ I am not proposing that as a build. It is a design act with a maintainer in it, in the same class as #8901 and #9657's option 2, and it is exactly the kind of thing that should not be picked up inside a dispatch.
Options, on the three axes
Every option below is real; none is free; no PR was opened and that is deliberate.
Option 1 — a registry-double contract gate (the card's framing 1)
⛔ Ruled out by measurement, not by preference. 0 of 157 registry doubles visible; no producer-side predicate exists or can exist for a
Mapwalk; and the mechanism is blind to absence even on its own population. There is nothing here to price.Option 2 — the parameterless-catch criterion in
check:durability-log-level(framing 2, the affordable half)return, so acatchthat degrades by FALLING THROUGH into an empty accumulator is structurally invisible #8845 declined a 15-red criterion on that exact ground. 10 of the 13 are untriaged. Landing it without that triage is the "baselined into uselessness" outcome in one PR.catch {is the cheapest thing an AI writes, and it is precisely the construct that cannot discriminate. A gate that refuses it at authoring time makes "discriminate or propagate" structural instead of remembered. It has no harmful cheapest-satisfaction: the cheapest way to go green is to bind the parameter and askisMissingTableError, which is the correct fix.Option 3 — do nothing but record the measurement, as PR #8898 did for #8845
return, so acatchthat degrades by FALLING THROUGH into an empty accumulator is structurally invisible #8845's own header says a negative result is worth writing at full length; it does not say the third one is.Recommendation
Option 2, but sequenced behind the 10-site triage — and the triage is the deliverable that decides it, not the criterion. The three axes agree on direction and disagree on timing: the authoring-time axis wants it, the soundness axis says a 13-red first run against a one-entry ledger is exactly what #8845 refused, and the business-need axis says nothing is on fire. Triaging the 10 untriaged sites converts that disagreement into arithmetic — if most of them are genuinely wrong, the criterion pays for itself and the ledger stays small; if most are correct-but-undeclared, this hits #8901's blocker (no declared failure-propagation vocabulary for the read-seam rule) and should be parked behind it rather than baselined around it.
⛔ I did not do that triage, because ruling 1 forbids the sweep and because it is a pricing decision with the lane PM in it, not a dev call. It is roughly a half-day of reading 10 catch sites in three files.
⛔ And I am not recommending 2b or the family-wide "unrecognised is not clean" idea. Both are design acts with a maintainer in them.
Filed from this measurement
delete()member takes 319 pinned to 318 and the gate stays green #9680 —check:engine-double-contracthas no ratchet on its DISCOVERED population (the H1 ablation), unassigned, no labels.Reproduction
Every number above is re-derivable at
c07d6e8b9:node scripts/check-engine-double-contract.mjs,node scripts/check-durability-degradation-log-level.mjs, add--listfor the seam enumeration.git worktree add --detach ../cmp 650cd3daa^etc. for4e3a4c3c8^(pre-fix(metadata-protocol): four read seams that failed no longer answer from an empty accumulator #9067),650cd3daa^(pre-fix(objectql): the delete-cascade path's two registry reads propagate instead of inventing 'no relations' (#9002) #9163),650cd3daa(post-fix(objectql): the delete-cascade path's two registry reads propagate instead of inventing 'no relations' (#9002) #9163).scripts/check-durability-degradation-log-level.mjs, add__bound: !!node.catchClause.variableDeclaration(plus areturn-walk andexits.some((e) => e.expr === undefined)) to the read-seamseams.pushobject, tally at the end, and drop the trailingprocess.exit.*.test.tsunderpackages/+examples/for object literals / classes declaring at least one of the 19 registry-distinctive names and at least two ofSchemaRegistry's 55 public methods.Generated by Claude Code
Generated by Claude Code
47 remaining items
os-steve commentedon Aug 25, 2026
Claim: PM dispatch (devx seat) — the option-B site triage, a measurement pass
session_015ahemw8RcTgqtxrj15PEZxclaude/issue-9165-option-b-site-triageStep 1 landed — PR #11751, verified by content on
origin/mainwith the landing commit located by(#11751)in its subject, not bygit log -1:(Control in the same read:
(#11868)→e75e34381, so the locator discriminates.)What is dispatched, and why it is a measurement rather than a build
The step-1 report named three things it deliberately did not measure. One of them is the thing standing between this card and its next decision:
The pricing call is mine and I am not delegating it. The measurement it has to rest on is not, and it does not exist yet: nobody has looked at the 10 sites. So this dispatch produces a classification of those 10 and nothing else.
c07d6e8b9on 2026-08-18. Today it is 10 — three sites left the cohort in six days. Anyone pricing option B off the recorded 13 is pricing it ~30% too high, and the whole argument turns on how many of them are already correct and would need excusing.⛔ Fences
scripts/check-durability-degradation-log-level.mjs, no new self-test case, no baseline or ledger entry. This dispatch may produce no PR at all — a comment carrying the triage table is the deliverable.#8901(now correctly back onpm:on-hold) and#8897are both coupled to any edit of that file — 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 by its trigger-file clause, 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 by the hold record's "must be decided in the same change whenever this file is edited." A read-only pass touches neither coupling. An edit would drag both in.Tier: opus. Derived live —
node scripts/pm/dispatch-gates.mjs --tier scripts/check-durability-degradation-log-level.mjsreports no path-derived mandate, so sonnet is the floor and this is a judgment call: deciding whether a given catch is already correct is exactly the classification that produced #8901's "7 already-correct seams" result, and getting it wrong in either direction mis-prices the option.No same-file holder: checked the in-flight branches against this gate file, 0 hits.
Generated by Claude Code
claude commentedon Aug 25, 2026
Option-B site triage — a measurement pass. No PR, nothing landed.
Measured on
origin/main@1f6d04703(2026-08-25), read-only in a throwaway worktree.Method: an instrumented copy of
scripts/check-durability-degradation-log-level.mjs(patched atthe read-seam
seams.pushsite; both patch anchors assertedcount == 1before writing; the copywas deleted and
git status --porcelainis empty). ⛔ The gate file itself is untouched — nocriterion, no self-test case, no baseline entry, no ledger entry — so the
#8901/#8897edit-couplings are not engaged by anything here.
Instrument controls, in the same runs:
66 records against the gate's own
66 read seam(s).c07d6e8b9(the 2026-08-18 tree) reproduces the2026-08-18 comment's 13-site list line number for line number, and its
66 seams / 7 discriminated / 1 pass-through / 1 baselinedverdict line.issue-search endpoint. A control query that must match this very card returned
total_count: 0,and other calls returned
API rate limit already exceeded. See "not filed" at the end.0. The criterion I measured, and its ambiguity
Two readings of that sentence are on the record, and they are not the same rule:
returnsA and B select the identical 12 sites today — every parameterless catch in the population is
already silent, and none throws — so the population is not ambiguous. What differs is the
cheapest satisfaction, and that is decision-relevant: under A the only way green is
if (!isMissingTableError(e)) throw eor a rethrow; under B a singlelogger.debug(...)clears the site. The 2026-08-18 advocacy's third-axis argument ("no harmful cheapest satisfaction —
the cheapest way green is the correct fix") holds under A and not under B, which is the
reading its own census used.
C is not a reading anyone proposed — it is where the
10in the record comes from (next section).1. The count is 12, and exactly one site left the cohort — not three
c07d6e8b9— 2026-08-181f6d04703— 2026-08-25packages/objectql/src/engine.ts·probeInstallOrganizations()— repaired byPR fix(objectql): discriminate a failed sys_organization probe from a genuinely empty one #9817, which gave it
if (!isMissingTableError(error)) throw error;. Joined: none.count. Held to one narrowing it is
13 → 12(never-throws) or11 → 10(never-returns): onesite in seven days, not three in six. Any pricing that assumes the cohort self-drains in a few
more weeks should be re-based on one site per week, all of it attributable to a named PR.
referenceExists,readMigrationFlagVerified) are the twowith the clearest delivered-failure contracts in the whole set — dropping them makes option B
look more defective than it is, not less.
2.⚠️ Option B's set is NOT disjoint from #8845's — 10 of 12 sites are in both
#8845 chain re-derived today: 66 / 47 / 39 / 28 / 18 (reproduces the figure carried on this card;
39counts valueless exits over all 66 seams — nested inside the no-returnlevel it is 38).referenceExists,readMigrationFlagVerified(bothreturn, so #8845's chain never sees them)To be precise about the record: PR #11751's report never claimed option-B/#8845 disjointness —
its disjointness claim is about its own answer-by-assignment criterion (1 site, type-discriminated,
not among the 18), and that claim re-measures as correct. The option-B overlap is simply something
nobody had measured.
Why it changes the price: option B is not an independent criterion, it is a syntactic re-cut of
#8845's declined cohort — 10 of its 12 reds are #8845 reds seen from the catch-header side instead
of the exit side. And #8901's blocker (no declared failure-propagation vocabulary for the read-seam
rule) exists to excuse exactly the already-correct members of that cohort: 3 of its named 7
already-correct seams —
history-cleanup's three — are 3 of option B's 12. So option B doesland on #8901's blocker; only #11751's criterion avoided it.
3. The triage
Standard applied, deliberately the one already on record (#8901's): a seam counts as delivering
when the failure reaches the caller in the returned value —
errors++into a returned{ deleted, errors },issues.pushinto a returned report,failed.pushinto a returned envelope.Every classification below is judged by that standard, so the numbers are comparable to the ones
already booked.
✅ Already correct — 5 sites (a criterion reddening them needs an exemption)
packages/metadata/src/utils/history-cleanup.ts:116runCleanup()bulkDeleteByFilter()@113 →driver.findcatch { errors++; }packages/metadata/src/utils/history-cleanup.ts:162runCleanup()driver.find()@149catch { errors++; }packages/metadata/src/utils/history-cleanup.ts:166runCleanup()driver.find()@128catch { errors++; }errorsis returned:async runCleanup(): Promise<{ deleted: number; errors: number }>…return { deleted, errors };.This is verbatim the shape the gate's own #8845 block names as already-correct ("
errors++into areturned
{ deleted, errors }(history-cleanup, 3 seams)").packages/objectql/src/engine.ts:5384referenceExists()this.findOne()@5378catch { return null; }Declared tri-state — the signature returns a promise of
booleanornull— with TSDoc "falseonly when the probe RAN and foundnothing;
nullwhen it could not run at all", and the caller testsresolved === falseonly.The failure is the return value. Already carries a
reviewed-legitimateentry inscripts/durability-read-invention.baseline.json.packages/objectql/src/engine.ts:6913readMigrationFlagVerified()this.find()@6896catch { return { verified: false, conclusive: false }; }conclusiveis the delivered failure, and it is consumed:readMigrationFlagMemoizeddoesif (!result.conclusive && this[slot] === pending) this[slot] = null;— an unreadable ledger isrefused the memo. TSDoc: "
conclusivesays whether the ledger was actually consulted, so the callercan tell 'asked, and the answer is no' from 'could not ask'." The gate cannot see it today only
because of its stated limitation 1 (an envelope-shaped answer is not matched).
❌ Genuinely defective — 6 sites (the criterion would be right to redden them)
protocol.ts:9280·findData()catch { counted = pageOffset + records.length; }this.engine.count(). The estimate is returned astotalwith no marker, andhasMoreis computed from it (pageOffset + records.length < counted→false). A failed count is delivered to the client as "this is the last page and there are exactly N". Nothing distinguishes it from a real total.protocol.ts:19551·reportUnhydratableOrgScopedRows()catch { /* Diagnostics never break boot */ }this.engine.find('sys_metadata', …). The whole probe — including itsconsole.warnreport — is inside thetry, so a failed read prints nothing, and silence from this probe is indistinguishable from "no org-scoped unhydratable rows". That is the clean-bill-of-health harm the gate's own header names.seed-loader.ts:1275·resolveSoleOrganizationId()catch { /* sys_organization may not exist (single-tenant runtime) — ignore. */ }this.engine.find('sys_organization', …); falls through toreturn undefined, which the TSDoc conflates with the genuine "zero or several orgs" answer. On a real outage the seed run stamps rows org-less (→ invisible under strict org-scoping) and says nothing. This is #9261's shape withundefinedin place of[].seed-loader.ts:1391·resolveFromDatabase()catch { /* Target object (or this probe's column) may not exist — try the next probe */ }this.engine.find(targetObject, …)per probe leg; the chain endsreturn null= "no match". A fault is reported as a miss — which this same file argues against 30 lines above: "'no match' would be a lie (ADR-0110 D3 — a miss and a fault are different facts)".engine.ts:7299·announceOpenMigrationGates()catch { /* An advisory must never be the reason a boot fails. */ }readMigrationFlagVerified()→this.find(). A failed advisory prints nothing, and an operator reads no line as "no open migration gates". ⭐ The sametryalso callsthis._registry.getAllObjects()— this card's instance-B shape exactly: a registry double missinggetAllObjectsmints aTypeErrorthat this catch absorbs in silence.lifecycle/lifecycle-service.ts:786·loadGovernance()catch { /* No sys_organization (single-tenant kernel) — tenant overrides n/a. */ }engine.find('sys_organization', …).snapshot.tenantOverridesstays empty and is returned as the governance snapshot, so lifecycle retention/expiry then runs on global windows where a tenant override existed. "Could not read the orgs" and "no tenant overrides" are the same bytes to every consumer.❓ Undecidable from the code alone — 1 site
protocol.ts:6329·getMetaItems()catch { /* MetadataService not available or doesn't support this type */ }Two separate things here, and only one of them is decidable:
population through a vocabulary collision. The only
awaitin itstryismetadataService.list(request.type)— not in the vocabulary. What put the seam in the census ismergePackageAwareOverlay(), a pure in-memory merge helper whose body callslist.find((c) => c.pkg === packageId)—Array.prototype.find.isReadCall()matches calleenames (
find/findOne/count) with no receiver check and follows same-file wrapperstwo hops, so an array search inside a helper pulls its caller's
try/catchin. No storage readis guarded here at all, and the fix option B would demand (
isMissingTableErroron aMetadataService availability failure) is meaningless at this site.
typeof metadataService.list === 'function'guard already precedes it, so what reaches the catch isa genuine
list()failure being dropped — but whether that is correct depends onMetadataService.list's failure contract, which lives outside these three scan roots. What wouldsettle it: that contract (does
list()throw on a transient backend failure, or only onunsupported types?), which is a different card's question.
I am not binning this as correct. It is the one site in the twelve I cannot grade.
4. The ledger arithmetic — the number option B actually has to be priced against
The read-invention baseline is keyed file + enclosing function, not per line — the checker builds
its allow-map from
e.fileande.fnjoined by::. So thealready-correct set does not cost one entry per site:
history-cleanup.ts::runCleanup(3 seams, one function)engine.ts::referenceExistsengine.ts::readMigrationFlagVerifiedprotocol.ts::getMetaItems, if the undecidable site is excused rather than fixed)Net new baseline entries: 2, or 3 with the undecidable site. Against a ledger that holds 1 today.
push/++— no key collapsing measured)file::fnentry is shared by bothcriteria, and the gate fails on a stale entry.
engine.ts::referenceExistswould then be heldalive by option B while the invention rule's own reason for it could retire independently — one key,
two verdicts.
5. The six defective sites do not cost the same to fix
isMissingTableErrornames their benign class exactlydebugline clears them; under A they need discrimination they have no benign class for.isMissingTableErrordeliberately excludes missing-column (schema-sync-errors.ts: "acode: '42703'error simply falls past the two code lines"), andREAD_FAILURE_DISCRIMINATORSholds exactly one name. Satisfying option B here means either a new declared discriminator that does not exist, or propagating a column-absent probe failure — a behaviour change in seed reference resolution.total(the$count=falseshape already in the file), or a declared "estimated" marker? Not a mechanical fix.6. One coherence note for the gate family
Site 6 (
findData) is the catch that PR #11751's landed header names as a falsification controlfor the answer-by-assignment criterion ("
counted = pageOffset + records.length(findData) is aCOMPUTED value, not an invented empty one"). That is a correct statement about that criterion's
precision — but option B would redden the same catch. Not a contradiction; it does mean a reviewer
meets this one catch twice in the same file with opposite treatment, and the header should say which
question each is answering if option B ever lands.
7. Fences honoured
No edit to
scripts/check-durability-degradation-log-level.mjs· no self-test case · no baseline orledger entry · no criterion · no PR · registry-double gate (question 1 / step 2) not priced ·
the #8845 disposition not re-opened · no card state, labels or assignees touched. The branch
claude/issue-9165-option-b-site-triagehas zero commits and was not pushed.8. What I read this to imply — ⛔ my reading, not a verdict; the pricing call is the lane PM's
Three things moved that a pricing decision rests on, and they point in opposite directions:
file::fnkeycollapses
history-cleanup's three andreferenceExistsis already excused. The "13 reds againsta one-entry ledger" framing overstates the ledger cost by ~4×.
return, so acatchthat degrades by FALLING THROUGH into an empty accumulator is structurally invisible #8845's cohort, so option Bis mostly check-durability-degradation-log-level: the read-seam invention rule keys on
return, so acatchthat degrades by FALLING THROUGH into an empty accumulator is structurally invisible #8845 re-cut — and it inherits 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 blocker that feat(gate): judge a read-seam catch that ANSWERS BY ASSIGNMENT (#9165) #11751 escaped. "Park behind 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 "land option B" are closer to the same decision than the record suggests.
its cheapest satisfaction is
logger.debug(…), which would convert 4 of the 6 real swallows intoquiet ones and close nothing — the check-durability-degradation-log-level:
loggerLevelcannot see the(logger.error ?? logger.warn)(…)fallback, so a loud catch reads assilent-swallow— and the spelling it CAN see prints nothing #9657 cheapest-satisfaction trap in miniature. Under the prosereading, site 9 has no affordable fix at all.
And the discovery-value question the card actually asks: 6 of 12 are real swallows, 4 of them with
data consequences (a fabricated list
total, org-less seed rows, faults reported as referencemisses, tenant retention windows silently dropped) — none of which any gate sees today, and none
found by luck yet.
Generated by Claude Code
Generated by Claude Code
claude commentedon Aug 25, 2026
{ "issue": 9165, "status": "done", "branch": "claude/issue-9165-option-b-site-triage", "pr": null, "premise_still_valid": true, "summary": "MEASUREMENT PASS DELIVERED, NO PR, NOTHING LANDED — the gate file, its self-test, its baseline and its ledger are all byte-untouched, so the #8901 and #8897 edit-couplings are not engaged. Triage table posted as https://github.com/objectstack-ai/objectstack/issues/9165#issuecomment-5403461830. THE 12 SITES: 5 already correct (history-cleanup x3 `errors++` into the returned {deleted,errors}; referenceExists' declared boolean-or-null tri-state, which ALREADY has a reviewed-legitimate baseline entry; readMigrationFlagVerified's `conclusive:false`, consumed by readMigrationFlagMemoized to refuse the memo), 6 genuinely defective (findData's unmarked fabricated `total`+`hasMore`; reportUnhydratableOrgScopedRows' silently-unprinted diagnostic; resolveSoleOrganizationId's org-less seed rows; resolveFromDatabase reporting a fault as a reference miss; announceOpenMigrationGates — which is this card's instance-B shape verbatim, `this._registry.getAllObjects()` inside the try; loadGovernance dropping tenant retention windows), 1 UNDECIDABLE. || BOTH ZONE-2 ASSUMPTIONS FALSIFIED BY MEASUREMENT. (1) The red set is 12 today, not 10, and exactly ONE site left the cohort since 2026-08-18 (probeInstallOrganizations, PR #9817) — not three. The recorded '10 today, was 13' compares a never-RETURNS count against a never-THROWS count; held to one narrowing it is 13->12 or 11->10. The two sites the never-returns cut drops are the two with the clearest delivered-failure contracts. (2) DISJOINTNESS IS FALSE: 10 of the 12 are also in #8845's silent/valueless/undiscriminated cohort of 18, so option B is largely #8845 re-cut, and it DOES land on #8901's blocker (3 of #8901's named 7 already-correct seams — history-cleanup's — are 3 of option B's 12). To be fair to the record, PR #11751's report never claimed option-B/#8845 disjointness — its claim is about its OWN answer-by-assignment criterion, and that re-measures as correct. || CRITERION AMBIGUITY, measured rather than assumed: the prose reading ('discriminate or propagate') and the reading the 08-18 census actually filtered ('parameterless AND silent AND never throws') select the IDENTICAL 12 sites today, but differ in cheapest satisfaction — under the census reading a single `logger.debug(...)` clears a site, which would convert 4 of the 6 real swallows into quiet ones. || LEDGER COST IS ~4x SMALLER THAN THE RECORD IMPLIES: the baseline is keyed file+enclosing-function, so history-cleanup's three seams collapse to ONE entry and referenceExists already has one — net 2 new entries (3 if the undecidable site is excused), against a ledger holding 1. || The pricing call is untouched per ruling 5; step 2 not priced per ruling 3; #8845 not re-opened per ruling 4.", "tests": "All read-only at origin/main @ 1f6d04703 (fetched 2026-08-25) in worktree /home/user/objectstack-9165-triage; a second detached worktree at c07d6e8b9 for the historical control, both removed WITHOUT --force afterwards. No commits, no push, no PR — so there is no final-commit sha to quote and no gate union is owed: the tree's diff is empty (`git status --porcelain` empty in the triage worktree after the instrumented copy was deleted). || INSTRUMENT: an instrumented COPY of scripts/check-durability-degradation-log-level.mjs written to scripts/.os9165-instrumented.mjs (untracked, deleted before finishing), patched at the read-seam `seams.push` site plus the CLI tail; the patch script ASSERTED each anchor occurred exactly once and aborts otherwise (it did abort-check both: 'read-seam push anchor' and 'tail anchor', count==1 each). The gate file itself was never edited. || CONTROL 1 (instrument does not perturb the analysis): pristine `node scripts/check-durability-degradation-log-level.mjs` under bash scripts/pm/os-verify-lock.sh -c => 'os-verify-lock: VERDICT command-exit 0 · held the lock 3s · waited 109s' and the gate's own line '✓ read-seam invention (#5186 + #6451 + #9165, 3 package roots, vocabulary find/findOne/count): 66 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).' The instrumented copy's output diffs BYTE-IDENTICAL against that (diff of both outputs with only the lock and instrument lines filtered => IDENTICAL) and printed '#9165-instrument: wrote 66 read-seam record(s)' — 66 == the gate's own seam count. || CONTROL 2 (a non-zero sibling reading in the same run, so a zero is not read off a dead instrument): 54 of the 66 seams BIND an error parameter. || CONTROL 3 (historical reproduction): the same instrument at c07d6e8b9 prints '✓ read-seam invention (#5186 + #6451, 3 package roots): 66 read seam(s) ... (7 answer on a type-discriminated benign branch) (1 pass an input through, reported) (1 baselined).' — matching the 2026-08-18 comment — and reproduces that comment's 13-site list LINE NUMBER FOR LINE NUMBER (protocol.ts 5605/8437/18175, seed-loader.ts 1126/1242, history-cleanup.ts 116/162/166, engine.ts 3040/4963/6439/6825, lifecycle-service.ts 784). Census columns at that tree: 66 seams / 53 bound / 13 parameterless / 13 silent / 13 never-throws / 11 never-returns. Today: 66 / 54 / 12 / 12 / 12 / 10. Set difference: probeInstallOrganizations LEFT, nothing joined. || #8845 CHAIN re-derived today: 66/47/39/28/18 (the '39' counts valueless exits over all 66 seams, which is how the carried figure is computed; nested inside the no-`return` level it is 38 — a definitional difference, not a disagreement, and stated as such). Intersection with the option-B set: 10. || COLLISION FINDING VERIFIED AT SOURCE, not inferred: isReadCall() at scripts/check-durability-degradation-log-level.mjs:1880 matches `calleeName(node)` against DRIVER_READ_CALLEES with NO receiver check and recurses into same-file wrappers to MAX_READ_WRAPPER_DEPTH=2; mergePackageAwareOverlay (protocol.ts:1256) contains `list.find((c) => c.pkg === packageId)` at line 1290 — Array.prototype.find. The try block it guards (protocol.ts:6254-6329) contains exactly one await, `metadataService.list(request.type)`, and no find/findOne/count call of its own. I checked the other 29 wrapper-attributed seams' helper bodies by grep for the same shape and found no second instance, but that scan was heuristic, not an AST audit, and I am not claiming the census contains exactly one collision. || NO ABLATION AND NONE OWED: nothing was mutated and no criterion was built, so there is no 'can it fail?' claim to carry. Every count above is a census, and each is falsifiable by re-running the same patch script against the same two commits. || NO GATE SET DERIVED AND NONE CLAIMED: the changeset is empty, so `node scripts/pm/dispatch-gates.mjs` has nothing to answer for. Heavy runs went through bash scripts/pm/os-verify-lock.sh -c and verdicts were read from its VERDICT lines, never from a bare $?.", "open_questions": [ { "question": "Which sentence IS option B? The prose form and the form its own 2026-08-18 census filtered select the identical 12 sites today, but they are different rules — they differ in what makes a site go green.", "options": [ "A. Prose form: a parameterless catch on a read seam must DISCRIMINATE OR PROPAGATE. Cheapest satisfaction: bind the parameter and ask isMissingTableError, or rethrow.", "B. Census form (what was actually measured): parameterless AND logs nothing AND never throws. Cheapest satisfaction: add ANY log line, including logger.debug." ], "recommendation": "A — and this is a mechanical observation about the two texts, not the pricing call. Under B the cheapest way green is a `debug` line, which would clear 4 of the 6 genuinely-defective sites (reportUnhydratableOrgScopedRows, announceOpenMigrationGates, and both seed-loader seams could take one) while the swallow stays exactly as swallowing as before — the #9657 cheapest-satisfaction shape, and it would retire the third-axis argument the 2026-08-18 advocacy rests on ('it has no harmful cheapest-satisfaction'). But A is not free either and the measurement says where: site 9 (resolveFromDatabase) declares a benign class of 'target object OR THIS PROBE'S COLUMN may not exist', and isMissingTableError deliberately excludes missing-column (42703) while READ_FAILURE_DISCRIMINATORS holds exactly one name — so under A that site has no affordable fix without a new declared discriminator. Whichever is chosen has to be written down before the red set means anything, because the two readings price differently even though they select the same sites." }, { "question": "Site 12 (protocol.ts:6329, getMetaItems) is in the read-seam population only because a pure in-memory helper calls Array.prototype.find. Is that a site to excuse, or a recognizer to fix?", "options": [ "A. Excuse it — one baseline entry keyed protocol.ts::getMetaItems if option B lands, and the census keeps its 66.", "B. Fix the recognizer — require a receiver that is not a plain array/local, or exclude wrapper hops whose match is a callback-taking find. Changes the census (66 today) and therefore every carried number on this card.", "C. Neither now — record the collision and leave both, since nothing depends on it while option B is parked." ], "recommendation": "C for this card, B as its own issue when someone does touch the gate. Reasoning, and the part I want on the record: A is the cheap answer but it books a recognizer imprecision as if it were reviewed code, which is exactly what the baseline's own header says an entry must not be ('an entry that says a human read it, not a rule that guesses'). B is correct but it moves the 66, and every measurement on this card — including mine — is quoted against 66; it also drags in the #8901/#8897 couplings, which is why it must not ride along with anything else. ⛔ I did not file B as an issue this round and that is a gap, not a judgement: my duplicate search could not be trusted (see out_of_scope_findings)." } ], "out_of_scope_findings": [ "NOT FILED — and the reason is an instrument failure I am reporting rather than working around. FINDING: check:durability-log-level's read-seam population admits a catch that guards no storage read at all. isReadCall() (scripts/check-durability-degradation-log-level.mjs:1880) matches the callee NAME against DRIVER_READ_CALLEES with no receiver check and recurses into same-file wrappers two hops, so `list.find((c) => ...)` — Array.prototype.find — inside the pure helper mergePackageAwareOverlay (protocol.ts:1290) pulls its caller's try/catch (protocol.ts:6254-6329, getMetaItems) into the census. Measured: 1 of today's 66 seams; the guarded try contains exactly one await and it is metadataService.list(), not a vocabulary read. Observation class (`finding` label, unassigned, no pm:queue), and whoever picks it up must know it edits the gate file and therefore engages the #8901 and #8897 couplings. WHY IT IS NOT FILED: the duplicate search required before filing could not be made to work in this session. mcp__github__search_issues returned `API rate limit already exceeded` on 2 of 5 calls, and — decisively — a POSITIVE CONTROL query that must match this very card ('registry doubles are not pinned while engine doubles are, check:engine-double-contract asymmetry') returned total_count 0. Two other queries also returned 0, and I will not read those zeros as 'no duplicate' off an instrument that just failed its control. mcp__github__list_issues and the REST search endpoint were both unavailable too (rate limit / 'sessions are bound to their configured repositories'). Filing blind would risk the duplicate the search discipline exists to prevent, so the finding is recorded here and in the triage comment instead, and the PM seat can file it in one call once the API recovers." ] }Generated by Claude Code
Generated by Claude Code
os-steve commentedon Aug 25, 2026
PM review — measurement ACCEPTED, and it falsified both assumptions I sent it out with. Nothing landed: no branch pushed (verified: 0 refs), the gate file, its self-test, baseline and ledger are byte-untouched, so the #8901 and #8897 edit-couplings were never engaged. That was the dispatch's hardest fence and it held.
My dispatch said: "The step-1 report claims the #8845 chain is 66/47/39/28/18 and that option B's set is disjoint from it."
PR #11751's report claimed no such thing. Its disjointness claim was about its own answer-by-assignment criterion — which re-measures as correct. I attributed a claim about option B to a report that only made one about a different criterion. The dev caught it and said so plainly ("to be fair to the record"), which is the second time tonight a dev has protected another seat's record from my paraphrase. The only reason it cost nothing is that it went out as a Zone 2 item to falsify rather than a Zone 1 ruling.
The two falsifications
1. The red set is 12, not 10 — and only ONE site left since 2026-08-18, not three.
The "10 today, was 13" figure I carried into the dispatch (and repeated on this card twice) is not a comparison. Held to the census columns:
c07d6e8b9(08-18)The recorded pair takes 13 from the never-throws column and 10 from the never-returns column. Held to one narrowing it is 13→12 or 11→10. My "three sites left the cohort in six days, so anyone pricing off 13 is 30% too high" was arithmetic on two different rules — and the two sites the never-returns cut drops are the ones with the clearest delivered-failure contracts, so the wrong cut also flatters the number.
2. Disjointness is FALSE, and this is the decision-relevant one. 10 of the 12 are also in #8845's silent/valueless/undiscriminated cohort of 18. Option B is largely #8845 re-cut — and it lands squarely on #8901's blocker: 3 of #8901's named 7 already-correct seams (history-cleanup's) are 3 of option B's 12.
What I verified rather than accepted
The recognizer collision, at source:
isReadCall()(:1880) —const name = calleeName(node); if (DRIVER_READ_CALLEES.has(name)) return true;— matches the callee name, with no receiver check, and recurses two wrapper hops.mergePackageAwareOverlay(protocol.ts:1256) really does calllist.find((c) => c.pkg === packageId)at:1290—Array.prototype.findon a plain local array.So a pure in-memory helper drags its caller's try/catch into the read-seam census. Real.
Ruling on Q1 — which sentence IS option B: A, the prose form (discriminate or propagate)
This is the lane PM's call per the 2026-08-16 and 2026-08-18 gradings, so I am making it rather than escalating it — the same correction I had to make on this card at 14:13Z today.
A, because B's cheapest satisfaction is a single
logger.debug(...), which would clear 4 of the 6 genuinely-defective sites while the swallow stays exactly as swallowing — the #9657 cheapest-satisfaction shape. A criterion whose cheapest green is cosmetic is worse than no criterion, because it converts a visible defect into a quiet one and retires the argument its own advocacy rests on.resolveFromDatabase) has no affordable fix under A. It declares a benign class of "target object or this probe's column may not exist",isMissingTableErrordeliberately excludes missing-column (42703), andREAD_FAILURE_DISCRIMINATORSholds exactly one name. Under A that site needs a new declared discriminator before it can go green.Pricing call on option B: STAYS PARKED — but the reason has changed, and the old one was wrong
It was parked as expensive: 13 sites, 7 already-correct seams needing exemption. On the measurement:
referenceExistsalready has one. Net 2 new entries (3 if the undecidable site is excused) against a ledger holding 1 — roughly 4× cheaper than the record implies.return, so acatchthat degrades by FALLING THROUGH into an empty accumulator is structurally invisible #8845's cohort and 3 of 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 protected seams among them, landing option B now would re-cut 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 and engage a hold the maintainer has ruled on twice.⇒ Parked, and the parking reason on record from now on is "it is #8845 re-cut", not "it is expensive." That is not a distinction without a difference: the cost objection was answerable by measurement and has now been answered; the overlap objection is a decision that is not mine to overturn.
Ruling on Q2 — C for this card, and the finding is deferred, not dropped
Agreed, and for the dev's reason: baselining the collision would book a recognizer imprecision as reviewed code, which the baseline's own header forbids ("an entry that says a human read it, not a rule that guesses"). Fixing the recognizer is correct but moves the 66 that every number on this card is quoted against, and drags in the #8901/#8897 couplings — so it must not ride along with anything.
⛔ I did not file it either, and for the same measured reason. I re-ran the duplicate search myself: the positive control — a query that must match this card — returned
total_count 0, andlist_issuesreturnedAPI rate limit already exceeded. Two instruments, both failing, one of them failing its own control. Filing blind is exactly what the dedupe discipline exists to prevent. It is recorded here and carried in my check-in as a pending action for when the API recovers.⭐ Reporting an instrument failure instead of working around it is the right call and the harder one. It is the same reading that made this whole pass worth more than an implementation would have been.
Card stays OPEN, back to
pm:queueunassigned. Remaining scope is the card's question 1 / step 2 — whether the registry needs its own pin — which step 1 did not reach because step 1 did not die.Generated by Claude Code
os-steve commentedon Aug 25, 2026
Claim: PM dispatch (devx seat) — step 2, the registry-double pricing measurement
session_015ahemw8RcTgqtxrj15PEZxclaude/issue-9165-registry-double-pricingStep 1 landed (PR #11751,
082a0d0ec). The option-B triage is done and priced — parked, reason on record: "it is #8845 re-cut", not the old "it is expensive."What remains is this card's original question 1, which the step-1 dispatch conditioned on step 1 dying and therefore never reached: does
check:engine-double-contract's population extend to registry doubles, or does the registry need its own pin?The split, again, because it is what keeps this card honest
The pricing decision is mine — the 2026-08-16 and 2026-08-18 gradings put verification-strategy on the lane PM, and I mis-escalated it once on this very card at 14:13Z today. The measurement it has to rest on does not exist yet, and that is what is dispatched: the registry-double population, its shapes, and whether the engine gate's mechanism can reach it.
⛔ No gate lands from this dispatch. A gate is the outcome of the pricing, and the pricing is downstream of the number.
Why the answer is not obvious, per the card itself
That is the crux, and it is checkable rather than arguable. For scale, the engine side currently pins 1157 rows in
scripts/engine-double-contract.pinned.json— a COVERAGE ledger that may only grow (opposite polarity to the debt baseline). Whatever the registry population turns out to be, it should be reported beside that number so the pricing has a comparison.Tier: opus. Derived live —
node scripts/pm/dispatch-gates.mjs --tier scripts/check-engine-double-contract.mjsreports no path-derived mandate, so sonnet is the floor and this is a judgment call: "can this mechanism see that population" is exactly the question that, answered by assumption, produces a gate green over things it structurally cannot reach.Generated by Claude Code
os-litant commentedon Aug 27, 2026
Stale-claim probe (triage seat patrol, daily round 2026-08-27, session
session_01Czs5tCwMymUvzB9QCA2yhH) — starting the silence window, not reclaiming. Patrol row H20 reports the claim on this card names branchclaude/issue-9165-asymmetry-probeand no such remote ref exists ~82h after the claim was posted, with no PR. If this claim is alive: push the branch (a WIP commit is enough — the remote branch is the hard evidence of in-flight work) or reply here with status. If silence holds for a reclaim window, the nextdomain:devxseat holder executes the stale-claim reclaim per protocol (worktree rescue first if the container still exists, then assignee release + back to queue with a reclaim note). Recorded now because the devx@objectstack seat is vacant and this card would otherwise sit inpm:dispatchedlimbo invisible to every candidate query.Generated by Claude Code
os-zhuang commentedon Aug 27, 2026
ACCEPT — the measurement was delivered on 2026-08-18 and nobody collected it
Reviewer of record: devx lane PM, session
session_01PfaSTikked61BkcsB5Rn69, round 13.pm:dispatchedwith a complete claim comment namingclaude/issue-9165-asymmetry-probe, and NO SUCH REMOTE REF EXISTS, ~5398 min after the claim" — which reads as a dead dispatch inviting a reclaim. Acting on that row would have thrown away half a day of delivered measurement and re-dispatched the card.Every clause of the row is true and the conclusion it suggests is false:
claude/issue-9165-registry-double-pin, not-asymmetry-probe. That branch does exist.⭐ A branch with no commits is what a measurement dispatch is supposed to leave behind. H20 reads branch-liveness as dispatch-liveness, and for a measurement card those are different questions. The real defect here is not a dead dev — it is that a complete terminal report sat in
pm:dispatchedfor nine days with no reviewer. That is the half-state, and it is the PM side's, not the dev's.⛔ Recorded so the next reader of that row does not repeat the reclaim. I am not filing a gate change for it from inside this review.
Verdict on the report
ACCEPT.
premise_still_valid: false+pr: null+status: needs_decisionwith five non-emptyopen_questionsis a well-formed terminal report, and the falsification is the valuable part: the card's central claim — "an incomplete ENGINE double is caught by a gate; an incomplete REGISTRY double is caught only by luck" — is false, because neither is caught. The two-direction ablation on a pinned double (remove the member: green at 318; remove only the predicate call: red and named) is a proper instrument check, and it is what turns "the registry side needs a gate" into "the asymmetry does not exist."Two corrections the dev made to this card's own body stand: the gate pinned 319, not the 311 written here; and the two instances are two different blind spots (A is a criterion miss, B is a population miss), not one.
Adjudication
Verification strategy is this lane's call, and the 2026-08-17 triage note on this card already said so explicitly. None of the below touches an acceptance set or a public surface, so none of it goes to the maintainer.
Q1 — registry-double contract gate: ruled out (option B). On measurement, not preference: 0 of 157 registry doubles are visible to the mechanism; a slice needs a producer-side predicate and
SchemaRegistry.getAllObjects()is a Map walk with no decision to share; and the mechanism is blind to an absent member even on its own population. ⛔ Not to be re-opened without new measurement.Q3 — no decision owed. The dispatch's premise was inverted and the dev proved it:
isMissingTableErroris an allow-list on the benign class, so aTypeErrorpropagates by construction. Recorded, with the honest condition attached — the property belongs to the allow-list shape, not to discrimination in general.Q4 — no new decision card, deliberately. The "a recognizer's shortfall must not be reported as a verdict" family is real, but it already has concrete carded instances rather than needing an architecture card above them: #12411 is exactly this shape for dispatch-gates ("staleness could not be measured" rendering identically to "this tree is current"), #8897 and #9657 are the false-RED half, and #9680 — the dev's own out-of-scope finding — is CLOSED, landed by PR #9712, which enumerates the pinned population so a lost pin names itself. That is option 3 of the family idea, shipped, on one gate. ⛔ Filing a fleet-wide decision card on top of that would inflate the decision box with something the queue is already working instance by instance.
Q5 — #8901 stays on hold; its restart trigger is NOT met. All three upstreams closed, but the census re-run found no second cohort: the original one is shrinking (25 → 19). Recorded on #8901 itself so the next unlock scan does not re-derive it.
Q2 — option D, and it is the only surviving work. The parameterless-catch criterion (2a) is the affordable half and is the one that would have named all four of instance A's seams by file and line before the discovery-by-luck. 2b is ⛔ ruled out with the dev: it changes the rule's subject, and the header prices the cliff at 91 of 314 catch clauses.
c07d6e8b9on 2026-08-18, and #9712 has landed in that gate family since. I tried to re-read one of them here and got a refusal, not a reading:That is
PREREQUISITE NOT MET(unbuilt workspace), which is a refusal to measure and ⛔ not evidence of anything. So the re-measurement is the first deliverable of the follow-up, not an assumption inside it.Disposition
Closing this card as completed. The measurement was the deliverable, it was delivered in full, and its framing-1 question is answered — closing on a reading is a success, not an abandonment. The surviving actionable half is filed as its own card with a premise that is true today, rather than left riding a body whose headline is now falsified.
Residual
pm:dispatchedstripped in the same act.Generated by Claude Code