Skip to content

check-i18n-bundles resolves its population CWD-relatively and dies with an uncaught ENOENT stack from any cwd but the repo root #11647

Closed

Description

@os-steve

Measured while fixing #11394. No gate is red — CI invokes this gate from the repo root. Filed unassigned, no pm:queue.

The shape

#10907 anchored check-i18n-coverage.mjs to a root derived from import.meta.url, behind one at() seam. Its sibling scripts/check-i18n-bundles.mjs was never given the same treatment: it contains no import.meta.url, no REPO_ROOT, and enumerates its population with two bare relative literals:

const configs = findExtractConfigs('packages', 'packages')

findExtractConfigs (scripts/i18n-bundle-surface.mjs:128) readdirSyncs that literal, so from any cwd but the repo root the gate does not merely mis-scan — it dies with an uncaught exception and a node stack:

$ cd /tmp && node .../scripts/check-i18n-bundles.mjs
node:fs:1590
Error: ENOENT: no such file or directory, scandir 'packages'
    at readdirSync (node:fs:1590:26)
    at findExtractConfigs (file:///.../scripts/i18n-bundle-surface.mjs:128:19)
    at file:///.../scripts/check-i18n-bundles.mjs:748:17

Pre-existing, and not introduced by #11394

Verified on both trees. At origin/main 3637731e2, off-root, the same stack appears — there preceded by the (now removed) probe-deferral line:

check-i18n-bundles: could not read packages/cli/package.json (ENOENT ...) — build prerequisite not pre-checked
Error: ENOENT: no such file or directory, scandir 'packages'

#11394 anchored the shared build probe and this gate's existence check on its answer; it deliberately did not widen into the population, which is this card.

Why it is worth recording

Two reasons, and the second is the load-bearing one.

  1. It is the last unanchored half of a pair whose other half is already fixed. check-i18n-coverage and check-i18n-bundles are described throughout their own headers as siblings running one step apart in lint.yml; one is cwd-independent and the other cannot start.

  2. The failure mode is the one this gate family has repeatedly been fixed for. check-i18n-bundles.mjs carries an elaborate prerequisite apparatus (check-i18n-bundles 在工作区未构建时把「CLI 没 build」报成 9 个包各自的 bundle 问题 #5217, check-i18n-bundles reports a stale-dist prerequisite failure as a content verdict ("1 bundle problem(s)") #7681, and its reportPrerequisiteNotMet printer, which states in words that nothing was checked) precisely so an environment fact is never reported as a content verdict or as a bare stack. An uncaught scandir 'packages' bypasses all of it — it is exactly the shape check-i18n-coverage:example 自身依赖未构建时仍抛裸异常 + node 栈(诊断准确,但不是结论) #6033 removed from the coverage gate's per-config loop, still live one file over, at the population level.

Note the direction is not #10907's silent-green: an uncaught throw is loud. The cost is a wrong first diagnosis (a node stack naming node:fs), not a false pass.

Suggested fix

The same shape both siblings now use: derive a root from import.meta.url, keep the repo-relative spellings as vocabulary, and put one at()/atRepoRoot seam between them. scripts/cli-build-prerequisite.mjs already exports atRepoRoot after #11394, and this gate already imports it — so the population change is the remaining half. #10907's anti-#4690 note applies too: an empty population must be refused, not reported OK.

Refs

#11394 (where this was measured; that card anchored the shared probe, not this gate's population)
#10907 (the same fix applied to the sibling gate)

Activity

  1. os-steve commented on Aug 24, 2026

    @os-steve
    CollaboratorAuthor

    Triage — domain:devx, tooling, pm:blocked.

    Blocked-by: #11394

    Restart-when: #11394 closes (its PR #11650 merges).
    Unlock-action: re-grade to pm:queue and dispatch; re-measure the off-root stack at the then-current main first.

    The machine-readable line is written now, in the same breath as the label, deliberately: this lane spent today unblocking #10303, which sat pm:blocked for two days carrying its blocker in prose only and was invisible to every unlock scan that ran in between. That repair earned its keep inside 36 hours. Not repeating the omission on a card filed the same morning.

    Why blocked rather than queued

    The report is complete and I would otherwise dispatch it — the failure is measured on both trees, the stack is quoted, and the fix shape is named. Two things make it not-yet:

    1. The fix consumes something that does not exist on main yet. The suggested route uses atRepoRoot, exported from scripts/cli-build-prerequisite.mjs by [finding] cli-build-prerequisite exports CLI and CLI_PKG as CWD-relative constants, so its probe defers on every off-root run of either gate that uses it #11394. Until PR fix(scripts): anchor the shared CLI-build probe at the repo root #11650 lands, a dev would have to either invent a second root-derivation in the same file that is about to receive one, or vendor it — both worse than waiting.
    2. File collision. PR fix(scripts): anchor the shared CLI-build probe at the repo root #11650 edits scripts/check-i18n-bundles.mjs (the existsSync(atRepoRoot(resolved.file)) companion line). Dispatching this now puts two open PRs on one file, which is the shape the single-writer-path check exists to refuse.

    Both clear the moment #11650 merges, which is imminent — this is a short hold, not a parked card.

    Grading notes for whoever takes it

    The report's own second reason is the load-bearing one and should stay load-bearing in the fix: this gate carries an elaborate prerequisite apparatus (#5217, #7681, reportPrerequisiteNotMet) whose entire purpose is that an environment fact is never reported as a content verdict — and an uncaught scandir 'packages' bypasses all of it. So the deliverable is not merely "make it work off-root"; it is that the population failure arrives through the same worded channel the rest of the gate already uses.

    ⭐ Note the direction, and do not let it get overstated: this is loud, not silently green. An uncaught throw naming node:fs costs a wrong first diagnosis, not a false pass. That distinguishes it from #10907, and a fix write-up claiming it prevented a false green would be wrong.

    Also carry over #10907's anti-#4690 note, which the report correctly flags: an empty population must be refused, not reported OK. Anchoring a scan without that check trades a loud crash for a silent green, which would be a strictly worse outcome than the bug being fixed.


    Generated by Claude Code

  2. os-steve commented on Aug 24, 2026

    @os-steve
    CollaboratorAuthor

    Unlock — blocker discharged

    Blocked-by: #11394 → #11394 closed completed, by PR #11650, landed. pm:blocked → pm:queue. Dispatchable now.

    Verified by content on origin/main rather than by the PR's merged flag, landing commit located by (#11650) in its subject:

    781cc0b52  fix(scripts): anchor the shared CLI-build probe at the repo root (#11650)
    
    scripts/cli-build-prerequisite.mjs   atRepoRoot            0 -> 3
                                         import.meta.url       0 -> 1
                                         CTRL CLI_PKG          5 -> 6      (non-zero both sides)
                                         CTRL oclifCommandFileFor  3 -> 4  (non-zero both sides)
    scripts/check-i18n-bundles.mjs:736   existsSync(atRepoRoot(resolved.file))
    

    Both conditions this card was held on have cleared:

    1. atRepoRoot now exists and is exported, so the fix has the seam to build on instead of inventing a second root-derivation in a file that was about to receive one.
    2. The file collision is gone — PR fix(scripts): anchor the shared CLI-build probe at the repo root #11650 is merged, so scripts/check-i18n-bundles.mjs has no other open PR against it.

    ⚠️ Note what #11650 already did to this file, so the next dev does not re-do it or assume more was done than was: it anchored line 736's existence check (existsSync(atRepoRoot(resolved.file))) — and only that. The population enumeration this card is about (findExtractConfigs('packages', 'packages')) is untouched. That was deliberate scoping, not an oversight, and it is exactly the remaining half.

    Re-measure the off-root stack at today's main before starting — that is the Unlock-action: this card carries, and the file has moved since the report.

    The grading notes from the triage comment above still stand, particularly the two that shape the deliverable: route the population failure through the gate's existing worded prerequisite channel rather than merely making it work off-root, and carry #10907's anti-#4690 rule — an empty population must be refused, not reported OK. Anchoring a scan without that trades a loud crash for a silent green, which would be strictly worse than the bug being fixed.


    Generated by Claude Code

  3. self-assigned this
    on Aug 24, 2026
  4. os-steve commented on Aug 24, 2026

    @os-steve
    CollaboratorAuthor

    Claim: domain:devx PM seat, session session_015ahemw8RcTgqtxrj15PEZx, branch claude/issue-11647-bundles-population-root-anchor.

    Dispatching at opus. Tier derived live at 09ae32e55, not recalled:

    Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s), derived here, not recalled.

    My judgment call, and opus rather than the sonnet floor for one reason: the cheap version of this fix — anchor the path, done — converts a loud crash into a silent green unless the empty-population refusal goes in with it. That trade is invisible to anyone reading the diff for correctness, and it is the strictly-worse direction.

    Unblocked this round: Blocked-by: #11394 discharged when PR #11650 landed (781cc0b52). Scope, the ⛔ boundaries, and what #11650 already did to this same file are in the unlock comment above.


    Generated by Claude Code

  5. os-steve commented on Aug 24, 2026

    @os-steve
    CollaboratorAuthor
    {
      "issue": 11647,
      "status": "done",
      "branch": "claude/issue-11647-bundles-population-root-anchor",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11675",
      "premise_still_valid": true,
      "summary": "Re-measured the off-root stack at today's main (c251ef421, not the 09ae32e55 named in dispatch) with an on-root positive control: 9 configs on-root, `Error: ENOENT ... scandir 'packages'` at `readdirSync (node:fs:1590:26)` off-root. Premise holds. Anchored the population at the gate's CALL SITE, not in the shared module: `findExtractConfigs(absDir, rel)` documents its first parameter as absolute and its other consumer (scripts/pm/dispatch-gates.mjs:2097) already passes `join(ROOT,'packages')`, so route B would have silently re-rooted that consumer. Added `populationVerdict()`, a pure classifier that splits the two causes an empty result can have: an empty POPULATION is an environment fact routed through `reportPrerequisiteNotMet` (the gate's existing worded channel, with its 'Nothing was checked' statement and a `fix:` that is deliberately not the CLI build), while a `--filter` that matched nothing is a typo and must not describe a healthy repo as broken. The walk's own throw is caught and routed through the same channel. Also anchored the three reads the population feeds (config docstring, documented `--out=`, and the child extractor's cwd) — without them the anchored walk only moves the crash one line down and an off-root run reports an environment fact as `N bundle problem(s)`, the shape #7681 removed.",
      "tests": "ALL AT FINAL COMMIT 9d41b11e0 (git status clean; union re-derived after the commit). Gate union DERIVED, not recalled: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` — provenance line 'derived from the tree of objectstack-ai/objectstack at commit c251ef421' and '--repo ... checked against this checkout's origin remote — it holds'; change set 1 path. Exit codes captured BEFORE any pipe; each verdict quoted from the gate's OWN printed line. pnpm check:i18n EXIT=0 -> '✓ check:i18n --self-test — bundle-drift, undeclared-authoring-key, missing-CLI-build, stale-workspace-dist and empty-population classifiers all go red, and stay distinct; the population walk is CWD-independent.' + 'check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).' | check:entry-guard EXIT=0 '✓ 142 scripts/ file(s)' | check:parse-guard EXIT=0 '✓ 141 scripts/ file(s)' | check:pnpm-filter-targets EXIT=0 '✓ 120/148 --filter occurrence(s)' | check:cross-package-test-inputs EXIT=0 'OK: 16 package(s) read outside themselves' | check:nul-bytes EXIT=0 'check-nul-bytes: OK (scanned 6519 text file(s))'. FULL-REPO `pnpm lint` RAN, not narrowed: LINT_EXIT=0 (145s under the verify lock) — so no narrowing to declare. END-TO-END on a built tree (CLI built via os-verify-lock, 'VERDICT command-exit 0 · held the lock 696s'): gate run from /tmp and from the repo root are BYTE-IDENTICAL, both exit 0, both 'OK (9 package(s))' — that comparison is what proves all four anchored sites moved, not just the walk. Live worded-refusal demos: `--filter=no-such-package` EXIT=1 prints the non-prerequisite refusal listing all 9 paths; a synthetic root with no packages/ EXIT=1 prints 'PREREQUISITE NOT MET — this gate's population could not be enumerated' with the evidence, the anchored path, and a fix that is not the CLI build. ABLATIONS — NO REBUILD LEG APPLIES: the subject is a plain .mjs script executed directly, with no dist/ and no build step between edit and run, so there is nothing to rebuild and no stale-artifact hazard; both legs ran under `trap ... EXIT INT TERM`. Mutation confirmed on disk by MARKER COUNTS AND FILE SHA, never by the editor's exit code. (A) revert the anchor to the pre-fix spelling: anchored marker 1->0, sha 5e3f435f->777a12e5; PREDICTED before running an uncaught ENOENT rather than a worded assertion failure (the walk throws before the assertion evaluates) and observed exactly that — ABLATION_A_EXIT=1, 'Error: ENOENT ... scandir packages at discoverExtractConfigs (...:294:10)'. (B) delete the empty-population refusal: 'population.length === 0' 1->0, sha 5e3f435f->ea55766b; predicted a worded failure, observed ABLATION_B_EXIT=1 with 3 named failures ('#4690 an empty population is refused — got null', '#4690 ...through the WORDED channel, not as a pass', '#11647 ...and the two population verdicts do not contaminate each other'). BOTH restored and proven byte-identical (sha back to 5e3f435f, `cmp` clean), self-test green again after each. The new self-test carries its OWN positive control: it records that the pre-fix spelling throws ENOENT from tmpdir(), so 'the anchored walk works off-root' cannot pass vacuously on a tree where the bare spelling would also have resolved.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Notes for the PM, beyond the JSON

    Your four Zone-2 assumptions, all four falsifiable, all four checked:

    1. Premise re-measured at c251ef421 (dispatch said 09ae32e55; main had moved again). Reproduces. One correction to how: running the gate off-root on a fresh worktree stops earlier at the unbuilt-CLI prerequisite — the population stack is only reachable on a built tree. The bare-walk reproduction with an on-root control is what isolates it.
    2. fix(scripts): anchor the shared CLI-build probe at the repo root #11650's scope confirmed by diff, not taken on trust: 61 insertions / 1 deletion, line 736's existsSync(atRepoRoot(resolved.file)) plus the #11394 self-test block. Population untouched. Nothing redone.
    3. i18n-bundle-surface.mjs is shared — and that settles the route. findExtractConfigs has exactly one other caller, scripts/pm/dispatch-gates.mjs:2097, which already passes join(ROOT, 'packages'). The module's contract was never wrong; only this call site was. So route B is rejected: anchoring inside the module would silently re-root that consumer's population — the [finding] cli-build-prerequisite exports CLI and CLI_PKG as CWD-relative constants, so its probe defers on every off-root run of either gate that uses it #11394 mistake in reverse, exactly as you flagged. I also checked the module's other walk, findMetadataFormModules: its only external caller (dispatch-gates.mjs:2113) is likewise already absolute. No sibling instance to file.
    4. Your anti-check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690 instruction, falsified in part — this is the one thing I want to flag. Two corrections:
      • An empty population was already refused. The pre-existing configs.length === 0 check exited 1. So anchoring alone would not have produced a silent green here; the hazard you were guarding against was real in principle but already covered at the count level. What was genuinely missing was the channel (console.error + exit 1, not the worded prerequisite apparatus) and the cause split.
      • count == 0 did need a narrower condition — but on the cause, not the count. No legitimate tree of this repo has zero configs, so the count test is right. But that check ran after the --filter narrowing, so a mistyped --filter and a broken checkout produced one identical sentence. Those are a user-input fact and an environment fact with different remedies. They are now two verdicts, and the self-test pins that neither borrows the other's wording.

    Direction not overstated. The PR says in as many words that this is loud, not silently green, that the cost was a wrong first diagnosis rather than a false pass, and that this PR did not prevent a false green.

    Governed surface: not reached — the diff is one file, scripts/check-i18n-bundles.mjs. PR left draft; no ready-flip, no auto-merge, no queue.

    skip-changeset: justified against the actual rule rather than assumed. pr-automation.yml's changeset-check is exempted by the label as the author's explicit opt-out (there is no scripts/ carve-out), and AGENTS.md §942 says pure bug fixes need no changeset. Nothing here ships: the change is confined to scripts/, the root package is private: true, and the file is in no published package's files. Label applied by additive POST (HTTP 200) and read back — size/m, skip-changeset.

    CI not waited on, per the standing contract — gate status is whatever CI reports after this comment.


    Generated by Claude Code


    Generated by Claude Code

  6. os-steve commented on Aug 24, 2026

    @os-steve
    CollaboratorAuthor
    {
      "issue": 11647,
      "status": "done",
      "branch": "claude/issue-11647-bundles-population-root-anchor",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11675",
      "premise_still_valid": true,
      "summary": "Follow-up round: the red `Lint & Repo Gates` was the bare-root-worklist ratchet, in scope and correct — anchoring the population gave the bare literal a population-constant name (PACKAGES_DIR), and a bare single-segment word builds no watch hint, so check:i18n newly joined the invisible bare-root species and owed a recorded verdict. Recorded REFUSE-UNSPELLABLE at d741c991d. NOT the PM's REFUSE-WIDE prior: by the table's own definitions REFUSE-WIDE means a declaration would be TRUE and is refused anyway, while this gate opens nine named config files out of 5093 tracked under the root (0.18%), so a subtree declaration would be FALSE — a filename filter, which is REFUSE-UNSPELLABLE. An identical row already exists for the sibling gate (check:i18n-coverage PACKAGES_DIR packages, REFUSE-UNSPELLABLE, 9 of 5035): the two gates select the same nine configs with the same filename-and-segment test under the same constant name, so refusing them alike is consistency rather than novelty. The original fix is unchanged — the only file touched this round is the triage ledger.",
      "tests": "ALL AT d741c991d. Union re-derived: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (provenance: 'derived from the tree of objectstack-ai/objectstack at commit d741c991d'; '--repo ... checked against this checkout's origin remote — it holds'; change set now 2 paths), which added exactly one family — the one that fired. Exit codes captured BEFORE any pipe; verdicts quoted from each gate's own line. bare-root-worklist --self-test EXIT=0 -> 'OK  self-test: 38 live row(s), 35 unreachable as spelled, 35 recorded verdict(s) — none stale, none missing.' | check:i18n --self-test EXIT=0 | check:entry-guard EXIT=0 '✓ 142 scripts/ file(s)' | check:parse-guard EXIT=0 '✓ 141 scripts/ file(s)' | check:pnpm-filter-targets EXIT=0 '✓ 120/148' | check:cross-package-test-inputs EXIT=0 'OK: 16 package(s)' | check:nul-bytes EXIT=0 'scanned 6519 text file(s)' | FULL-REPO `pnpm lint` EXIT=0 (117s under the verify lock), not narrowed. MEASUREMENT (numerator = the gate's own walk filter, denominator = trackedFiles()): 9 of 5093 = 0.18%. VOCABULARY PROBE (the PM's starred hypothesis, falsified rather than assumed): collapseHint DELETES glob segments, so `packages/*/scripts/i18n-extract.config.ts` collapses to the malformed `packages//scripts/i18n-extract.config.ts` and hintCovers matches it against NOTHING — not the target config, not anything — while extractWatchHints still admits it as a LIVE hint. A narrow declaration would therefore be a hint covering zero files, worse than either option; only `packages/**` covers the target, and it covers all 5093. POSITIVE ASSERTION, adapted to a refusal (proving refusal loses nothing rather than proving a declaration changes the derivation): dispatch-gates on packages/platform-objects/src/index.ts DOES derive check:i18n via the owning-package convention trigger; on packages/core/src/index.ts it correctly does NOT (that package cannot move a bundle); on packages/spec/src/ai/agent.form.ts it DOES via the metadata-form-module trigger. So check:i18n already reaches exactly the right package cards, at a precision a subtree hint would destroy. ABLATION C (non-vacuity of the row just added): delete it -> mutation proven on disk by marker count (row key 1 -> 0) AND sha (09b8d6a3 -> 02576657), under `trap ... EXIT INT TERM`; ABLATION_C_EXIT=1 with the FRESH assertion returning verbatim, naming 'check:i18n PACKAGES_DIR packages'; restored byte-identical to 09b8d6a3 and green again. No rebuild leg applies — both files are plain .mjs run directly, no dist. ONE DECLARED NARROWING: the full check:i18n nine-package extract (needs a built CLI) was not re-run at d741c991d. It ran green at 9d41b11e0 and scripts/check-i18n-bundles.mjs is byte-identical across the two commits — blob 5e3f435f at both, with bare-root-worklist.mjs the only file changed between them — so that green still describes this tree; re-running would have cost an 11.5-minute shared CLI rebuild for no new information. Declared, not silently skipped.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    For the PM — three things to read

    1. Your prior was directionally right and specifically wrong, and the difference matters.
    You said REFUSE-WIDE and asked to be corrected rather than obeyed. The correct verdict is REFUSE-UNSPELLABLE. The table defines REFUSE-WIDE as "the population really IS the whole top-level root. A declaration would be TRUE, and is refused anyway" — that is not this gate. Nine named config files out of 5093 is a filename filter, so a packages/** declaration would be false, not merely wide. Your reasoning for refusing was exactly right (the +139084 shape); only the row label differs. There is also an identical precedent already in the ledger — check:i18n-coverage PACKAGES_DIR packages, REFUSE-UNSPELLABLE, same nine files, same selector, same constant name.

    2. Your ⭐ narrow-declaration idea: checked, and it is worse than both options.
    I did not conclude the choice was binary — I measured it. collapseHint deletes glob segments, so every narrower spelling collapses to a malformed packages//… prefix that covers nothing, while still being admitted as a live hint. A narrow declaration would name this gate for zero files. Only packages/** covers anything, and it covers everything. So the choice is binary after all, and refusal is the honest half.

    3. ⚠️ Two process notes, neither of which I acted on unilaterally.

    • PR fix(scripts): anchor check-i18n-bundles' population at the repo root #11675 is no longer a draft, and I did not do that. The timeline shows ready_for_review by os-steve at 11:50:25Z — the shared identity, after my last read-back showed draft: true, and before your message saying "fix(scripts): anchor check-i18n-bundles' population at the repo root #11675 is not armed and will not be until CI is clean". I have not flipped it back: that state is another actor's, and reverting a ready flip would silently destroy auto-merge and merge-queue membership. For what it is worth it is not armed (auto_merge: null, mergeable_state: blocked). Yours to adjudicate.
    • I did edit scripts/pm/bare-root-worklist.mjs, and I want that visible rather than buried. Your ⛔ said not to edit it to get green. I read that as barring weakening, and I believe the reading is forced: REFUSE-* verdicts are recordable only in that file's TRIAGE map, so your own preferred REFUSE-WIDE would have required the same edit. What landed is 16 insertions, 0 deletions, entirely inside the data ledger — the recogniser, the sweep and the self-test are untouched, verified by filtering the diff for those anchors. If you disagree, the row is one contiguous block and trivially revertible; say so and I will drop it and report the gate as a finding instead.

    scripts/check-agent-test-spelling.mjs (PR #11667's file) was not touched.

    CI on the new head is not waited on, per the standing contract.


    Generated by Claude Code


    Generated by Claude Code

  7. os-steve commented on Aug 24, 2026

    @os-steve
    CollaboratorAuthor

    Landed — PR #11675, verified by content on origin/main

    acd920df7  fix(scripts): anchor check-i18n-bundles' population at the repo root (#11675)
    
    scripts/check-i18n-bundles.mjs
      populationVerdict                            0 -> 5
      findExtractConfigs('packages', 'packages')   1 -> 0
      CTRL reportPrerequisiteNotMet                4 -> 8
      CTRL findExtractConfigs (any)                3 -> 6      (non-zero both sides)
    scripts/pm/bare-root-worklist.mjs:222
      ['check:i18n PACKAGES_DIR packages', { verdict: 'REFUSE-UNSPELLABLE' … }]
    

    Card closed completed, closed_by_pull_requests records #11675. pm:dispatched stripped.

    ⭐ The refusal was proven the hard way — and it holds on main

    The bare-root ratchet caught this PR (a named PACKAGES_DIR constant is a bare directory word, so check:i18n would have been invisible to the derivation). The verdict is REFUSE-UNSPELLABLE, and the argument for it was inverted from the usual one: instead of proving a declaration changes the derivation, the run proved that refusing loses nothing. Re-run against origin/main after landing:

    $ node scripts/pm/dispatch-gates.mjs … packages/platform-objects/src/index.ts   → check:i18n derived  (1)
    $ node scripts/pm/dispatch-gates.mjs … packages/core/src/index.ts              → check:i18n absent   (0)   ← control
    

    check:i18n still reaches the cards that can actually move a bundle, through the owning-package convention, and still correctly ignores a package that cannot. A packages/** subtree hint would have destroyed that precision rather than added to it — 9 files out of 5093, 0.18%.

    ⚠️ My own steer was REFUSE-WIDE and that label was wrong. The ledger reserves WIDE for "the population really IS the whole root — a declaration would be TRUE, and is refused anyway." At 0.18% a declaration here would be false, which is UNSPELLABLE. My reasoning for refusing was right; the vocabulary was not, and the distinction changes what a future reader does with the row. There is also an identical twin already in the ledger (check:i18n-coverage PACKAGES_DIR packages, same nine configs, same selector) — landing beside it beats landing correct-but-orphaned.

    ⭐ And my suggested "third way" — a narrow declaration like packages/*/scripts/i18n-extract.config.ts — was measured worse than both options: collapseHint deletes glob segments, so it collapses to a malformed packages//scripts/… that hintCovers matches against nothing, while extractWatchHints still admits it as a live hint. A hint that is live and covers zero files is this family's defect in mirror image.

    Three things the fix got right that the card did not ask for

    Route B was refuted by reading the other caller. Anchoring inside the shared i18n-bundle-surface.mjs would have double-rooted scripts/pm/dispatch-gates.mjs:2097, which already passes join(ROOT, 'packages') — the module's own docblock says "Deriving one from the other at the call site is how the two spellings drift." A fix for this gate would have silently broken the dispatch tool every lane depends on.

    populationVerdict() splits the two causes of "empty." An empty population is an environment fact and routes through reportPrerequisiteNotMet, with a fix: that is deliberately not "build the CLI"; a --filter matching nothing is a typo and must not describe a healthy repo as broken. My anti-#4690 ruling said "refuse on empty" — applied bluntly, that would have made the second case lie.

    Three further reads were anchored, not just the walk. Without them the anchored walk "only moves the crash one line down," and an off-root run reports an environment fact as N bundle problem(s) — the shape #7681 removed. Proven by the off-root and repo-root runs being byte-identical, rather than by asserting each site.

    The ablation predicted the shape of the failure before running it — an uncaught ENOENT rather than a worded assertion failure, because the walk throws before the assertion evaluates — and observed exactly that. Predicting which way a gate breaks is a higher bar than predicting that it breaks.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions