Skip to content

Commit d82f867

Browse files
fix(scripts): check:i18n-coverage declares the example configs and their src trees to the dispatch derivation (#21271)
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. - The derivation reads both declared hints off this source. - Positive: the three showcase source files of PR #20998 derive the gate, and so does every config `discoverExamples()` finds (4). - Completeness: no relative import in a config or in a non-test file under its `src/` resolves outside that example's `src/`. On this tree that is 177 edges and 0 escapes. A negative control proves the scan still sees an escape. - Precision: these do not derive through this declaration: `examples/app-showcase/test/gap-fill.test.ts`, `examples/README.md`, and the two `packages/` paths of PR #20998. Controls show that the extractor drops a computed spelling, and that `examples/**` would have named the test file. ## 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](https://claude.ai/code/session_01FNKm1SmPpuJASnbjxWGtsJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 95b91cc commit d82f867

2 files changed

Lines changed: 240 additions & 28 deletions

File tree

‎scripts/check-i18n-coverage.mjs‎

Lines changed: 189 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@
184184
// here is one sentence in that remedy, not a mechanism.
185185
import { execFileSync } from 'node:child_process';
186186
import { readdirSync, readFileSync, writeFileSync, existsSync, openSync, closeSync, unlinkSync } from 'node:fs';
187-
import { dirname, join, resolve } from 'node:path';
187+
import { dirname, join, posix, relative, resolve } from 'node:path';
188188
import { fileURLToPath } from 'node:url';
189189
import { tmpdir } from 'node:os';
190190
import { randomUUID } from 'node:crypto';
@@ -221,11 +221,12 @@ const SELF_TEST_BATTERIES = Object.freeze({
221221
'Root anchoring and the population classifier (#10907). These are the only': 4,
222222
'The build-prerequisite CLOSURE (#12564). What makes the remedy worth naming': 15,
223223
'The prerequisite refusal CLASS, and the advisory that names it (#14857).': 23,
224+
'The dispatch-gates declaration (#21011): the example configs and the trees they import': 10,
224225
});
225226

226227
// DELETING an entry silences that battery's floor exactly as effectively as
227228
// zeroing it, so the roster's own size is pinned too.
228-
const SELF_TEST_BATTERY_FLOOR = 5;
229+
const SELF_TEST_BATTERY_FLOOR = 6;
229230

230231
// The key an assertion is filed under when no battery is open. It is not a
231232
// declared battery, so it reds by the same set difference rather than silently
@@ -246,16 +247,52 @@ const EXAMPLES_DIR = 'examples';
246247
const PACKAGES_DIR = 'packages';
247248
const BASELINE_PATH = 'scripts/i18n-coverage-baseline.json';
248249

249-
// ⛔ Neither root above is declared to the dispatch derivation, and that is a
250-
// recorded REFUSAL rather than an omission. Both populations are FILENAME
251-
// filters — one `objectstack.config.ts` per example directory (3 of 240), and
252-
// files named `i18n-extract.config.ts` beneath a `scripts` segment (9 of 5035).
253-
// The `ROOT_DIR_WATCH_HINTS` idiom can only name a whole subtree, so the only
254-
// spellable claim here would name this gate for 5035 files to reach 9 — the
255-
// costlier error, per `hintCovers`. Both verdicts are recorded as
256-
// REFUSE-UNSPELLABLE in the triage that `scripts/pm/bare-root-worklist.mjs`
257-
// self-tests on every PR; giving `PACKAGES_DIR` a population-constant name is
258-
// what made this root visible to that sweep at all.
250+
// ── What the dispatch derivation is told this gate reads (#21011) ──────────
251+
//
252+
// `scripts/pm/dispatch-gates.mjs` names a gate for a card by reading path
253+
// literals out of the gate's own source, and it refuses a bare single-segment
254+
// word like the two constants above as too generic. So with nothing else
255+
// declared, an edit under `examples/` derived nothing for this gate. Measured on
256+
// a real producer (#21011): a showcase diff derived 116 families locally, none
257+
// of them this one, and CI then failed it here (`app-showcase` 414 -> 416).
258+
//
259+
// The EXAMPLES population is not the filename filter `discoverExamples` applies.
260+
// This gate spawns `os lint` on each config, and `os lint` loads the config
261+
// through bundle-require — esbuild bundling every relative import, with
262+
// `@objectstack/*` kept external — so the count moves with any file the
263+
// config's import graph reaches: objects, datasets, dashboards, translation
264+
// bundles. Measured with that bundler and those externals at `30c530e5`: the 4
265+
// configs' import closures are 153 tracked files, each one either the config
266+
// itself or a file under that example's own `src/` tree.
267+
//
268+
// So the declaration below names exactly those two shapes, and nothing wider:
269+
// - `examples/**` would also name this gate for the test suites, e2e specs,
270+
// READMEs, changelogs and manifests — 103 of the 256 tracked files under the
271+
// root, none of which any config imports (60% precise);
272+
// - the two hints below cover 170: the whole closure (complete), plus 17 files
273+
// no config imports — 11 `src/docs/*.md` pages (`os lint` reads them, but
274+
// its docs rules are not `i18n/` rules, so they never reach this count), two
275+
// `*.test.ts` and three other unimported files under `src/`, and the `src/`
276+
// of an example that has no config (90% precise).
277+
// `--self-test` holds this against the live tree from both sides: every config
278+
// the population finds, and the files a real showcase edit touched, are covered
279+
// as the derivation itself reads this source; a test file and a `packages/` path
280+
// are not; and no relative import in a config or under its `src/` leaves that
281+
// example's `src/` — the one edit that would make this declaration under-name.
282+
//
283+
// ⛔ The PACKAGES half stays undeclared, on its own recorded verdict:
284+
// `scripts/pm/bare-root-worklist.mjs` holds it SPELLABLE-UNDECLARED (nine extract
285+
// configs, one spelling shared with two sibling i18n gates, deferred with them).
286+
// That map's EXAMPLES row is DECLARED-NARROWER and holds its recorded spelling
287+
// SET-EQUAL to the array below, so the record and the declaration cannot drift
288+
// apart silently. Nor does this declaration chase the workspace packages a
289+
// config imports by name: a spec edit can move this count too, but that closure
290+
// is the `packages/` root wholesale — the REFUSE-WIDE shape — and its CLI half
291+
// already reaches this gate through `cli-build-prerequisite.mjs`.
292+
//
293+
// Provenance, never a lookup key: the measurement never reads the array, and
294+
// only `--self-test` does — to hold it against what the gate really loads.
295+
const ROOT_DIR_WATCH_HINTS = ['examples/*/objectstack.config.ts', 'examples/*/src/**'];
259296

260297
/** A repo-relative path, resolved against the module-derived root. */
261298
const at = (rel) => join(REPO_ROOT, rel);
@@ -709,7 +746,7 @@ function measureAllConfigs(configPaths, measure) {
709746
// as one that passed (#13798).
710747
const SELF_TEST_VERDICT = 'check-i18n-coverage self-test reached its verdict';
711748

712-
function selfTest() {
749+
async function selfTest() {
713750
// The battery ledger this self-test's floor is evaluated against (#13489).
714751
// `battery()` opens a battery; every assertion below is attributed to the one
715752
// most recently opened, so a section that stops running stops registering and
@@ -1275,6 +1312,141 @@ function selfTest() {
12751312
'the stale-spelling predicate no longer sees `Exit code 1`',
12761313
);
12771314

1315+
// -------------------------------------------------------------------------
1316+
// The dispatch-gates declaration (#21011). `ROOT_DIR_WATCH_HINTS` is read by
1317+
// ANOTHER tool, so nothing in this gate's own run can go red when it is wrong:
1318+
// a missing hint is a gate the derivation never names for an example edit
1319+
// (the defect this card was filed on), a surplus one names it for files it
1320+
// never opens. So both directions are asked here, of the live tree, through
1321+
// the derivation's OWN extractor and covering rule — imported inside this
1322+
// body, where the extractor's self-test mask blanks the specifier, so the
1323+
// import adds nothing to the population this file declares.
1324+
// -------------------------------------------------------------------------
1325+
battery('The dispatch-gates declaration (#21011): the example configs and the trees they import');
1326+
const { extractWatchHints, hintCovers } = await import('./pm/dispatch-gates.mjs');
1327+
const ownPath = relative(REPO_ROOT, fileURLToPath(import.meta.url)).split('\\').join('/');
1328+
const derivedHints = extractWatchHints(readFileSync(fileURLToPath(import.meta.url), 'utf8'), ownPath);
1329+
const derives = (path, hints = derivedHints) => hints.some((h) => hintCovers(h, path));
1330+
expect(
1331+
'#21011 the derivation reads every declared hint off this source',
1332+
ROOT_DIR_WATCH_HINTS.length > 0 && ROOT_DIR_WATCH_HINTS.every((h) => derivedHints.includes(h)),
1333+
`declared ${JSON.stringify(ROOT_DIR_WATCH_HINTS)}, extracted ${JSON.stringify(derivedHints)} — a declaration ` +
1334+
'the extractor cannot read (a computed spelling, a leading glob) names this gate for nothing',
1335+
);
1336+
// The positive case the card asked for, spelled as the paths of the real diff
1337+
// that went red in CI (#20998's three example source files). Strings, not
1338+
// files: `hintCovers` judges a path, so a later rename cannot empty this case.
1339+
const SHOWCASE_EDIT = [
1340+
'examples/app-showcase/src/data/analytics/showcase.cube.ts',
1341+
'examples/app-showcase/src/system/translations/index.ts',
1342+
'examples/app-showcase/src/ui/datasets/chart-gallery.dataset.ts',
1343+
];
1344+
expect(
1345+
'#21011 an edit to the showcase files a real red diff touched derives this gate',
1346+
SHOWCASE_EDIT.every((p) => derives(p)),
1347+
`not derived: ${SHOWCASE_EDIT.filter((p) => !derives(p)).join(', ')}`,
1348+
);
1349+
const liveConfigs = discoverExamples();
1350+
expect(
1351+
'#21011 every example config the population finds derives this gate',
1352+
liveConfigs.length > 0 && liveConfigs.every((p) => derives(p)),
1353+
`${liveConfigs.length} config(s); not derived: ${liveConfigs.filter((p) => !derives(p)).join(', ') || 'none'}`,
1354+
);
1355+
1356+
// COMPLETENESS against what `os lint` loads. Its bundler follows relative
1357+
// specifiers and keeps every `@objectstack/*` import external, so the only way
1358+
// a config's import graph can reach an example file outside the declaration is
1359+
// a relative specifier — in the config or in a file under its `src/` — that
1360+
// leaves that example's `src/`. Asked of every such file, not only the ones
1361+
// the graph reaches: a superset, so a miss cannot hide behind a guess about
1362+
// which files are imported. Test files are skipped, being what no config loads.
1363+
const RELATIVE_SPECIFIER = /(?:\bfrom|\bimport|\brequire)\s*\(?\s*(['"])(\.{1,2}\/[^'"\n]*)\1/g;
1364+
const escapesOf = (file, text, srcRoot) => {
1365+
const out = [];
1366+
for (const m of String(text).matchAll(RELATIVE_SPECIFIER)) {
1367+
const target = posix.normalize(posix.join(posix.dirname(file), m[2]));
1368+
out.push({ target, escapes: target !== srcRoot && !target.startsWith(`${srcRoot}/`) });
1369+
}
1370+
return out;
1371+
};
1372+
const sourcesUnder = (rel, out = []) => {
1373+
if (!existsSync(at(rel))) return out;
1374+
for (const e of readdirSync(at(rel), { withFileTypes: true })) {
1375+
const p = posix.join(rel, e.name);
1376+
if (e.isDirectory()) sourcesUnder(p, out);
1377+
else if (/\.(?:[cm]?[jt]sx?)$/.test(e.name) && !/\.(?:test|spec)\./.test(e.name)) out.push(p);
1378+
}
1379+
return out;
1380+
};
1381+
const edges = liveConfigs.flatMap((config) => {
1382+
const srcRoot = posix.join(posix.dirname(config), 'src');
1383+
return [config, ...sourcesUnder(srcRoot)].flatMap((file) =>
1384+
escapesOf(file, readFileSync(at(file), 'utf8'), srcRoot).map((edge) => ({ file, ...edge })));
1385+
});
1386+
const escaped = edges.filter((e) => e.escapes);
1387+
expect(
1388+
'#21011 the import scan sees real edges — the configs reach their sources by relative specifiers',
1389+
edges.length > 0 && edges.some((e) => liveConfigs.includes(e.file)),
1390+
`${edges.length} edge(s) scanned; an empty scan would pass the next case over nothing`,
1391+
);
1392+
expect(
1393+
'#21011 no relative import in a config or under its `src/` leaves that example\'s `src/` — the ' +
1394+
'declaration names the whole graph `os lint` loads',
1395+
escaped.length === 0,
1396+
`${escaped.map((e) => `${e.file} -> ${e.target}`).slice(0, 4).join(' · ')} — this import reaches a ` +
1397+
'file the declaration does not name, so an edit there derives nothing for this gate. Move the file under ' +
1398+
'`src/`, or widen ROOT_DIR_WATCH_HINTS to the root it really reads and re-measure its precision',
1399+
);
1400+
expect(
1401+
'#21011 …and every target the scan resolved is a path the derivation names',
1402+
edges.every((e) => e.escapes || derives(e.target)),
1403+
edges.filter((e) => !e.escapes && !derives(e.target)).map((e) => e.target).slice(0, 4).join(' · '),
1404+
);
1405+
expect(
1406+
'NEGATIVE CONTROL: the escape scan can still see an escape, and passes an in-tree import',
1407+
escapesOf('examples/app-x/src/a.ts', "import s from '../../shared/s.js';", 'examples/app-x/src')
1408+
.every((e) => e.escapes)
1409+
&& escapesOf('examples/app-x/objectstack.config.ts', "import a from './src/a.js';", 'examples/app-x/src')
1410+
.every((e) => !e.escapes)
1411+
&& escapesOf('examples/app-x/src/a.ts', "export { b } from './b.js'; const c = await import('../c.js');",
1412+
'examples/app-x/src').filter((e) => e.escapes).length === 1,
1413+
'the relative-specifier scan stopped discriminating, so the completeness case above cannot fail',
1414+
);
1415+
1416+
// PRECISION: what the derivation must NOT name. A test file under an example
1417+
// is exactly what `examples/**` would add and no config imports; the two
1418+
// `packages/` paths are the same red diff's non-example files, which the
1419+
// card's other pin says must not derive through THIS declaration (the CLI half
1420+
// of this gate's reach is inherited from `cli-build-prerequisite.mjs`, and
1421+
// judged there).
1422+
const NOT_THIS_DECLARATION = [
1423+
'examples/app-showcase/test/gap-fill.test.ts',
1424+
'examples/README.md',
1425+
'packages/spec/src/data/analytics.zod.ts',
1426+
'packages/services/service-analytics/README.md',
1427+
];
1428+
expect(
1429+
'#21011 a test file, a root README and a packages/ path that touches no declared root do NOT derive it',
1430+
NOT_THIS_DECLARATION.every((p) => !derives(p)),
1431+
`derived: ${NOT_THIS_DECLARATION.filter((p) => derives(p)).join(', ')}`,
1432+
);
1433+
// The controls that keep the cases above measurements: the extractor really
1434+
// reads a literal fixture and really drops the computed spelling, so the first
1435+
// case can fail; and `examples/**` really would name the test file, so the
1436+
// precision case is asked of a declaration that could have been wider.
1437+
const literalFixture = extractWatchHints("const HINTS = ['examples/*/src/**'];\n", ownPath);
1438+
const computedFixture = extractWatchHints("const HINTS = ['examples'].map((r) => r + '/*/src/**');\n", ownPath);
1439+
expect(
1440+
'NEGATIVE CONTROL: the extractor reads a literal declaration and drops a computed one',
1441+
literalFixture.includes('examples/*/src/**') && !computedFixture.includes('examples/*/src/**'),
1442+
`literal -> ${JSON.stringify(literalFixture)}, computed -> ${JSON.stringify(computedFixture)}`,
1443+
);
1444+
expect(
1445+
'NEGATIVE CONTROL: the wholesale spelling WOULD name the test file this declaration leaves out',
1446+
derives(NOT_THIS_DECLARATION[0], ['examples/**']) && !derives(NOT_THIS_DECLARATION[2], ['examples/**']),
1447+
'hintCovers no longer separates a subtree from its root, so the precision case is not asking anything',
1448+
);
1449+
12781450
// ── The floor: every declared battery RAN, and ran its cases (#13489) ────
12791451
//
12801452
// Evaluated after every battery has had its chance and BEFORE the verdict, so
@@ -1331,14 +1503,16 @@ function selfTest() {
13311503
`from outside the repo root as well as inside it, and an empty one is refused rather than reported OK; ` +
13321504
`the build-prerequisite closure names all ${onRoot.length} of them plus the CLI, and refuses whole rather ` +
13331505
`than naming some; and all three refusal paths exit ${EXIT_PREREQUISITE_NOT_MET} — distinct from a finding's ` +
1334-
`${EXIT_FINDINGS} — with an advisory that names the number it claims (#14857).`,
1506+
`${EXIT_FINDINGS} — with an advisory that names the number it claims (#14857); and the dispatch derivation, ` +
1507+
`reading this source, names this gate for all ${liveConfigs.length} example config(s) and every file their ` +
1508+
`${edges.length} relative import(s) reach, and for no test file or undeclared packages/ path (#21011).`,
13351509
);
13361510

13371511
return SELF_TEST_VERDICT;
13381512
}
13391513

13401514
if (process.argv.includes('--self-test')) {
1341-
if (selfTest() !== SELF_TEST_VERDICT) {
1515+
if ((await selfTest()) !== SELF_TEST_VERDICT) {
13421516
console.error(
13431517
'\n✗ check-i18n-coverage self-test: selfTest() returned without reaching its verdict,\n'
13441518
+ 'so no success line was printed. Exiting 0 here would report a self-test\n'

0 commit comments

Comments
 (0)