Skip to content

fix(scripts): check:i18n-coverage declares the example configs and their src trees to the dispatch derivation - #21271

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21011-i18n-coverage-examples-roots
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21011-i18n-coverage-examples-roots

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21011
Clause-②: no

What

check:i18n-coverage now declares what it reads to the dispatch derivation, so an edit under an example app derives the gate locally. CI already ran it on the same diff.

  • scripts/check-i18n-coverage.mjs: const ROOT_DIR_WATCH_HINTS = ['examples/*/objectstack.config.ts', 'examples/*/src/**']; beside EXAMPLES_DIR, plus a new --self-test battery that holds the declaration against the live tree (details below). What the gate judges is unchanged.
  • scripts/pm/bare-root-worklist.mjs: the check-i18n-coverage.mjs EXAMPLES_DIR examples row is re-decided from SPELLABLE-UNDECLARED to DECLARED-NARROWER. Its recorded spelling is re-measured as the two hints above, and its old single-hint spelling entry is replaced.
  • scripts/pm/dispatch-gates.mjs is not touched. It is frozen under ruling 208 R6, and its self-test stayed green with the declaration in place (see Gates).

Why this spelling and not examples/**

The gate runs os lint on each example config. os lint loads the config through bundle-require: esbuild bundles every relative import, and @objectstack/* plus the driver modules stay external (BUNDLE_REQUIRE_EXTERNALS in packages/cli/src/utils/config.ts). So the count moves with anything in the config's relative import graph. Measured with esbuild and those externals at 30c530e5:

tracked files of which in the 4 configs' import closures
closure (the real read) 153 153
declared: examples/*/objectstack.config.ts + examples/*/src/** 170 153 (complete, 90% precise)
examples/** (bare-root spelling) 256 153 (60% precise)

Every closure file is either a config or a file under that same example's src/. Here are the 17 declared files that no config imports:

  • 11 src/docs/*.md pages. os lint reads them, but only through docs/* rules, and the gate counts i18n/ rules alone.
  • 2 *.test.ts files under app-todo/src.
  • 3 unimported files under app-showcase/src.
  • the src/ of embed-objectql, which has no config.

examples/** would add the test suites, e2e specs, READMEs, changelogs and manifests. That is 86 more files, none of which a config imports.

Pins (in the gate's own --self-test, new battery, 10 cases)

The battery imports the derivation's own extractWatchHints and hintCovers. The import is dynamic and sits inside selfTest, which the extractor masks. So the gate's own extracted hints gain exactly the two declared literals, and its import follow is unchanged: before [cli-build-prerequisite.mjs, import-prerequisite.mjs], after the same.

Measured end to end with dispatch-gates --commands

Before is base 30c530e5, after is head 61bd05aa.

change set before after
PR #20998's full file list (15 paths) 116 commands, no check:i18n-coverage 117, the only addition is pnpm check:i18n-coverage
its 4 examples/app-showcase/** paths alone 40, absent 41, present
its 2 packages/ paths alone (packages/spec/src/data/analytics.zod.ts, packages/services/service-analytics/README.md) 74, absent 74, byte-identical list
examples/app-showcase/test/gap-fill.test.ts alone (not measured) 38, absent

Ablations

All four were run through scripts/ablation-replace.mjs against the committed head. Each mutation was proven on disk by its anchor count and blob hash, and each restore was proven by blob == HEAD and an empty git diff HEAD:

  1. Declaration replaced by ['examples/**']: the self-test goes red, 1 failure. The precision case names the test file and examples/README.md.
  2. Declaration computed from EXAMPLES_DIR: red, 4 failures. The extracted hints fall back to ["scripts/i18n-coverage-baseline.json","i18n","node_modules"].
  3. src/** hint dropped: red, 2 failures (the showcase edit and the import targets are no longer derived).
  4. Same mutation as 3, run against bare-root-worklist.mjs --self-test: red, 1 failure, "NOT DECLARED: examples/*/src/**". The map and the gate cannot drift apart silently.

Gates (all at head 61bd05aa)

The family list came from node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths, change set from the merge base). It names the same 30 the dispatch listed, and all 30 exited 0. --ran reconciliation: "30 derived famil(ies) accounted for — 30 run, 0 NOT-MEASURED".

  • node scripts/check-i18n-coverage.mjs --self-test passed, then the gate itself: check-i18n-coverage: OK (13 config(s), 621 baselined untranslated string(s), none new). It ran after the gate's own closure build under the verify lock (VERDICT command-exit 0).
  • node scripts/pm/bare-root-worklist.mjs --self-test: OK. There are now 12 DECLARED-NARROWER records held set-equal to their gates' arrays; there were 11.
  • pnpm check:pm-dispatch-gates: dispatch-gates self-test: 1976 cases pass, run detached for 825.6s. No pin moved, and the frozen file is not in the diff.
  • Not on the derived list, run anyway because it judges declarations: node scripts/check-declared-population-live.mjs passed with 266 of 326 families live.
  • The other 27 derived families were all exit 0: ci-filter-parity, closing-keyword-parity (+self-test), comment-mask-corpus, declaration-mirrors (+self-test), scripts-symbol-anchors (+self-test), self-test-wired (+self-test), self-test-workflow-commands (+self-test), whole-set-label-write (+self-test), agent-test-spelling, bash32-floor, cli-command-ids, cross-package-test-inputs, driver-memory-census, entry-guard, gitlink-declared, nul-bytes, parse-guard, pnpm-filter-targets, ratchet-remedy-authority, refd-timer-probe and watch-hint-literal.
  • Lint, narrowed and declared as narrowed: eslint --no-inline-config --format json over the 2 changed files returned 2 files, 0 errors and 0 warnings. eslint.config.mjs enables no type-aware linting (no parserOptions.project), so this diff cannot move the verdict on any untouched file. The full pnpm lint is left to CI.

Acceptance notes

  • The PACKAGES half is out of scope, on purpose. PACKAGES_DIR stays undeclared, and its worklist row keeps SPELLABLE-UNDECLARED (9 extract configs share one spelling with check:i18n-bundles and check:i18n-stale-fill, deferred together). The gate's closure also reaches every workspace package a config imports by name, so a packages/spec edit can move this count too. Declaring that would be the packages/ root wholesale, the REFUSE-WIDE shape. The CLI half already reaches this gate through cli-build-prerequisite.mjs.
  • The dispatch premise was off by one verdict. The dispatch and claim describe the worklist record as REFUSE-UNSPELLABLE, and so does the gate's old header comment. On main the row was SPELLABLE-UNDECLARED with "Deferred: no consumer has asked". This card is that consumer, and moving the row to DECLARED-NARROWER follows the split criterion of the 2026-08-26 ruling recorded in the worklist header. A paragraph there records the re-decision. The stale header comment in the gate is replaced.
  • selfTest in check-i18n-coverage.mjs became async, because it needs the dynamic import. The dispatch reads (await selfTest()) !== SELF_TEST_VERDICT, a spelling other gates already use. Self-test runtime went from about 0.11s to about 0.2s.
  • skip-changeset: scripts/** publishes nothing from any package.

Generated by Claude Code

claude added 2 commits October 1, 2026 23:52
…e source trees they import to the dispatch derivation

dispatch-gates names a gate for a card from the path literals in the gate's own
source, and refuses the bare 'examples' constant as too generic, so an edit under
examples/ never derived check:i18n-coverage. The gate runs os lint on each example
config, which bundles the config's whole relative import graph; measured at
30c530e that graph is the config plus files under its own src/ tree (153 tracked
files). Declare exactly those two shapes and hold the declaration in --self-test
through the derivation's own extractor and covering rule.

Claude-Session: https://claude.ai/code/session_01FNKm1SmPpuJASnbjxWGtsJ
Co-authored-by: Claude <noreply@anthropic.com>
…mples row as DECLARED-NARROWER

The row was SPELLABLE-UNDECLARED for want of a consumer; #21011 is that consumer,
measured. Its recorded spelling now names what the gate reads (each example config
and the src/ tree its bundled import graph stays inside), re-measured on one tree
at 30c530e, and is held set-equal to the gate's declared array.

Claude-Session: https://claude.ai/code/session_01FNKm1SmPpuJASnbjxWGtsJ
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants