Repository navigation
finding(devx): COVERED_SPECIFIERS is documented GROW-ONLY but only @object-ui/i18n is pinned — dropping any other member retires that ratchet silently #8018
Description
Activity
- addeddomain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repoobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
on Sep 8, 2026 Triage routing
Labels
toolingfindingdomain:devxpm:queuepriority:p3· Type TaskLane —
domain:devx. The gate isscripts/check-vi-mock-inherit.mjsand the fix lands inscripts/__tests__/check-vi-mock-inherit.test.ts— the engineering face, and the gate's subject is test-mock hygiene rather than any product contract.Re-measured on
83d4fda16— the card's central claim holds, and I verified it the way it needs verifying: by looking for the pin that would refute it.The one membership assertion is at
:1185—).toContain(I18N);. Searching the whole test file for any equivalent pin on@object-ui/reactfinds none: the only occurrences are:25(a prose docblock),:61(const COVERED = '@object-ui/react'— a fixture constant, not an assertion about the real list),:563(a prefix-boundary case), and:1090(a comment). ⛔:61is the one a hasty reader would mistake for a pin; it is not one.The generic assertions are at
:469-479and are exactly as the card's table describes:expect(COVERED_SPECIFIERS.length).toBeGreaterThan(0); for (const spec of COVERED_SPECIFIERS) { … expect(…name).toBe(spec); }
⭐ And that test's own comment states the limit the card is pointing at: "A typo here empties the population silently, and the gate then reports OK over nothing." — i.e. it was written against emptying, not against removing one member. The card is not disagreeing with the guard; it is observing that the guard's stated scope and the ratchet's stated promise are not the same size.
Admission — class (b), violation of a declared contract.
check-vi-mock-inherit.mjs's header states the covered set is GROW-ONLY, and the ratchet's entire force rests on that. The enforcement covers one member.The mechanism, restated because it is the part that must survive into the fix:
findCallSitesclassifies a call site ascoveredonly when its specifier is inCOVERED_SPECIFIERS; everything else isworkspace/unjudged. So removing a member reddens nothing — it makes that specifier's whole population invisible and the gate reports OK over a tree it no longer looks at. Deleting@object-ui/reactleavescheck:vi-mock-inheritgreen, the guard's own suite green (length > 0still holds, i18n still present), and 134 sites unwatched. The three specifiers objectui#6892 slice 1 adds inherit the same gap.⛔ The obvious backstop does not reach, and the card measured why.
FLOORS.covered = 50(check-vi-mock-inherit.mjs:1186) is the nearest thing: with@object-ui/reactremoved the covered count is still 95 (i18n's 92 plus the three 1-site specifiers) — comfortably above the floor. It only catches a collapse to near-nothing, which is the case it was written for. ⛔ Do not "fix" this by raising the floor: a floor is a count, and the defect is about which population is being counted.Grade p3. Nothing is wrong today — no member has ever been removed and the gate is green. This is a guard-strength finding, filed because the protection currently reads as general and is not. Re-grade to p2 if a member is ever removed in a PR (which is the event the pin exists to catch, and would prove it does not).
On the suggested shape — I endorse it and add one constraint. Replacing the single-member
toContain(I18N)with a pin over the swept set — every specifier a landed sweep has added, listed once, asserted present — keeps the existing case's rationale and makes it hold for all of them. The card's justification for why this is a ledger rather than an exemption table is correct: each entry is justified by a merged sweep PR.⛔ The constraint: the new pin must be a literal list, not derived from
COVERED_SPECIFIERSitself. The verdict-line case at:977(const named = COVERED_SPECIFIERS.join(', ')) is the cautionary example already in the file — it adapts to any removal rather than resisting it, which is precisely the property that makes it useless as a ratchet. A pin that reads its expectation from the thing it is pinning cannot fail. That is this repo's recurring shape and it is worth naming in the new test's docblock.Verified: nothing else reads the constant.
git grep -l COVERED_SPECIFIERSreturns the gate, its test, six changesets and one workflow (.github/workflows/vi-mock-specifiers.yml) — no other consumer. The card said "one changeset"; there are six now, all inert for this purpose. So the pin really is the only protection there is.Correctly not fixed under objectui#6892 slice 1 — that slice's file surface is the three converted test files plus the three lines in the constant, and generalising this pin is a decision about the ratchet's own pin rather than part of the sweep.
Triage seat only — no claim, no dispatch, no code.
Generated by Claude Code
Claim: PM loop round R48 —
domain:devx @ objectuiexecution seat.
Session:session_01FhBNJcLRZLe8M87VcUgpKr
Branch:claude/issue-8018-covered-specifiers-membership-pin
Worktree:/home/user/objectui-issue-8018
Domain:domain:devx
Dispatch: oneos-devsubagent of this session (mode:subagent,model: opus); the dispatch note follows. Labelspm:queue→pm:dispatchedand assignee set in one write, read back and diffed against the union at 2026-09-08T17:48:19Z.Why this card
Written order, as above: no
p0, ⛔ nopm:blocking, notarget:, theBuglevel unworkable ⇒ the rest, oldest first; third of the 2026-09-06 group.⭐ The reading that makes this card, and the trap inside it
Triage verified the claim the way it needs verifying — by looking for the pin that would refute it, and found none.
⚠️ scripts/__tests__/check-vi-mock-inherit.test.ts:61isconst COVERED = '@object-ui/react'— a fixture constant, ⛔ not an assertion about the real list, and it is exactly what a hasty reader mistakes for the missing pin.⭐ And the guard's own comment states the limit: "A typo here empties the population silently, and the gate then reports OK over nothing." — written against emptying, ⛔ not against removing one member. The gate's header says the covered set is GROW-ONLY; the enforcement covers one member, while
@object-ui/reactcarries 134 judged call sites and no membership pin at all.Concurrency and file surface
Three in flight (
batch:3), surfaces pairwise disjoint — this card touchesscripts/__tests__/check-vi-mock-inherit.test.ts(andscripts/check-vi-mock-inherit.mjsonly if the fix genuinely requires it).
Generated by Claude Code
Dispatch — round R48,
domain:devx @ objectuiexecution seat, 2026-09-08T17:49ZOne
os-devsubagent of sessionsession_01FhBNJcLRZLe8M87VcUgpKr(mode:subagent,model: opus), branchclaude/issue-8018-covered-specifiers-membership-pin, worktree/home/user/objectui-issue-8018. Three in flight (batch:3), surfaces pairwise disjoint.Adjudication section
⛔ No maintainer ruling applies. The constraints are the card's and triage's (
5583428563).⚠️ The trap, carried in as a required discrimination⛔
:61is the one a hasty reader would mistake for a pin; it is not one.check-vi-mock-inherit.test.ts:61isconst COVERED = '@object-ui/react'— a fixture constant, ⛔ not an assertion about the real list; the other occurrences are a docblock (:25), a prefix-boundary case (:563) and a comment (:1090). ⇒ a symbol grep concludes "already pinned" and is wrong. The dev must state how it established each occurrence's syntactic role, ⛔ not that it grepped.⭐ And the guard's own comment names the gap exactly: "A typo here empties the population silently, and the gate then reports OK over nothing." — written against emptying, ⛔ not against removing one member. ⇒ the card is not disagreeing with the guard; the guard's stated scope and the ratchet's stated promise are not the same size.
The change, and what it may not become
Dropping any member must redden, not just
@object-ui/i18n. Shape decided before implementation and justified in the PR body, under these:- ⛔ It must resist removal of every current member, including ones added later —
⚠️ a pin listing today's members by hand is itself a hand-copy that will drift, which is this repo's recurring disease. - ⛔ It must not restate
COVERED_SPECIFIERSfrom the same import: that assertsx === xand holds nothing. ⭐ If the pin cannot fail, it is not a pin — the mutation test is what proves it can. - ⛔ Nothing existing is weakened: the
length > 0floor, the real-package loop andtoContain(I18N)all stay. ⛔ No threshold lowered, no assertion deleted, no test skipped.
⭐ The mutation test is the deliverable that proves the work
One leg per member, mutating the gate and ⛔ never the test; each mutation proven to have reached disk by anchored grep or blob hash, ⛔ not by an editor's exit code; restored under an
EXIT INT TERMtrap and proven restored by blob hash. ⭐ The load-bearing reading is that exactly the new pin fails and the rest of the suite keeps passing — plus a control leg: the same removal against the unmodified test file must leave it green, which is the defect reproduced. ⛔ Without that leg nothing has been shown to close.Changeset
⚠️ The dev picks the form fromAGENTS.md§9 itself; ⛔ this brief names no bump level.
Generated by Claude Code
- ⛔ It must resist removal of every current member, including ones added later —
os-dev-report
{ "issue": 8018, "status": "done", "branch": "claude/issue-8018-covered-specifiers-membership-pin", "worktree": "/home/user/objectui-issue-8018", "pr": "https://github.com/objectstack-ai/objectui/pull/8630", "pr_number": 8630, "pr_draft": true, "head_sha": "7c8d521e277f35b71b3eed317c1e76855e1fd0d8", "base_sha": "86ef0c7643f82a8843fd430ed3a746e52f677235", "premise_still_valid": true, "premise_correction": "The card's DEFECT holds and is larger than filed; the card's chosen EXHIBIT does not. Dropping `@object-ui/react` today does NOT leave the guard's suite green (11 of 86 cases fail, incidentally). The unprotected population is 18 members / 384 judged call sites, not 1 member / 134 sites.", "preconditions": [ { "id": 1, "claim": "header declares the set GROW-ONLY; findCallSites classifies by membership, so a removal makes a population invisible rather than red", "verdict": "CONFIRMED", "reading": "scripts/check-vi-mock-inherit.mjs — header '## Scope: a declared, GROW-ONLY set of covered specifiers' and, above the constant, 'The workspace packages this gate judges. GROW-ONLY, and a specifier joins it only after its frozen factories have been swept to zero'. findCallSites: `: isCovered(specifier) ? 'covered' : specifier.startsWith('@object-ui/') ? 'workspace' : 'external';` then `if (scope !== 'covered') { sites.push({ ..., verdict: 'unjudged', ... }); continue; }`. Empirically confirmed too: dropping 18 of the 21 members left the guard's own suite at 86 passed / 86, exit 0.", "floor_does_not_reach": "FLOORS.covered = 50 against 666 judged sites today; the largest member is 136." }, { "id": 2, "claim": "toContain(I18N) is the only membership assertion; @object-ui/react has none", "verdict": "CONFIRMED as an assertion-level claim; its stated CONSEQUENCE ('the guard's own suite stays green') FALSIFIED", "how_established": "Not by symbol grep. Enumerated every expression in the test file whose subject is the imported COVERED_SPECIFIERS and read each one's syntactic role: :472 `expect(COVERED_SPECIFIERS.length).toBeGreaterThan(0)` (cardinality, argument is `.length`); :473 `for (const spec of COVERED_SPECIFIERS)` (per-member real-package loop, the assertion subject is a package.json read); :576 the same loop shape asserting `spec.split('/')` has length 2 (package-root shape); :977 `const named = COVERED_SPECIFIERS.join(', ')` (a derived VALUE feeding a RegExp, not an expectation); :1183-1185 `expect(COVERED_SPECIFIERS, '...').toContain(I18N)` — the sole assertion whose actual is the real list and whose expected is a literal member. Then the four `@object-ui/react` occurrences by enclosing construct: :25 sits inside the file's leading `/** ... */` docblock (prose); :61 is a top-level `const COVERED = '@object-ui/react';` declaration — a fixture constant, and every one of its 17 uses is a fixture INPUT (`mockCall(COVERED, ...)`, `verdictOf`'s default `spec`, `{ covered: [COVERED] }` — an override that replaces the real list outright — or a template inside a failure message), never the actual or expected of an assertion about the real list; :563 sits inside a `//` comment within `it('a sibling package whose NAME starts with a member is NOT covered')`, whose assertions are about `${COVERED}-native` / `${COVERED}ive` fixtures; :1090 sits inside a `/** ... */` docblock in the nested-generic describe. So no membership pin on @object-ui/react — the trap named by triage is confirmed as a trap.", "falsified_half": "Removing @object-ui/react from the gate reddens 11 of 86 cases today (THE ELEVEN pinned against real files, the automock/inline-factory/historical-instance/discriminating-neighbour cases, and the non-zero-exit case). Cause: the fixture constant at :61 IS a real member, and those cases run against the DEFAULT covered set, so the fixture sites fall out of `covered`. That is protection by coincidence — it depends on a fixture spelling and on which files exist on disk, its failure text names no ratchet, and it covers exactly two members (@object-ui/components reddens 1 case the same accidental way, via THE SUBPATH SITES / @object-ui/components/ui/sonner)." }, { "id": 3, "claim": "current contents of COVERED_SPECIFIERS; are #6892 slice 1's three members in?", "verdict": "CONFIRMED — all three are in; the set holds 21 members (the header says 'twenty-one', matching)", "contents_in_source_order": ["@object-ui/react", "@object-ui/i18n", "@object-ui/plugin-markdown", "@object-ui/data-objectstack", "@object-ui/plugin-report", "@object-ui/plugin-charts", "@object-ui/plugin-dashboard", "@object-ui/auth", "@object-ui/collaboration", "@object-ui/plugin-form", "@object-ui/components", "@object-ui/plugin-grid", "@object-ui/permissions", "@object-ui/plugin-detail", "@object-ui/plugin-chatbot", "@object-ui/plugin-designer", "@object-ui/plugin-list", "@object-ui/fields", "@object-ui/app-shell", "@object-ui/plugin-view", "@object-ui/core"] }, { "id": 4, "claim": "the 134-call-site figure for @object-ui/react", "verdict": "RE-MEASURED — today's number is 139, not 134", "instrument": "the gate's own exported scan(), the same code path `node scripts/check-vi-mock-inherit.mjs` runs, invoked as scan(repoRoot, { covered: ['@object-ui/react'], floors: {} }) on 86ef0c764 — a call form, not a substring probe", "reading": "@object-ui/react judged=139 inherits=139 frozen=0 unreadable=0", "controls": "Positive control in the same class and the same run: @object-ui/i18n judged=93 (a known non-empty population the suite independently asserts is >60), so the instrument counts. Negative control in the same class and the same run: @object-ui/no-such-package-8018 judged=0 — a zero is reachable and distinguishable, so 139 is not an artefact of every probe reading non-zero. Whole-set control: the unmodified gate binary prints '666 call site(s) ... judged (666 inherit, 0 auto-mocked); 0 other workspace', matching the per-member sum of 666." } ], "per_member_judged_sites_on_86ef0c764": {"@object-ui/react": 139, "@object-ui/i18n": 93, "@object-ui/plugin-markdown": 1, "@object-ui/data-objectstack": 1, "@object-ui/plugin-report": 1, "@object-ui/plugin-charts": 1, "@object-ui/plugin-dashboard": 3, "@object-ui/auth": 136, "@object-ui/collaboration": 34, "@object-ui/plugin-form": 33, "@object-ui/components": 50, "@object-ui/plugin-grid": 25, "@object-ui/permissions": 48, "@object-ui/plugin-detail": 19, "@object-ui/plugin-chatbot": 16, "@object-ui/plugin-designer": 10, "@object-ui/plugin-list": 10, "@object-ui/fields": 10, "@object-ui/app-shell": 24, "@object-ui/plugin-view": 10, "@object-ui/core": 2, "sum": 666}, "pin": { "shape": "A hand-written ledger literal `SWEPT_MEMBERS` in scripts/__tests__/check-vi-mock-inherit.test.ts, pinned in BOTH directions by two new cases: 'NOTHING LEAVES the covered set — every swept specifier is still a member' (every ledger entry must still be in COVERED_SPECIFIERS) and 'NOTHING JOINS unrecorded — a new member is pinned in the PR that adds it' (every member must be in the ledger).", "why_a_literal": "Triage's constraint and the brief's: a pin derived from COVERED_SPECIFIERS asserts x === x and cannot fail. The cautionary example was already in the file — the verdict-line case builds `COVERED_SPECIFIERS.join(', ')`, so it adapts to a removal instead of resisting it.", "why_bidirectional": "The second direction is the answer to 'a hand-listed pin will drift'. Without it the ledger is a snapshot that decays and every specifier added after today inherits exactly the unprotected state the 18 were measured in. With it, the sweep PR that widens the constant must widen the ledger in the same PR — which is the same PR the gate's own 'precondition for widening' already requires — so a member added later arrives already pinned. Proven able to fail, see mutation.addition_leg.", "what_it_still_permits": [ "An author who deletes a member from the constant AND its ledger line in one edit passes. That is the floor of any literal pin and it is the intent: a member cannot leave SILENTLY, not that it can never leave — retirement becomes a visible, reviewable edit to a list whose docblock says the set only grows.", "The ledger carries names only. Per-entry provenance stays in the gate header (slice, measurement, PR); a second copy here would be the unchecked hand-copy this case exists to catch.", "It says nothing about whether a member's sites are actually swept — that stays the gate's own job, and the pin does not substitute for it." ], "nothing_weakened": "length > 0, the real-package loop and toContain(I18N) are untouched; no threshold moved, no assertion deleted, nothing skipped or disabled. scripts/check-vi-mock-inherit.mjs itself is unchanged — the change is test-only." }, "mutation_test": { "method": "Mutate the GATE's COVERED_SPECIFIERS (never the test). On-disk proof per leg = anchored `grep -c \"^ '<member>',$\"` before/after PLUS `git hash-object` before/after; a count that does not move, an unchanged blob, or an empty hash aborts the leg as a VOID READING rather than being re-run quietly. Restore = `git checkout HEAD -- scripts/check-vi-mock-inherit.mjs` from a `trap ... EXIT INT TERM`, proved by comparing `git hash-object` to `git rev-parse HEAD:<path>` — never by an exit code. Every one of the 43 legs printed `restore-proof blob=08a986a0af5f00ccac4e8a2c4efeaa6b2ec5e85f == HEAD:scripts/check-vi-mock-inherit.mjs`.", "post_change_removal_legs": "21/21 red. For the 18 previously-unprotected members: `Tests 1 failed | 87 passed (88)`, the single failure being 'NOTHING LEAVES the covered set — every swept specifier is still a member' — exactly the new pin, the rest of the suite passing. @object-ui/i18n: 2 failed | 86 passed (new pin + its own toContain(I18N)). @object-ui/react: 12 failed | 76 passed (new pin + the 11 incidental cases).", "control_leg_pre_change": "Same removals against the UNMODIFIED test file, all 21 members. 18 legs: `Tests 86 passed (86)`, vitest exit 0 — the defect reproduced, and those 18 are exactly the legs that turned red above. @object-ui/i18n: 1 failed (its own pin). @object-ui/components: 1 failed ('THE SUBPATH SITES', incidental). @object-ui/react: 11 failed (incidental, see precondition 2).", "addition_leg": "The other direction, proving it can fail: added '@object-ui/layout' (a real non-member workspace package) to the gate's constant without recording it in the ledger → `Tests 1 failed | 87 passed (88)`, failure = 'NOTHING JOINS unrecorded — a new member is pinned in the PR that adds it'. On-disk proof: anchored count 0 -> 1, blob 08a986a0 -> 6527a9c1; restored to 08a986a0 by hash." }, "files_touched": ["scripts/__tests__/check-vi-mock-inherit.test.ts (+93)", ".changeset/8018-covered-specifiers-membership-pin.md (+9, empty frontmatter — test-only, nothing released)"], "checks": [ "pnpm exec vitest run scripts/__tests__/check-vi-mock-inherit.test.ts → Test Files 1 passed (1), Tests 88 passed (88) [86 before]; run under the shared verify lock, VERDICT command-exit 0", "node scripts/check-vi-mock-inherit.mjs → exit 0, 'check-vi-mock-inherit: OK', 4546 tracked source files, 666 judged (666 inherit), 0 other workspace, 0 frozen", "pnpm run type-check:scripts (tsc -p tsconfig.scripts.json — the program that types this file) → exit 0", "eslint over the FULL lint:root population (not a narrowed subset): 286 files, 0 errors, 32 pre-existing warnings, exit 0; the changed file alone re-linted: 1 file, 0 errors, 0 warnings", "node scripts/check-changeset-presence.mjs → exit 0, '1 changeset(s) added'", "node scripts/check-governed-queue-guard.mjs --test <the 2 paths> → 'NOT GOVERNED — 2 path(s) checked against 5 governed surface(s); none matched'", "control-character self-scan: grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]' on the changed test file → exit 1 (no matches)", "Exit codes captured before any pipe throughout (cmd > file 2>&1; EXIT=$?), and gate results read from the tool's own verdict line", "NOT run, declared for CI: package-level turbo lint / type-check / test — no package source is touched (one file under scripts/__tests__/ plus a changeset)" ], "ci_status": "in_progress — reported at draft-PR time, per dispatch contract; CI convergence is the PM's read, not mine", "labels_touched": "none (no pm:* label written, assignee never written)", "mcp_calls": "0 — issue body, comments, PR creation and PR read-back all went over repo-scoped REST after a successful probe; no MCP GitHub call was made", "open_questions": [], "out_of_scope_findings": [], "stopped_on": [] }
Generated by Claude Code
LANDED — objectui#8018 via PR objectui#8630, merged
1ebaf7217merged_at 2026-09-08T18:55:32Z;removed_from_merge_queue@18:55:31Zwithmerged: true⇒ that removal is the merge, ⛔ not a dequeue.⚠️ First, this seat's own probe was wrong, and the wrong reading is published rather than droppedThe landing probe's ledger count came back 0 against a firing control of 21 on the gate side. ⛔ That was not a finding — it was this seat's bad anchor: the probe assumed a two-space indent, and
SWEPT_MEMBERSlives inside adescribe, indented four.⭐ The errata this seat maintains already names that trap — "an anchored pattern cannot answer a question about an indented region" — and it walked into it anyway. ⇒ ⛔ a zero from a pattern whose shape has not been controlled is not a reading, and the correct response was to look at the text before reporting a defect, which is what happened.
Landing probe, re-run correctly on a re-fetched
origin/main(tip1ebaf7217)check reading the pin's parts SWEPT_MEMBERSpresent;NOTHING LEAVES the covered set …× 1;NOTHING JOINS unrecorded …× 1ledger entries 21 ( :523-543)⭐ firing control — the gate's own COVERED_SPECIFIERS21 ⭐ and the sets are EQUAL, ⛔ not merely equinumerous in gate not ledger: []·in ledger not gate: []in-class control @object-ui/reactfound in bothnegative control @object-ui/no-such-8018found in neither — ⇒ the membership test can say nononsense control exit 1 ⚠️ Counting only the ledger side would have passed while the two silently diverged — this card's own defect in miniature, which is why the gate side is the firing control rather than an afterthought.What is now true that was not
The gate's header declares
COVERED_SPECIFIERSGROW-ONLY, and its whole force rests on that:findCallSitesmarks a sitecoveredonly on membership, so removing a member reddens nothing — it makes that specifier's population invisible and the gate reports OK over a tree it no longer looks at. ⇒ from1ebaf7217a removal failsNOTHING LEAVES, and an addition that skips the ledger failsNOTHING JOINS unrecorded— so a member added later arrives already pinned, by the same PR that sweeps it.⭐ And the card understated its own defect. Measured this round: the genuinely unprotected population was 18 of 21 members, ⛔ not one — with the control leg proving it (removing any of those 18 against the unmodified test file left
86 passed (86), exit 0).⚠️ @object-ui/reactdid redden 11 cases, but by coincidence: the fixture constant at:61happens to name a real member, so those cases fell out ofcovered— protection that depends on a fixture spelling and on which files exist, whose failure text names no ratchet. ⇒ the card's defect was real and larger than filed; its chosen exhibit was not.⛔
FLOORS.covered = 50never backstopped it: the judged total is 666 and the largest single member is 136, so no single removal can push the total under the floor.The limit this keeps, stated rather than papered over
An author who deletes a member from the constant and its ledger line in one edit still passes. ⭐ That is the floor of any literal pin, and it is the intent: a member cannot leave silently — ⛔ not that it can never leave.
Generated by Claude Code
Surfaced while implementing objectui#6892 slice 1 (
@object-ui/plugin-markdown/@object-ui/data-objectstack/@object-ui/plugin-report). Filed unassigned, deliberately not fixed there — that slice's file surface is the three converted test files plus the three lines in the constant, and generalising this pin is a decision about the ratchet's own pin rather than part of the sweep.What was measured
scripts/check-vi-mock-inherit.mjs's header states the covered set is GROW-ONLY, and the ratchet's entire force rests on that:findCallSitesclassifies a call site ascoveredonly when its specifier is inCOVERED_SPECIFIERS, and everything else isworkspace/unjudged. So removing a member does not redden anything — it makes that specifier's whole population invisible and the gate reports OK over a tree it no longer looks at. That is the one direction a ratchet must never be free to move, and the guard's own test says so in as many words atscripts/__tests__/check-vi-mock-inherit.test.ts:That reasoning is correct and is enforced for exactly one member. Every membership assertion in the test file, read on
eeda78a780:expect(COVERED_SPECIFIERS.length).toBeGreaterThan(0)expect(COVERED_SPECIFIERS).toContain(I18N)@object-ui/i18ncannot be droppedconst named = COVERED_SPECIFIERS.join(', ')in the verdict-line case⇒
@object-ui/react— the founding member, 134 judged call sites — has no membership pin at all. Deleting it from the list leavescheck:vi-mock-inheritgreen, the guard's own suite green (length > 0still holds, i18n still present), and 134 sites unwatched. The three specifiers objectui#6892 slice 1 adds inherit the same gap.Why it is not caught elsewhere
FLOORS.covered = 50is the nearest thing to a backstop, and it does not reach: with@object-ui/reactremoved the covered count is still 95 (i18n's 92 plus the three 1-site specifiers), comfortably above the floor. It only catches a collapse to near-nothing, which is the case it was written for.git grep -l COVERED_SPECIFIERSreturns the gate, its test, one changeset and one workflow comment.Suggested shape
Replace the single-member
toContain(I18N)with a pin over the swept set — every specifier a landed sweep has added, listed once, asserted present — so the assertion grows with the ratchet instead of naming one member. That keeps the existing case's rationale (quoted above) and makes it hold for all of them. The list is short and each entry is justified by a merged sweep PR, so it is a ledger of landed work rather than an exemption table.Not urgent: no member has ever been removed, and the gate is green today. It is filed because the protection currently reads as general and is not.