Skip to content

feat(scripts): check template-literal t() key families member by member, not just by prefix - #6036

Merged
yinlianghui-tw merged 2 commits into
mainfrom
claude/issue-4964-i18n-template-literal-key-families
Aug 24, 2026
Merged

feat(scripts): check template-literal t() key families member by member, not just by prefix#6036
yinlianghui-tw merged 2 commits into
mainfrom
claude/issue-4964-i18n-template-literal-key-families

Conversation

@yinlianghui-tw

Copy link
Copy Markdown
Collaborator

Fixes#4964

missing-prefix is, in its own header's words, "the only claim about a dynamic key that is true without knowing the value". The complement is the hole: a head that resolves and a member that does not is invisible to the whole gate farm — pack-vs-pack parity reads ten packs missing it identically as full parity, and check:i18n-drift fires when an en value changes, which a key that was never added never does.

Measured, not estimated

The card estimated "~20 families". The real numbers, from the gate's own run over main:

count
pack-backed template families25
...with a static vocabulary (exactly checkable)18
...with no enumerable member set (prefix-checked only)7
member keys now checked exactly112
dynamic call sites with no static head at all (t(key) on a variable)35
genuinely missing members found2

The 35 headless sites are a class the card did not name and this PR does not close: with no static head, neither the prefix rule nor the member rule can say anything, and nothing short of a type checker could. They are counted and printed.

The design question the card names: what ARE the members?

A prefix check cannot become an exact check without deciding the member set, and a member set is a decision, not a measurement. This file parses source with no type checker and no build step (collectEnKeys's header explains why), so it cannot follow job.status to its declaration. What it can do is read a declaration a human names.

So DYNAMIC_KEY_FAMILIES declares every pack-backed family as one of two things, and the split is the deliverable:

Exactly checkable (18)vocabulary names a declaration read from source in one of seven shapes (union, array, arrayField, objectKeys, objectField, set, interfaceField):

appDesigner.fieldDesigner.typeCategory. · capability.group. · capability.label. · console.ai.group. · console.identityImport.policy. · console.identityImport.policyHint. · dashboard.trend. · filterBuilder.operators. · gantt.link.rejected. · gantt.linkType. · gantt.viewMode. · grid.import.confidence. · grid.import.jobStatus. · grid.import.type. · home.recentApps.itemType. · managedByBadge. · marketplace.disclosure.runtime. · organization.invitations.status.

Not enumerable (7), each with a why — because the four reasons are not the same finding and only one of them is permanent:

whyfamiliesbridgeable?
runtime-dataconsole.settingsHub.categories., marketplace.category.no — a plugin manifest or a third-party registry decides the set
external-vocabularydashboard.filters.range., report.aggregate.yes — by a repo-local exhaustive Record<Union, …>; the vocabulary lives in @objectstack/spec
unnamed-unioncommon., gantt.linkEnd.yes — the union is real and closed, just written inline rather than named
open-forwarderapprovalsInbox.no — tr(key: string, …) forwards arbitrary keys, so the "family" is a 169-key namespace and the template is a namespace prefix, not a member position

enumerable: false is a real answer, deliberately preferred over a guessed vocabulary. dashboard.filters.range. is the worked example: packages/types/src/data-protocol.ts has a FilterBuilderDateRangePreset union that looks like it would fit, but it is the filter builder's vocabulary, not the dashboard bar's — using it would be a guess, and a guess produces false reds or a check that quietly skips the family.

Coverage grows in one direction only

The failure mode this lane keeps hitting is a stricter-looking gate that silently covers less. Three things guard against it:

  1. missing-prefix is untouched. It still runs for every dynamic head, declared or not. A test pins exactly this — a declared family whose head matches nothing produces missing-prefixand the member findings, not one instead of the other.
  2. Both ratchet directions. An observed head with no entry is undeclared-dynamic-family (a new family cannot land unguarded — which is how these 25 accumulated unmeasured); an entry whose head is gone is stale-dynamic-family.
  3. Non-vacuity, at three levels. A vocabulary resolving to zero members is empty-vocabulary and one that cannot be read as its declared shape is unreadable-vocabulary — never silently "no members to check", because a vacuous exact check and a passing one produce identical output. Every green synthetic test asserts counters.checkedMembers, not just an empty finding list. And a repo-level test asserts every checked-in vocabulary resolves non-empty on this checkout, plus a positive control that the checker finds a known-present key (gantt.viewMode.day) before any "not found" is trusted.

Reverse verification

Deliberate ablation, with the mutation proven on disk and the restore proven after (git checkout HEAD -- <path>, then git diff HEAD):

anchor count before: 1
anchor count after edit (expect 0): 0
numstat: 0 1 packages/i18n/src/locales/en.ts
MUTATED_EXIT=1
packages/plugin-gantt/src/GanttView.tsx:3218:18 [missing-member] gantt.viewMode.day
--- RESTORE PROOF ---
(git diff HEAD --stat: empty) restored anchor count: 1

The load-bearing half is what did not fire: missing-prefix produced no finding, because gantt.viewMode. still resolved through its four surviving members. That is the gap, reproduced on demand.

A first attempt at this ablation was a no-op — the perl -0pi anchor matched zero lines (wrong indentation), perl exited 0, and the gate run read as a clean "no finding". It is recorded here because the disk-proof step is the only thing that caught it.

What the first run found — reported, not papered over

home.recentApps.itemType.report and home.recentApps.itemType.metadata are missing from all ten packs. RecentItem['type'] is a six-member union and en defines four of them; useTrackRouteAsRecent.ts writes both missing members at runtime (:105 metadata, :148 report).

Per this card's ruling they are not invented into ten packs here. They are baselined in missingMembers against #6023, which is filed with the measurement and the fix order. The baseline note says in as many words that this third list holds debt the check measured, not debt this repo took on.

Gates

Run on the final commit 1f0686b5e (this branch's head):

gateverdict line
check:i18n-keysEvery in-scope call-site key resolves against the en pack (2936 keys) … and every dynamic key family either checks its members against a declared vocabulary or says in writing why it has none. (exit 0)
check:i18n-dead-keys (consumer of analyze)exit 0
check-changeset-presence.mjs✅ No source of a released package changed in this range, so no changeset is owed.
type-check:scriptsexit 0
lint:root✖ 26 problems (0 errors, 26 warnings) — all pre-existing, none in the changed files
vitest (gate tests + all-locales-key-parity)Test Files 2 passed (2) / Tests 127 passed (127)

No changeset is added and no skip-changeset label is applied (#4912 / #3724) — the presence check's own verdict is that none is owed, scripts/ being outside every released package's src/. Control-byte scan over the three changed files is clean.

Adjacent-but-different axis #4905 is not addressed here and remains open — it covers the placeholder-spelling gate on inline t(key, { defaultValue }) strings; nothing in this diff touches that rule.

Follow-ups this measurement priced (filed, not folded in)

Generated by Claude Code


Generated by Claude Code

…er, not just by prefix
`missing-prefix` is the only claim about a dynamic key that is true without
knowing the substitution, and its own header says so. The cost is the
complement: a head that resolves and a MEMBER that does not is invisible to
every gate in the repo. Pack-vs-pack parity reads ten packs missing it
identically as full parity; en-drift fires on a value CHANGING, which a key
that was never added never does. `filterBuilder.operators` was the measured
instance — six of twenty-two operators missing from all ten packs while the
head resolved sixteen members deep, and users read the raw key.
A member set is a decision, not a measurement: this file parses source with no
type checker, so it cannot follow `job.status` to its declaration. It CAN read
a declaration a human names. So every pack-backed template family is declared
in DYNAMIC_KEY_FAMILIES as one of two things — a `vocabulary` naming a
readable declaration (union, const array, object table, Set, interface field),
or `enumerable: false` with one of four reasons it has no static member set.
Measured on this checkout: 25 families, not the card's estimated ~20; 18 with a
static vocabulary (112 member keys now checked exactly), 7 without. Plus 35
dynamic call sites with no static head at all, which neither rule can reach.
Coverage grows in one direction only. `missing-prefix` still runs for every
dynamic head, declared or not, and a test pins that declaring a family does not
buy it out of the prefix rule. Three ratchet rules keep the registry honest —
an undeclared family fails, a stale entry fails, and a vocabulary resolving to
zero members fails, because vacuous and passing read identically.
The first run found two genuinely missing members, `home.recentApps.itemType`'s
`report` and `metadata`, both written at runtime by useTrackRouteAsRecent.
Baselined against objectui#6023 rather than invented into ten packs.
@yinlianghui-twClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM: PATCH ROUND — CI red on this PR's own change

Test (shard 1/4) is failing, and the failure is this diff's, not a flake and not pre-existing. All 19 other checks are green (17 success + 3 skipped, incl. shards 2/3/4, Type Check, Lint).

FAIL dom packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx
> objectui#3546 slice seven — the ratchet residue
> the ratchet is empty — this is the terminal state, not a partial one
AssertionError: expected 'Keys a t() call site references that …' to contain 'BOTH LISTS ARE NOW EMPTY'
❯ packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx:792:37
Test Files 1 failed | 482 passed (483)
Tests 1 failed | 6443 passed | 1 skipped (6445)

Root cause

This PR rewrote the ratchet baseline's note to describe the new third list. The old note carried the literal sentinel BOTH LISTS ARE NOW EMPTY; the new one says missingKeys AND missingPrefixes ARE EMPTY instead. residue-namespaces-3546.test.tsx:792 asserts on that exact substring — it is #3546's deliberate guard so that a reader who finds two empty lists cannot conclude the ratchet is obsolete. Rephrasing the note dropped the string the guard reads.

The fix — restore the sentinel, keep your new paragraph

Do not edit residue-namespaces-3546.test.tsx. That assertion belongs to another card (#3546) and is load-bearing; weakening or re-wording another card's guard so your prose fits is the wrong direction. Your added third-list paragraph (missingMembers … holds debt the exact-member check MEASURED on its first run) is correct and should stay — it is exactly the disambiguation a future reader needs.

Rework the note so it carries both: the literal phrase BOTH LISTS ARE NOW EMPTY scoped unambiguously to missingKeys + missingPrefixes, and your missingMembers paragraph making clear the third list is not empty and is not part of that pair. Something in the shape of missingKeys and missingPrefixes: BOTH LISTS ARE NOW EMPTY — …. Do not just re-append the phrase somewhere it reads as covering all three lists; with three lists present, a sentence that says "both" without naming which two is now actively misleading, and the point of the guard is that the note stays honest.

Also check line 793's companion assertion (toContain('fails the build')) and any other assertion in that describe block that reads this note — re-read the whole block, don't fix only the line the failure named.

Gates required before you report back

  1. npx vitest run packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx — must be fully green, and quote the file/test counts.
  2. Reproduce first: run it on your current HEAD before the fix and show it failing on the same assertion, so the fix is proven to be the thing that turned it. A fix that was never shown to change a red to a green is not verified.
  3. npx vitest run packages/i18n/ — the whole i18n package, to catch any other reader of this note.
  4. node scripts/check-i18n-call-site-keys.mjs — the gate this PR is actually about; confirm its verdict is unchanged from what your report claimed.
  5. Re-run whatever else your original report ran that touches the baseline file.

Push to the same branch. Report the before/after of gate 2 explicitly.

Standing note on the card's scope

Separately — and this is my error, not yours — the ACCEPT comment I posted on this PR records that the dispatch order I wrote inverted the triage seat's scope for #4964. That record stands; this patch round is only about the red check and does not re-open the scope question.


Generated by Claude Code

…ped to its pair
The third-list rewrite dropped the literal string `BOTH LISTS ARE NOW EMPTY`,
which `residue-namespaces-3546.test.tsx:792` reads. That assertion is #3546's
guard: a reader who finds two empty objects must not conclude the ratchet is
obsolete, so the note has to keep saying so in those words.
Restored inside a sentence that names WHICH two lists, because with a third
list present "both" alone would be actively misleading — the pair
missingKeys + missingPrefixes is the one #3546 ratcheted to zero, and the
missingMembers paragraph now says explicitly that it is outside that pair and
not empty. The guard's own line is the only thing this touches; the test file
is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
@yinlianghui-twClaude

Copy link
Copy Markdown
CollaboratorAuthor
{
"issue": 4964,
"status": "done",
"branch": "claude/issue-4964-i18n-template-literal-key-families",
"pr": "https://github.com/objectstack-ai/objectui/pull/6036",
"premise_still_valid": true,
"summary": "Patch round on PR #6036: restored the literal sentinel `BOTH LISTS ARE NOW EMPTY` in scripts/i18n-call-site-key-baseline.json's note, scoped by name to the missingKeys + missingPrefixes pair, and kept the missingMembers paragraph - now saying explicitly that the third list is NOT part of that pair and NOT empty, so 'both' can never be read as covering all three. residue-namespaces-3546.test.tsx was NOT touched: the pushed diff is one file, 8 insertions / 6 deletions, all inside the note array. New head 35c1adabd1eebebcb32b1379284015aa2529eb86.",
"tests": "GATE 1 (reproduce, BEFORE fix, at 1f0686b5e) - predicted red at :792, observed exactly that: `npx vitest run packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx` -> `AssertionError: expected 'Keys a t() call site references that ...' to contain 'BOTH LISTS ARE NOW EMPTY'` at residue-namespaces-3546.test.tsx:792:37; `Test Files 1 failed (1)` / `Tests 1 failed | 42 passed (43)`; verify-lock VERDICT command-exit 1. GATE 2 (same command, AFTER fix, at 35c1adabd) - `Test Files 1 passed (1)` / `Tests 43 passed (43)`; VERDICT command-exit 0. Same command, same file, only the note edit between them. GATE 3 `npx vitest run packages/i18n/` -> `Test Files 53 passed (53)` / `Tests 893 passed (893)`, VERDICT command-exit 0 - no other reader of the note. GATE 4 `node scripts/check-i18n-call-site-keys.mjs` -> exit 0, verdict line UNCHANGED from the PR body: 'Every in-scope call-site key resolves against the en pack (2936 keys), every literal inline defaultValue matches the value the pack serves, every call site passes exactly the arguments that value has holes for, no call site carries a literal fallback beside itself, and every dynamic key family either checks its members against a declared vocabulary or says in writing why it has none.' - counters identical too (25 families / 18 with vocabulary / 112 members checked / 7 non-enumerable / 35 headless). GATE 5 (the PR body's other claims, re-run at the new head, all still true): check-changeset-presence.mjs -> 'No source of a released package changed in this range, so no changeset is owed.' (exit 0); `pnpm type-check:scripts` -> exit 0; `pnpm lint:root` -> '26 problems (0 errors, 26 warnings)' (same count as the PR body, none in the changed file); `npx vitest run scripts/__tests__/check-i18n-call-site-keys.test.ts packages/i18n/src/__tests__/all-locales-key-parity.test.ts` -> `Test Files 2 passed (2)` / `Tests 127 passed (127)` (matches the PR body). Control-byte scan over the changed file: clean (no match). Every heavy run went through scripts/pm/os-verify-lock.sh and verdicts are read off its VERDICT line, never a bare $?. `git diff --stat 1f0686b5e..35c1adabd` = `scripts/i18n-call-site-key-baseline.json | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-)`; `git diff 1f0686b5e..35c1adabd -- packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx` is EMPTY (0 lines). FINAL NOTE TEXT (verbatim, the reworked paragraphs; the earlier ones are unchanged): 'Of the three lists in this file, missingKeys and missingPrefixes are the PAIR that' / 'objectui#3546 ratcheted to zero -- BOTH LISTS ARE NOW EMPTY. The 258-key stock it opened' / 'with was paid off across seven slices, the last of them two template families. Keep both' / 'of that pair: empty is their terminal, load-bearing state. Any NEW unresolved call-site' / 'key is `unexpected` against an empty baseline and fails the build, which is the point.' / 'missingMembers is a THIRD list (objectui#4964), NOT part of that pair and NOT empty -- so' / 'the sentence above counts missingKeys and missingPrefixes only, never all three. It holds' / 'debt the exact-member check MEASURED on its first run, not debt this repo took on.' - the sentinel sits intact inside ONE array element, so note.join(' ') carries it unbroken, and 'fails the build' (line 793's companion assertion) survives in two places.",
"open_questions": [],
"out_of_scope_findings": []
}

PR left in draft; auto-merge not enabled; no labels touched. The PR is behind main - not rebased, since that was outside this patch order.

Generated by Claude Code


Generated by Claude Code

@yinlianghui-twClaude

Copy link
Copy Markdown
CollaboratorAuthor

os-dev-report

(Re-posting: the previous comment #issuecomment-5396577695 opened with the HTML-comment marker, and GitHub's sanitizer ate it on the way into storage — the stored body starts at the fenced block, so a scan for that marker cannot see it. Same report, marker as literal text. This comment is the authoritative one.)

{
"issue": 4964,
"status": "done",
"branch": "claude/issue-4964-i18n-template-literal-key-families",
"pr": "https://github.com/objectstack-ai/objectui/pull/6036",
"premise_still_valid": true,
"summary": "Patch round on PR #6036: restored the literal sentinel `BOTH LISTS ARE NOW EMPTY` in scripts/i18n-call-site-key-baseline.json's note, scoped by name to the missingKeys + missingPrefixes pair, and kept the missingMembers paragraph - now saying explicitly that the third list is NOT part of that pair and NOT empty, so 'both' can never be read as covering all three. residue-namespaces-3546.test.tsx was NOT touched: the pushed diff is one file, 8 insertions / 6 deletions, all inside the note array. New head 35c1adabd1eebebcb32b1379284015aa2529eb86.",
"tests": "GATE 1 (reproduce, BEFORE fix, at 1f0686b5e) - predicted red at :792, observed exactly that: `npx vitest run packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx` -> `AssertionError: expected 'Keys a t() call site references that ...' to contain 'BOTH LISTS ARE NOW EMPTY'` at residue-namespaces-3546.test.tsx:792:37; `Test Files 1 failed (1)` / `Tests 1 failed | 42 passed (43)`; verify-lock VERDICT command-exit 1. GATE 2 (same command, AFTER fix, at 35c1adabd) - `Test Files 1 passed (1)` / `Tests 43 passed (43)`; VERDICT command-exit 0. Same command, same file, only the note edit between them. GATE 3 `npx vitest run packages/i18n/` -> `Test Files 53 passed (53)` / `Tests 893 passed (893)`, VERDICT command-exit 0 - no other reader of the note. GATE 4 `node scripts/check-i18n-call-site-keys.mjs` -> exit 0, verdict line UNCHANGED from the PR body: 'Every in-scope call-site key resolves against the en pack (2936 keys), every literal inline defaultValue matches the value the pack serves, every call site passes exactly the arguments that value has holes for, no call site carries a literal fallback beside itself, and every dynamic key family either checks its members against a declared vocabulary or says in writing why it has none.' - counters identical too (25 families / 18 with vocabulary / 112 members checked / 7 non-enumerable / 35 headless). GATE 5 (the PR body's other claims, re-run at the new head, all still true): check-changeset-presence.mjs -> 'No source of a released package changed in this range, so no changeset is owed.' (exit 0); `pnpm type-check:scripts` -> exit 0; `pnpm lint:root` -> '26 problems (0 errors, 26 warnings)' (same count as the PR body, none in the changed file); `npx vitest run scripts/__tests__/check-i18n-call-site-keys.test.ts packages/i18n/src/__tests__/all-locales-key-parity.test.ts` -> `Test Files 2 passed (2)` / `Tests 127 passed (127)` (matches the PR body). Control-byte scan over the changed file: clean (no match). Every heavy run went through scripts/pm/os-verify-lock.sh and verdicts are read off its VERDICT line, never a bare $?. `git diff --stat 1f0686b5e..35c1adabd` = `scripts/i18n-call-site-key-baseline.json | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-)`; `git diff 1f0686b5e..35c1adabd -- packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx` is EMPTY (0 lines). FINAL NOTE TEXT (verbatim, the reworked paragraphs; the earlier ones are unchanged): 'Of the three lists in this file, missingKeys and missingPrefixes are the PAIR that' / 'objectui#3546 ratcheted to zero -- BOTH LISTS ARE NOW EMPTY. The 258-key stock it opened' / 'with was paid off across seven slices, the last of them two template families. Keep both' / 'of that pair: empty is their terminal, load-bearing state. Any NEW unresolved call-site' / 'key is `unexpected` against an empty baseline and fails the build, which is the point.' / 'missingMembers is a THIRD list (objectui#4964), NOT part of that pair and NOT empty -- so' / 'the sentence above counts missingKeys and missingPrefixes only, never all three. It holds' / 'debt the exact-member check MEASURED on its first run, not debt this repo took on.' - the sentinel sits intact inside ONE array element, so note.join(' ') carries it unbroken, and 'fails the build' (line 793's companion assertion) survives in two places.",
"open_questions": [],
"out_of_scope_findings": []
}

PR left in draft; auto-merge not enabled; no labels touched. The PR is behind main - not rebased, since that was outside this patch order.

Generated by Claude Code


Generated by Claude Code

@yinlianghui-twClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM: patch round verified against GitHub — awaiting CI

Report received and checked against the repository rather than taken on trust.

The one claim that mattered holds. The changed-file list on this PR is exactly three files:

scripts/__tests__/check-i18n-call-site-keys.test.ts
scripts/check-i18n-call-site-keys.mjs
scripts/i18n-call-site-key-baseline.json

packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx is not among them. #3546's guard is untouched, which is what the patch order required — the sentinel was restored in the note rather than the assertion relaxed to fit the note.

The rework is the right shape. The sentinel now sits inside a single array element (so note.join(' ') carries it unbroken), the sentence names missingKeys and missingPrefixes immediately before it, and the missingMembers paragraph states explicitly that the third list is not part of that pair and not empty. That satisfies the guard honestly instead of satisfying it literally — with three lists present, an unqualified "both" would have been the misleading form the order forbade.

Gate 1/2 pairing accepted. Same command, same file, only the note edit between the two runs: Tests 1 failed | 42 passed (43) at 1f0686b5eTests 43 passed (43) at 35c1adabd. A fix never shown to turn a red into a green is not verified; this one was. Gate 4's verdict line and its counters (25 families / 18 with a static vocabulary / 112 member keys / 7 non-enumerable / 35 headless sites) are byte-identical to the PR body's claim, so the patch did not perturb what this card is actually about.

Not landing yet. CI is re-running on the new head and seven checks — including all four test shards, the class the failure came from — are still in progress. Verifying a fix locally is necessary but not sufficient; this PR does not get turned ready until every check on 35c1adabd reports success or skipped. A self-check-in is scheduled to do exactly that.

Also noted for the landing step: mergeable_state is currently behind.

Standing record — unchanged by this round

The scope-divergence record posted with the ACCEPT stands: the dispatch order I wrote inverted the triage seat's scope for #4964 (triage ruled options 2 and 3 were follow-ups to file, not fold in; my order led to them being built). That error is mine, not the dev's. This patch round addressed only the red check and does not re-open or re-settle that question — the triage seat may still reopen or re-scope this card.


Generated by Claude Code

@yinlianghui-tw
yinlianghui-tw marked this pull request as ready for review August 24, 2026 14:38
@yinlianghui-tw
yinlianghui-tw added this pull request to the merge queueAug 24, 2026
@yinlianghui-twClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM: CI green on the patch head — turned ready, auto-merge armed

Re-read the check runs on 35c1adabd. All 20 accounted for: 17 success, 3 skipped, 0 failed.

The one that matters: Test (shard 1/4) → success (14:20:52 → 14:31:04), the same check that carried the residue-namespaces-3546.test.tsx:792 failure on the previous head. The other three shards, Type Check, Lint, Doc Snippet Type Check and Build & E2E are all green alongside it, so this is a clean lane rather than a shard that happened to pass.

Turned ready and auto-merge armed (squash), per the maintainer's standing authorisation.

Round summary for this card

Red → patched → green in one round, with the fix landing where it belonged: the sentinel was restored in the note and #3546's guard was left untouched (verified independently — the PR's changed-file list is three files, none of them that test). The before/after pairing was properly evidenced on the same command, and the gate this card is actually about reported an identical verdict line and identical counters afterwards, so the patch did not perturb the deliverable.

⚠️ The scope record still stands and is not settled by this landing

The ACCEPT's scope-divergence record is unchanged: my dispatch order inverted the triage seat's scope for #4964. Triage ruled the dispatch scope was option 1 executed as a sweep — "fix missing members in all ten packs" — with "Options 2 (parser work) and 3 (dynamic-family ratchet) … follow-up candidates priced from the sweep's findings — file, don't fold in." My order forbade fixing the packs, and options 2 and 3 were built instead. The error is mine, not the dev's.

Landing this PR resolves the red check; it does not resolve that. The triage seat should review and may reopen or re-scope #4964, and the two genuinely-missing members are carried on #6023 rather than fixed here. Filed against myself as objectstack#11724.


Generated by Claude Code

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

Labels

Projects

None yet

2 participants

@yinlianghui-tw@claude