diff --git a/scripts/check-i18n-bundles.mjs b/scripts/check-i18n-bundles.mjs index 82a581c069..ad468628ea 100644 --- a/scripts/check-i18n-bundles.mjs +++ b/scripts/check-i18n-bundles.mjs @@ -115,6 +115,37 @@ import { findExtractConfigs, flagsFromDocstring } from './i18n-bundle-surface.mj */ const SURFACE_MODULE = 'scripts/i18n-bundle-surface.mjs'; +/** + * The two producers of the `metadataForms` TYPE-LEVEL surface, declared as + * bare module-body path literals for the same reason as SURFACE_MODULE above + * (#9144). `walkMetadataForms` (packages/cli/src/utils/i18n-extract.ts) emits + * `metadataForms..label` / `.description` for every entry of + * `DEFAULT_METADATA_TYPE_REGISTRY` — including form-less types like + * `datasource`/`job`/`translation` — and separately reads + * `METADATA_FORM_REGISTRY` itself (the map, not the `*.form.ts` leaves it + * points at) to decide which types also get section/field labels. Editing + * either moves the same `platform-objects` bundles PR #9113 had to + * regenerate — and, unlike the `*.form.ts` leaves, neither carries a filename + * SURFACE_MODULE's own convention (`isMetadataFormModulePath`) can see. + * + * Not folded into SURFACE_MODULE: that module documents the ONE convention it + * enumerates at runtime (the `.form.ts` suffix), and these two files + * deliberately do not carry it — inventing a second filename convention for + * exactly two files would be the guess #9144 declined to make. A bare + * coupling constant was the option the card measured to have no downside + * beyond upkeep; see i18n-bundle-surface.mjs's header for the two rejected + * alternatives and why. + * + * This is per-coupling manual upkeep, deliberately, and it does not go quiet: + * dispatch-gates' own self-test pins that a card editing either path derives + * check:i18n, against the real files (existsSync) — delete either constant + * and that self-test reddens instead of the silence coming back. If either + * module is renamed or the registries merge, update the self-test's pins in + * the same change: the evidence goes with the claim, never ahead of it. + */ +const METADATA_TYPE_REGISTRY_MODULE = 'packages/spec/src/kernel/metadata-plugin.zod.ts'; +const METADATA_FORM_REGISTRY_MODULE = 'packages/spec/src/system/metadata-form-registry.ts'; + /** The one command this gate invokes per package, as oclif topic/command parts. */ const EXTRACT_COMMAND_ID = ['i18n', 'extract']; const write = process.argv.includes('--write'); diff --git a/scripts/i18n-bundle-surface.mjs b/scripts/i18n-bundle-surface.mjs index 11a677ebec..609a9dcc31 100644 --- a/scripts/i18n-bundle-surface.mjs +++ b/scripts/i18n-bundle-surface.mjs @@ -50,17 +50,23 @@ * form — the convention and the population coincide, with nothing left over on * either side. * - * What it deliberately does NOT cover, measured and stated so the next reader - * does not mistake silence for coverage: the type-level half of the same - * surface. `walkMetadataForms` in packages/cli/src/utils/i18n-extract.ts emits + * What it still deliberately does NOT cover, stated so the next reader does + * not mistake silence for coverage: the type-level half of the same surface. + * `walkMetadataForms` in packages/cli/src/utils/i18n-extract.ts emits * `metadataForms.TYPE.label`/`.description` for every entry of * DEFAULT_METADATA_TYPE_REGISTRY (packages/spec/src/kernel/metadata-plugin.zod.ts), - * and the registry module itself decides which forms are walked. Editing either - * moves the same four bundles and matches no convention here, because neither - * carries a filename that distinguishes it. Closing that edge needs an anchor - * this module does not have, and the candidates trade off against each other - * rather than being one obvious shape, so it is filed rather than guessed at: - * issue 9144. + * and the registry module itself (packages/spec/src/system/metadata-form- + * registry.ts) decides which forms are walked. Editing either moves the same + * four bundles, and neither carries a filename this module's ONE convention + * (the `.form.ts` suffix) can see — inventing a second convention for exactly + * two files would be a guess, not a discovery. #9144 closed that edge WITHOUT + * extending this module: the two module paths are declared as bare coupling + * constants directly in check-i18n-bundles.mjs (METADATA_TYPE_REGISTRY_MODULE + * / METADATA_FORM_REGISTRY_MODULE, next to SURFACE_MODULE), which the + * derivation reads straight off that gate's own module body — no population + * to enumerate, since it is exactly two known files. See that pair's doc + * comment for why here was the wrong home and dispatch-gates.mjs's self-test + * for how the constants are kept honest. */ import { readFileSync } from 'node:fs'; diff --git a/scripts/pm/dispatch-gates.mjs b/scripts/pm/dispatch-gates.mjs index c8e3ad887a..edf61e6a14 100644 --- a/scripts/pm/dispatch-gates.mjs +++ b/scripts/pm/dispatch-gates.mjs @@ -1159,15 +1159,16 @@ export function reachesMetadataFormModule(path, modulePaths) { * output for every card in the tree; * - `check:i18n` walks `packages/` at runtime for files NAMED * `i18n-extract.config.ts` and re-extracts each owning package's bundles. - * Its source is worse than silent: the path-ish literals it does carry are - * its CLI prerequisite and stale-dist checks (`packages/cli/dist/commands/ - * i18n/extract.js`, `packages/spec/dist`, measured — eleven hints, none of - * them the population). So it matches nothing AND, having hints, never - * reaches the "undetermined" bucket either: before this entry existed, an - * edit to `packages/services/service-messaging/src/objects/` — which - * regenerates that package's four bundles — printed the gate in NEITHER - * half of the output. A gate the derivation cannot mention at all is the - * one shape this script must not produce; it cost a PR a CI round. + * Its source names only three hints (measured, post-#9144): the shared + * walk module (SURFACE_MODULE) and the two metadata-registry coupling + * constants below — none of them the OWNING-PACKAGE population this entry + * answers for. So it still matches nothing on an ordinary object/field + * edit AND, having hints, never reaches the "undetermined" bucket either: + * before this entry existed, an edit to + * `packages/services/service-messaging/src/objects/` — which regenerates + * that package's four bundles — printed the gate in NEITHER half of the + * output. A gate the derivation cannot mention at all is the one shape + * this script must not produce; it cost a PR a CI round. * * No per-card gate list derived from paths can ever name these, however the * derivation improves. @@ -1251,6 +1252,32 @@ export function reachesMetadataFormModule(path, modulePaths) { * baseline. The day the last one opts out, no form module can move a committed * bundle and this entry stops firing on its own. * + * ## Why there is no THIRD i18n entry, for the type-registry edge (#9144) + * + * `walkMetadataForms` has a second edge the SECOND entry above does not reach: + * `DEFAULT_METADATA_TYPE_REGISTRY` (packages/spec/src/kernel/metadata-plugin. + * zod.ts) supplies `metadataForms..label`/`.description` for EVERY + * registry entry, including form-less types, and `METADATA_FORM_REGISTRY` + * itself (packages/spec/src/system/metadata-form-registry.ts, the map, not + * the `*.form.ts` leaves it points at) decides which types get section/field + * labels at all. Editing either moves the same bundles PR #9113 paid for — + * but unlike the `.form.ts` leaves, neither file carries a filename the + * `.form.ts` convention (or any convention) distinguishes, so a KIND entry + * here would need to invent one for exactly two files. + * + * That is not the same shape as the two entries above: this is not a + * runtime-enumerated population at all, it is two SPECIFIC, KNOWN files — + * the shape `SURFACE_MODULE` and `check-type-check-coverage.mjs`'s + * `ROOT_PROGRAM_COUPLED_SCRIPT` already use. So it is closed there instead: + * `check-i18n-bundles.mjs` declares both paths as bare module-body coupling + * constants (`METADATA_TYPE_REGISTRY_MODULE` / `METADATA_FORM_REGISTRY_ + * MODULE`), which the ORDINARY path-literal derivation now reads directly off + * that gate's own source — no `CHANGE_KIND_GATES` entry, no `matches` + * function, nothing here to keep in sync. See that pair's doc comment in + * check-i18n-bundles.mjs for the full reasoning, and this file's own + * self-test for the live pins that keep the constants honest as the coupling + * they are: manual, per-file, and silently rottable if nothing watched it. + * * ## How these entries stay honest * * - Every `name` here is resolved against the families actually discovered in @@ -2359,6 +2386,32 @@ function selfTest() { // than a pair of matching strings. t('the declared shared module exists', existsSync(join(ROOT, SHARED))); + // The same shape again, for the TYPE-registry edge of walkMetadataForms + // (#9144) — two specific, known files rather than a runtime-enumerated + // population, so they are closed as coupling constants in + // check-i18n-bundles.mjs rather than a third CHANGE_KIND_GATES entry. Both + // directions pinned LIVE: delete either constant and this reddens instead + // of the derivation going silently blind on that edge again. + const TYPE_REGISTRY = 'packages/spec/src/kernel/metadata-plugin.zod.ts'; + const FORM_REGISTRY = 'packages/spec/src/system/metadata-form-registry.ts'; + const i18nGateHints = readHints('scripts/check-i18n-bundles.mjs'); + t('the i18n gate declares the type-level metadata registry module', covers(i18nGateHints, TYPE_REGISTRY)); + t('the i18n gate declares the form registry module too (not just its *.form.ts leaves)', covers(i18nGateHints, FORM_REGISTRY)); + const typeRegistryVerdict = classifyEntry({ files: ['scripts/check-i18n-bundles.mjs'], hints: i18nGateHints }, [TYPE_REGISTRY]); + const formRegistryVerdict = classifyEntry({ files: ['scripts/check-i18n-bundles.mjs'], hints: i18nGateHints }, [FORM_REGISTRY]); + t( + 'so a card editing the type registry is MATCHED through that constant, not dropped as silent', + typeRegistryVerdict.verdict === 'matched' && typeRegistryVerdict.hits[0]?.hint === TYPE_REGISTRY, + ); + t( + 'and a card editing the form registry module is MATCHED through its own constant', + formRegistryVerdict.verdict === 'matched' && formRegistryVerdict.hits[0]?.hint === FORM_REGISTRY, + ); + // Both declared paths are real files, so the four claims above are live + // rather than a pair of matching strings. + t('the declared type registry module exists', existsSync(join(ROOT, TYPE_REGISTRY))); + t('the declared form registry module exists', existsSync(join(ROOT, FORM_REGISTRY))); + // ── A family's OWN script files as match keys (#8509) ───────────────────── // // Both directions are the product, and both are pinned: a card editing a