Skip to content

Give check:slot-lookup a --self-test, and wire it like every sibling ratchet - #12154

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-12052-slot-lookup-ratchet-selftest
Aug 25, 2026
Merged

Give check:slot-lookup a --self-test, and wire it like every sibling ratchet#12154
yinlianghui merged 2 commits into
mainfrom
claude/issue-12052-slot-lookup-ratchet-selftest

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Fixes#12052

scripts/check-slot-lookup-ratchet.mjs — the #4251 sweep ratchet — was one of two
shrink-only ratchets under scripts/ with no selfTest() at all, and the only gate
among the two. Its wiring carried no self-test invocation either. This adds the self-test
and wires it the way every sibling ratchet spells it.

scripts/slot-lookup-baseline.json is untouched — not one entry. Verified in the diff.

Why the wiring gate could not see the gap

scripts/check-self-test-wired.mjs enforces the mechanically decidable superset — every
script CI runs that ships a --self-test must have that self-test run by CI too
— so a
gate shipping none is outside it by construction. Both verdicts, quoted from the gate's
own output:

BEFORE exit 0
✓ check-self-test-wired: every one of the 131 script(s) CI runs that ship a `--self-test` has that self-test run by CI.
scope: 180 file(s) under scripts/, 143 carrying `--self-test` in code (comments masked); 131 of those are run by 28 workflow(s); 127 have their self-test run through the flag, 4 through a recorded route.
AFTER exit 0
✓ check-self-test-wired: every one of the 132 script(s) CI runs that ship a `--self-test` has that self-test run by CI.
scope: 180 file(s) under scripts/, 144 carrying `--self-test` in code (comments masked); 132 of those are run by 28 workflow(s); 128 have their self-test run through the flag, 4 through a recorded route.

The gate's population moved by exactly one (143 → 144 carrying, 131 → 132 run, 127 → 128
through the flag): this gate is now inside its superset. And it is now enforced — with
the script's --self-test in place but the package.json invocation reverted, the wiring
gate goes RED naming this script ([self-test-not-run] scripts/check-slot-lookup-ratchet.mjs,
exit 1). Before this PR the same tree was green.

The rule the self-test pins — TYPED, never COMPLETE

check:slot-lookup bans erasing a service lookup to any. It does not check that the
type you name describes the object you call. #11681 recorded exactly that gap and was
closed not_planned (2026-08-25): a completeness check has no ground truth for the
deliberately narrow per-consumer interfaces the #4251 B4 decision asks for on an OPTIONAL
slot. So the boundary is pinned as a case that must stay silent, deliberately.

The fixture (written out in words, because GitHub's body sanitizer eats short
angle-bracket fragments): an interface SettingsReadSurface declaring exactly one member,
get(key: string); a ctx.getService(...) lookup on slot 'settings' carrying
SettingsReadSurface as its type argument; then a call to s.getMany([...]) — a
member the named type does not declare. The gate must stay SILENT on it. The literal
source is in scripts/check-slot-lookup-ratchet.mjs, in the SILENT table, under
"a narrow per-consumer interface stays silent even where the call exceeds it".

A case asserting completeness would pin a rule this gate does not have, and would go red
the day someone read it as a bug.

What the self-test drives — the real rule, never a re-implementation

The fixtures are linted through the live eslint.config.mjs rule, via the same
measuringConfig() the production run builds, and counted through the same
countRuleHits() predicate measure() uses. There is no second copy of the rule and no
second copy of the counter.

  • 5 erasure shapes proved to REPORT (all three selectors, the
    slot-lookup/no-any-assignment plugin rule for the split declaration, and the third
    lookup name getRequestKernelService).
  • 8 canonical spellings proved to stay SILENT (a typed lookup; the narrow-interface
    boundary above; both UNCONTRACTED_SLOTS exemption channels; getObjectQL; an unrelated
    as any; the typed split declaration the sweep produces).
  • The grandfathering channel proved both ways on a synthetic witness pair — same
    source, same config, one ignores entry apart — so the pin survives the baseline
    shrinking to zero entries (test(devx): sweep the shrink-only ratchet self-tests for baseline-presence pins — 33 derived, 1 repaired #12050's discipline). The real baseline is checked too, when
    it still has an entry to check.
  • 6 ratchet-comparison cases through the extracted, pure diffRatchet().
  • Both refusals (ruleBlockProblem, populationScopeProblem), each in both
    directions — neither is reachable from a healthy tree at all.

Ablations — both directions, each confirmed on disk, each restored by a trap

Every leg grepped the injected and the deleted text before and after the mutation
(counts below), and restored through trap … EXIT INT TERM.

#mutationon disk--self-testproduction run
A0package.json: drop the --self-test && invocationdeleted 1→0n/acheck-self-test-wiredRED (exit 1)
A1under-fire: SLOT_LOOKUPS = ['resolveService'] onlyinjected 0→1, deleted 1→0RED, 7 failuresRED, 25 problems (ratchet-DOWN)
A2over-fire: selector 3's TSAnyKeyword type-argument test relaxed to any type argumentinjected 0→1, deleted 1→0RED, 2 failures — a different setnot run
A3diffRatchet(): delete the count > allowed branchinjected 0→1, deleted 1→0RED, 1 failureGREEN, exit 0

A1 reddens the report cases (getService / getRequestKernelService shapes plus the
grandfathering pair); A2 reddens a disjoint set — the two typed-silent cases, including the
#11681 boundary case — so neither set is green under both mutations.

A3 is the load-bearing reading. Deleting a comparison branch left
node scripts/check-slot-lookup-ratchet.mjs printing its usual verdict —
✓ slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s) …, exit 0 — while
--self-test went red naming the case. That is the class of regression this gate had no
instrument for, and it is now measured rather than argued.

The header states the one place the blindness is not uniform, rather than overclaiming:
because a fall below the baseline is itself an error here, a weakening broad enough to erase
live sites reddens production too (A1). What that does not cover is a weakening over a shape
with no live sites, an over-fire, diffRatchet(), or either refusal.

Restructure, and why it is the shape it is

The script was one top-level program. It is now measuringConfig() / countRuleHits() /
measure() / ruleBlockProblem() / populationScopeProblem() / diffRatchet() /
baselineKeysAddedSinceMergeBase() / main(), matching its structural twin
check-query-options-erasure-ratchet.mjs. Behaviour, output text, exit codes and
--update are unchanged. Nothing is exported (so check:entry-guard's import-safety rule
is not newly reached), POPULATION_GLOB's literal spelling is untouched (so
dispatch-gates.mjs's hint extraction and its pins still read it), and the counted text
lints go through lintTextStrict() (so the #10599 call bans stay satisfied — verified by
check:query-options-erasure's adoption walk, which runs over both gates).

One bounded in-place repair, named here with its evidence

Two prose statements elsewhere assert "pnpm check:slot-lookup has no self-test hook"
scripts/eslint-fatal-guard.mjs and scripts/check-query-options-erasure-ratchet.mjs.
This PR falsifies the first clause of each while their conclusion (the guard-adoption walk
in the sibling's self-test is the only coverage of this gate's call site) stays true, so
both sentences are re-spelled rather than deleted. Comment-only; both files' gates run
green below. Scan for the class: grep -rn 'check:slot-lookup' over the tree — those two
were the only occurrences making a self-test claim.

Verification

All at the final commit 9fe3f66d5. Exit codes captured before any pipe; the verdict lines
are the gates' own.

The 9 families node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
derives for this diff, all exit 0: check:agent-test-spelling,
check:cross-package-test-inputs, check:entry-guard, check:parse-guard,
check:pnpm-filter-targets, check:query-options-erasure, check:slot-lookup,
check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs — plus
check:self-test-wired and check:nul-bytes.

$ pnpm check:slot-lookup # the newly wired spelling, end to end
✓ check-slot-lookup-ratchet --self-test: the live rule reports all 5 erasure shape(s) and stays silent on all 8 canonical spelling(s) …
✓ slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new, and every file in the population parsed.
baseline key set verified against a11c1a5: no files added.
$ pnpm check:query-options-erasure # walks guard adoption over BOTH gates
✓ self-test: 10 reporting shape(s), 10 silent counterpart(s), … every counted lint call in both gates AND in the closures they import routed through it …
✓ query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new …
$ pnpm check:entry-guard
✓ check:entry-guard: 161 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 117 export bindings, 115 of them inert on import (2 known-unsafe, ⛔ SHRINK-ONLY).

Repo-wide pnpm lint was narrowed, and the narrowing is declared rather than implied.
Run: eslint --no-inline-config --format json over the three changed .mjs files →
3 files linted, 0 errors, 0 warnings (count read from ESLint's own JSON, and all three
came back as linted results rather than ignored). The narrowing is sound because this repo
enables no type-aware linting — project: appears nowhere in eslint.config.mjs — so a
change confined to scripts/*.mjs cannot move the verdict on any file it does not contain.
CI runs the full farm regardless.

No changeset: root scripts/ + package.json only, no published-package source change ⇒
skip-changeset.


Generated by Claude Code

…ry sibling ratchet
The #4251 sweep ratchet was one of two shrink-only ratchets under scripts/ with
no selfTest() at all, and the only gate among them. Its verdict is the emptiness
of a finding set, so weakening its rule can only shrink what it reports and the
production run cannot tell a swept tree from a broken rule.
The self-test drives the REAL rule (eslint.config.mjs, through the same
measuring config the production run builds) over synthetic sources in both
directions, plus the ratchet comparison and both refusals, which no clean tree
reaches. The silent half pins the rule that EXISTS: TYPED, never COMPLETE —
#11681 was closed not_planned, so a narrow per-consumer interface must stay
silent here on purpose.
scripts/slot-lookup-baseline.json is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
Both files state that `pnpm check:slot-lookup` has no --self-test hook — a
premise for why the guard-adoption walk in check-query-options-erasure-ratchet's
self-test is the only coverage of the other gate's call site. The wiring change
in this PR makes the first clause false while the conclusion stays true, so the
sentences are re-spelled rather than deleted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@github-actionsgithub-actionsBot added size/l dependencies Pull requests that update a dependency file labels Aug 25, 2026
@yinlianghuiyinlianghui added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed dependencies Pull requests that update a dependency file labels Aug 25, 2026 — with Claude
@yinlianghui
yinlianghui marked this pull request as ready for review August 25, 2026 11:35
@yinlianghui
yinlianghui added this pull request to the merge queueAug 25, 2026
@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 25, 2026
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dequeued — base-branch failure, not this PR's. Nothing pushed here.

CI_FAILURE at 12:18Z. Cause is main, not this diff: packages/client's check:exported-any-returns fails on a stale ObjectStackClient.packages.update ledger row that PR #12062 (22c42c9b21) should have deleted when it closed the gap. Confirmed from the merge-queue job log, and confirmed on origin/main (packages/client/exported-any-returns.json:17 still carries it; #12062's diff is three files and the ledger is not among them).

This diff is scripts/check-slot-lookup-ratchet.mjs + package.json + two comment-only edits. It touches nothing under packages/client and cannot change what an SDK method's return type resolves to. Its own head was verified green across all 41 jobs before queueing.

⛔ Filed already by two other seats — #12180 (priority:p0) and #12184 — both dispatched; packages/client is domain:cli's surface. Full diagnosis and the duplicate-collision warning are on #12146 and #12184; not repeated here.

Waits for main to recover, then re-queues unchanged.


Generated by Claude Code

@yinlianghui
yinlianghui added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit 7468640Aug 25, 2026
43 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-12052-slot-lookup-ratchet-selftest branch August 25, 2026 14:12
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] check:slot-lookup is a shrink-only ratchet with NO self-test, and the wiring gate cannot see the gap

2 participants

@yinlianghui@claude