Skip to content

feat(gate): judge a read-seam catch that ANSWERS BY ASSIGNMENT (#9165) - #11751

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-9165-durability-log-level-population
Aug 24, 2026
Merged

feat(gate): judge a read-seam catch that ANSWERS BY ASSIGNMENT (#9165)#11751
os-steve merged 1 commit into
mainfrom
claude/issue-9165-durability-log-level-population

Conversation

@claude

@claude claude Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Part of #9165 — this lands step 1's criterion. #9165 stays open: the registry-double
gate (its question 1) is untouched here and this PR makes no claim on it.

What this is

check:durability-log-level's read-seam invention rule judges the expression a catch
returns. So catch { return []; } is red and catch { ids = []; } is green — one
invention delivered a statement apart, and only the first is seen. This adds a third
invention criterion for the second spelling, reusing inventedEmptyValue unchanged.

#9261 is where that spelling cost something. probeInstallOrganizations answered a
failed sys_organization read with an assigned — and then memoised — empty list.
resolveSystemWriteOrganization maps 0 organizations to "no organization yet", so system
inserts landed unstamped on a single install and the mandated multi-organization
refusal was silently skipped. It was found by the #8901 census re-run, not by this
gate; #9261's own filing names the reason: "this catch returns nothing and lets the
accumulator above the try answer."

⛔ This is NOT #8845's declined criterion

The distinction is the whole reason this one is affordable, and it is stated at length in
the gate header next to the #8845 block it narrows.

#8845 (declined, unchanged) this criterion
catch writes nothing — falls through an invented empty value, explicitly
which variable is the answer the checker must guess the catch names it
value judged by a new inference inventedEmptyValue, unchanged
first-run reds 15, of which 7 already correct 0
publishPackageDrafts clears it (why the exemption is unsound) never in the set — a push, not an assignment

push / ++ / += accumulation stays unjudged, #8845's census is unchanged, and
#8901 is untouched. This closes one spelling of #8845, not #8845.

Measured before it was added — 收窄先行, the #6451 discipline

Census over the three scan roots at origin/main @ 945ffbea8, 66 read seams, narrowing
one criterion at a time (instrumented copy of the checker, seams.push site):

narrowing sites
catches containing ANY plain x = … assignment 3
…whose assigned value is an invented EMPTY one 1
…written to a name declared before the try 1
…silent AND undiscriminated → first-run RED set 0

The one site the criterion discovers is #9261's own seam, and it passes — PR #9817
gave it if (!isMissingTableError(error)) throw error; and the existing type-discrimination
exemption reads that correctly. Zero baseline entries, no new vocabulary, no new declared
name, no new script, no new CI step. Same answer #6451 measured, same reason it was affordable.

The other two assignments are the falsification control, and the criterion has to
decline both — it does: counted = pageOffset + records.length (findData) is a computed
value, and storeUnavailable = true (loadMetaFromDb) is a flag the catch raises so
the caller is told, the opposite of an invention. Both are pinned as passing fixtures.

⚠️ A zero red set is only a fact if the selector is known to select

A widened criterion that reports nothing is indistinguishable from a broken one — this
card's own subject. Three things make this one falsifiable rather than silently green.

1. The green verdict counts what the criterion found, so a run that finds none is
visibly different from today's. Before / after on the same clean tree:

before : … 66 read seam(s), none invents an unreported answer (7 answer on a type-discriminated
         benign branch) (1 pass an input through, reported) (1 baselined).
after  : … 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).

Note the seam census is 66 in both: this is a criterion extension, not a population
extension. No seam was added to the scan; DRIVER_READ_CALLEES is untouched.

2. Two-direction ablation on the real tree, planting #9261's pre-#9817 shape in
packages/objectql/src/engine.ts — direction predicted in writing before running, and
observed as predicted.

Mutation proven on disk by anchor counts in both directions plus sha256, never by an
editor exit code
: removed-text count 1 → 0 (measured inside the function), injected
marker 0 → 1, sha256 fdcb0ecb… → 09e6df3e…, wc -l 12941 → 12940. The script asserted
the removed-text count was exactly 1 and would have aborted the reading as void otherwise,
and it carried trap … EXIT INT TERM so a foreground-cap SIGTERM could not leave the tree
mutated.

leg result
patched gate on the mutated tree exit 1, exactly 1 violation
pre-change gate on the same mutated tree exit 0, green — the control

The control is what proves the new criterion is what fires, not something else. Its
message, quoted from the gate:

packages/objectql/src/engine.ts:3452  (in probeInstallOrganizations())
  guards  : find() at line 3443 — a multi-row read (IDataDriver.find)
  found   : catch logs nothing at all and writes `ids = []` at line 3455 into a variable
            declared before the `try`, which answers for the read below it

Restore proven byte-identical: sha256 back to fdcb0ecb…, injected-marker count back
to 0, git status --porcelain empty for the file. No rebuild leg applies and this is
verified rather than assumed — the gate is a pure AST scan reading
packages/objectql/src/** from disk, which the control run demonstrates empirically.

3. Five self-test fixtures, both directions, so deleting the criterion reddens
--self-test rather than passing vacuously. expectInvents pins the reported set, not
just the verdict — the #6451 lesson that expectViolation: false is a vacuous assertion
for a case that passes because an exemption fired.

Self-test: 35 → 40 read-seam cases.

⚠️ The one judgement call in this PR, stated for the reviewer rather than buried

#8901 says "⛔ Do not re-open the #8845 decision." This PR does not, and here is the
check rather than the assertion:

If the reviewer reads it the other way, the honest description of what to revert is one
commit — no ledger, no baseline and no vocabulary entry was created, so nothing outlives it.

Verification at 1d2f19a77

Gate union derived at this head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
(from the tree's own working-tree changeset, not a hand-written path list), all green,
each quoted from the gate's own verdict line, exit codes captured before any pipe:

  • check:durability-log-level✓ read-seam invention (#5186 + #6451 + #9165, 3 package roots, vocabulary find/findOne/count): 66 read seam(s), none invents an unreported answer … and ✓ self-test (read-seam invention rule): 40 case(s) passed
  • check:nul-bytescheck-nul-bytes: OK (scanned 6557 text file(s) … no raw ASCII control bytes).
  • check:agent-test-spelling✓ … 0 violations
  • check:cross-package-test-inputsOK: 16 package(s) read outside themselves, all declared
  • check:entry-guard✓ check:entry-guard: 143 scripts/ file(s) …
  • check:parse-guard — exit 0
  • check:pnpm-filter-targets✓ … 135/168 --filter occurrence(s) … resolve

Heavy commands ran through bash scripts/pm/os-verify-lock.sh -c; verdicts read from its
VERDICT lines.

No changeset — skip-changeset

Checked against the actual rule rather than assumed: changeset-check in
.github/workflows/pr-automation.yml exempts on the skip-changeset label (or the
changeset-release/main PR) and has no path-based carve-out, so a label is the only
route. This PR touches one CI gate script under scripts/, which sits in no published
package (scripts/ has no package.json; the root manifest is private: true) — nothing
published changes. Direct precedent: PR #9712, a gate-only change to this same family,
merged with skip-changeset and the same justification.

Corrections to the record this run measured

Generated by Claude Code


Generated by Claude Code

The read-seam invention rule judges the expression a `catch` RETURNS, so
`catch { return []; }` is red and `catch { ids = []; }` is green — one
invention delivered a statement apart, and only the first is seen. #9261
is where that cost something: `probeInstallOrganizations` answered a
failed `sys_organization` read with an assigned-and-memoised empty list,
system inserts landed unstamped, the mandated multi-org refusal was
skipped, and the gate was green throughout. It was found by the #8901
census re-run, not by this gate.

Third invention criterion, on the same three preconditions (a read in the
`try`, no log at all, no type discrimination): an `inventedEmptyValue`
written into a variable the function declared BEFORE the `try`. It reuses
the value table unchanged — the criterion is about how the invention is
DELIVERED, never about which values count as invented.

⛔ This is NOT #8845's declined fall-through criterion, and the difference
is why it is affordable. #8845 judges a catch that writes NOTHING, which
needs the checker to guess which variable is the answer (15 sites, 7
already correct, and the obvious exemption clears `publishPackageDrafts`'
fabricated push). Here the catch NAMES the variable. `push`/`++`/`+=`
accumulation stays unjudged and #8845's census is unchanged.

Measured before it was added (收窄先行, the #6451 discipline) at 945ffbe
over the three scan roots, 66 read seams: 3 catches carry any plain
assignment, 1 assigns an invented empty value to a pre-`try` name, and the
first-run RED set is 0 — that one site is #9261's own seam, passing
because PR #9817 gave it `isMissingTableError` discrimination. Zero
baseline entries, no new vocabulary, no new declared name.

A criterion whose discoveries are invisible on a green run cannot be told
from a selector that stopped selecting — this card's own subject — so the
green verdict now counts them: 7 type-discriminated seams becomes 8, plus
a named answer-by-assignment count.

Part of #9165

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 24, 2026

Copy link
Copy Markdown
Collaborator

ACCEPT. Verified by content against origin/main @ 2a6122bd9 and against the branch at 1d2f19a77, not by reading the report back.

Both corrections to the record hold

You corrected the card on two points and both check out on main:

1. PR #9284 landed propagate, not discrimination. packages/objectql/src/engine.ts carries const objects: ServiceObject[] = this._registry.getAllObjects(); at three sites (7448, 10562, 10805), and grep -c 'objects = \[\]' over the file is 0. Both halves of the swallow are gone. So the 2026-08-18 re-pricing's antecedent — that #9154/#9284's landed shape is the isMissingTableError precedent — is not met, exactly as you say.

2. The precedent that actually transfers is #9817 / #9261. probeInstallOrganizations at engine.ts:3439, inside its catch:

if (!isMissingTableError(error)) throw error;
ids = [];

Discrimination, on a catch that answers by assignment, in the read-seam rule's own scan root. That is the shape, verbatim.

And the conclusion you drew from it is the part I want to name. The easy report here was "the named precedent is dead, so step 1's premise is dead" — literally true of the antecedent, and wrong. You found the precedent that post-dates the re-pricing and proceeded on it. Reporting a card dead on a technicality when a better precedent exists is a failure mode I would not have caught from the outside.

The population claim — and a correction of my own

READ_SEAM_SCAN_ROOTS and DRIVER_READ_CALLEES are byte-identical between main and the branch. Vocabulary still find / findOne / count. This is a criterion extension, not a population extension, as claimed.

I say that having first measured it wrong. My extractor terminated the range on ^]; — but DRIVER_READ_CALLEES is a new Map([…]), so the terminator never matched, the range ran to EOF, and I got a 3785-line "array" whose hash differed. The reading was my instrument, not your diff. (Second time today a range-based extractor under-read a declaration whose shape I assumed; noting it against my own method, not yours.)

Criterion and fixtures, by content

probe main branch
verdict tag #5186 + #6451+ #9165 changed ✓
answer by writing a pre- (the new count clause) 0 1
assigns ids = [] 0 2
declared before the `try` 0 4
counted = pageOffset (falsification control) 0 2
storeUnavailable = true (falsification control) 0 2
publishPackageDrafts (#8845 push fixture) 2 5
type-discriminated 6 7

Controls that must NOT move, and don't: READ_SEAM_SCAN_ROOTS 4=4, 'find' 1=1. Without those the table above only shows "the branch is bigger than main", which is not a measurement.

git diff --name-only origin/main...1d2f19a77 = one file. No ledger entry, no baseline entry, no vocabulary entry, no new script, no new CI step — so your "the revert is one commit and nothing outlives it" is structurally true, not a reassurance.

The judgement call — you were right to flag it, and it holds

I checked the quote the whole thing turns on. #8901 says verbatim: "⛔ Do not re-open the #8845 decision. Recording the measurement and adding no criterion was the ruled outcome." And its scope warning carries "drop it and this shape matches 91 of the 314 catch clauses" — which is why my grep for that string in the gate file read 0 in both trees; it lives in #8901's text and you attributed it there correctly.

The disjointness argument survives the check because the two chains branch at their first narrowing, not at their last:

A catch cannot be in both sets: the first is defined by writing nothing, the second by writing something. That is a structural exclusion, not a coincidence of today's numbers, and it is what makes "not one site moves between them" load-bearing rather than incidental. The one seam you discover is type-discriminated, so it is excluded from #8845's chain at the last narrowing too — belt and braces.

And the decisive practical point: #8901 exists because #8845's criterion reddens 7 already-correct seams that nothing declared can excuse. Yours reddens 0. It asks nothing of the missing vocabulary, which is the entire content of #8901's blocker.

⇒ Not a re-opening. Ruling made at this seat — prior triage twice recorded #9165 as lane-PM territory, so this is mine to make, and I am not routing it to the decision box.

What I am carrying forward

Two of your measurements are decision-relevant beyond this PR and I am recording them on #8901 rather than letting them sit in a merged PR body: the #8845 chain now reads 18 (was 19 on 08-18, 25 at 8664a2c) with still no second cohort — which is #8901's own Restart-when: condition, measured and still unmet — and option B's red set is 10, not the 13 the record keeps quoting.

The two-dot/three-dot trap you hit is real and I have hit the same class today: origin/main moved from 945ffbea8 to 2a6122bd9 under you mid-run, so origin/main..HEAD attributed four other seats' merged files to your branch. Your three-way confirmation (recorded $BASE, three-dot, and the PR's own changed_files: 1) is the right shape.

#9165 stays OPEN — this is Part of, the registry-double gate (question 1) is untouched, and step 2 was correctly not reached since step 1 did not die. I will strip pm:dispatched by hand when this lands.

Flipping to ready. Arming once every check is green — mergeable_state: clean covers only the required subset.


Generated by Claude Code

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

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants