Skip to content

Commit 97b1f07

Browse files
fix(pm): make dispatch-gates see the metadata TYPE registry's i18n coupling (#9219)
walkMetadataForms (packages/cli/src/utils/i18n-extract.ts) emits the metadataForms i18n surface from two producers: METADATA_FORM_REGISTRY (the 17 *.form.ts modules, already covered by the form-module convention entry in dispatch-gates.mjs) and DEFAULT_METADATA_TYPE_REGISTRY plus the METADATA_FORM_REGISTRY module itself (packages/spec/src/kernel/ metadata-plugin.zod.ts, packages/spec/src/system/metadata-form-registry.ts). Editing either of the latter two moves the same platform-objects bundles PR #9113 had to regenerate, and derived nothing from dispatch-gates. Both are exactly two known files, not a runtime-enumerated population, so this closes the gap the same way SURFACE_MODULE and check-type-check-coverage.mjs's ROOT_PROGRAM_COUPLED_SCRIPT already do: two bare module-body path-literal constants declared directly in check-i18n-bundles.mjs (the script check:i18n resolves to), read by the ordinary path-literal derivation with no new CHANGE_KIND_GATES entry. Pinned live in dispatch-gates.mjs's own self-test (existsSync + a matched classifyEntry verdict for each), so a renamed or moved registry module reddens that self-test instead of the derivation silently going blind on this edge again. Fixes #9144 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 75b7c24 commit 97b1f07

3 files changed

Lines changed: 108 additions & 18 deletions

File tree

scripts/check-i18n-bundles.mjs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,37 @@ import { findExtractConfigs, flagsFromDocstring } from './i18n-bundle-surface.mj
115115
*/
116116
const SURFACE_MODULE = 'scripts/i18n-bundle-surface.mjs';
117117

118+
/**
119+
* The two producers of the `metadataForms` TYPE-LEVEL surface, declared as
120+
* bare module-body path literals for the same reason as SURFACE_MODULE above
121+
* (#9144). `walkMetadataForms` (packages/cli/src/utils/i18n-extract.ts) emits
122+
* `metadataForms.<type>.label` / `.description` for every entry of
123+
* `DEFAULT_METADATA_TYPE_REGISTRY` — including form-less types like
124+
* `datasource`/`job`/`translation` — and separately reads
125+
* `METADATA_FORM_REGISTRY` itself (the map, not the `*.form.ts` leaves it
126+
* points at) to decide which types also get section/field labels. Editing
127+
* either moves the same `platform-objects` bundles PR #9113 had to
128+
* regenerate — and, unlike the `*.form.ts` leaves, neither carries a filename
129+
* SURFACE_MODULE's own convention (`isMetadataFormModulePath`) can see.
130+
*
131+
* Not folded into SURFACE_MODULE: that module documents the ONE convention it
132+
* enumerates at runtime (the `.form.ts` suffix), and these two files
133+
* deliberately do not carry it — inventing a second filename convention for
134+
* exactly two files would be the guess #9144 declined to make. A bare
135+
* coupling constant was the option the card measured to have no downside
136+
* beyond upkeep; see i18n-bundle-surface.mjs's header for the two rejected
137+
* alternatives and why.
138+
*
139+
* This is per-coupling manual upkeep, deliberately, and it does not go quiet:
140+
* dispatch-gates' own self-test pins that a card editing either path derives
141+
* check:i18n, against the real files (existsSync) — delete either constant
142+
* and that self-test reddens instead of the silence coming back. If either
143+
* module is renamed or the registries merge, update the self-test's pins in
144+
* the same change: the evidence goes with the claim, never ahead of it.
145+
*/
146+
const METADATA_TYPE_REGISTRY_MODULE = 'packages/spec/src/kernel/metadata-plugin.zod.ts';
147+
const METADATA_FORM_REGISTRY_MODULE = 'packages/spec/src/system/metadata-form-registry.ts';
148+
118149
/** The one command this gate invokes per package, as oclif topic/command parts. */
119150
const EXTRACT_COMMAND_ID = ['i18n', 'extract'];
120151
const write = process.argv.includes('--write');

scripts/i18n-bundle-surface.mjs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,23 @@
5050
* form — the convention and the population coincide, with nothing left over on
5151
* either side.
5252
*
53-
* What it deliberately does NOT cover, measured and stated so the next reader
54-
* does not mistake silence for coverage: the type-level half of the same
55-
* surface. `walkMetadataForms` in packages/cli/src/utils/i18n-extract.ts emits
53+
* What it still deliberately does NOT cover, stated so the next reader does
54+
* not mistake silence for coverage: the type-level half of the same surface.
55+
* `walkMetadataForms` in packages/cli/src/utils/i18n-extract.ts emits
5656
* `metadataForms.TYPE.label`/`.description` for every entry of
5757
* DEFAULT_METADATA_TYPE_REGISTRY (packages/spec/src/kernel/metadata-plugin.zod.ts),
58-
* and the registry module itself decides which forms are walked. Editing either
59-
* moves the same four bundles and matches no convention here, because neither
60-
* carries a filename that distinguishes it. Closing that edge needs an anchor
61-
* this module does not have, and the candidates trade off against each other
62-
* rather than being one obvious shape, so it is filed rather than guessed at:
63-
* issue 9144.
58+
* and the registry module itself (packages/spec/src/system/metadata-form-
59+
* registry.ts) decides which forms are walked. Editing either moves the same
60+
* four bundles, and neither carries a filename this module's ONE convention
61+
* (the `.form.ts` suffix) can see — inventing a second convention for exactly
62+
* two files would be a guess, not a discovery. #9144 closed that edge WITHOUT
63+
* extending this module: the two module paths are declared as bare coupling
64+
* constants directly in check-i18n-bundles.mjs (METADATA_TYPE_REGISTRY_MODULE
65+
* / METADATA_FORM_REGISTRY_MODULE, next to SURFACE_MODULE), which the
66+
* derivation reads straight off that gate's own module body — no population
67+
* to enumerate, since it is exactly two known files. See that pair's doc
68+
* comment for why here was the wrong home and dispatch-gates.mjs's self-test
69+
* for how the constants are kept honest.
6470
*/
6571

6672
import { readFileSync } from 'node:fs';

scripts/pm/dispatch-gates.mjs

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,15 +1159,16 @@ export function reachesMetadataFormModule(path, modulePaths) {
11591159
* output for every card in the tree;
11601160
* - `check:i18n` walks `packages/` at runtime for files NAMED
11611161
* `i18n-extract.config.ts` and re-extracts each owning package's bundles.
1162-
* Its source is worse than silent: the path-ish literals it does carry are
1163-
* its CLI prerequisite and stale-dist checks (`packages/cli/dist/commands/
1164-
* i18n/extract.js`, `packages/spec/dist`, measured — eleven hints, none of
1165-
* them the population). So it matches nothing AND, having hints, never
1166-
* reaches the "undetermined" bucket either: before this entry existed, an
1167-
* edit to `packages/services/service-messaging/src/objects/` — which
1168-
* regenerates that package's four bundles — printed the gate in NEITHER
1169-
* half of the output. A gate the derivation cannot mention at all is the
1170-
* one shape this script must not produce; it cost a PR a CI round.
1162+
* Its source names only three hints (measured, post-#9144): the shared
1163+
* walk module (SURFACE_MODULE) and the two metadata-registry coupling
1164+
* constants below — none of them the OWNING-PACKAGE population this entry
1165+
* answers for. So it still matches nothing on an ordinary object/field
1166+
* edit AND, having hints, never reaches the "undetermined" bucket either:
1167+
* before this entry existed, an edit to
1168+
* `packages/services/service-messaging/src/objects/` — which regenerates
1169+
* that package's four bundles — printed the gate in NEITHER half of the
1170+
* output. A gate the derivation cannot mention at all is the one shape
1171+
* this script must not produce; it cost a PR a CI round.
11711172
*
11721173
* No per-card gate list derived from paths can ever name these, however the
11731174
* derivation improves.
@@ -1251,6 +1252,32 @@ export function reachesMetadataFormModule(path, modulePaths) {
12511252
* baseline. The day the last one opts out, no form module can move a committed
12521253
* bundle and this entry stops firing on its own.
12531254
*
1255+
* ## Why there is no THIRD i18n entry, for the type-registry edge (#9144)
1256+
*
1257+
* `walkMetadataForms` has a second edge the SECOND entry above does not reach:
1258+
* `DEFAULT_METADATA_TYPE_REGISTRY` (packages/spec/src/kernel/metadata-plugin.
1259+
* zod.ts) supplies `metadataForms.<type>.label`/`.description` for EVERY
1260+
* registry entry, including form-less types, and `METADATA_FORM_REGISTRY`
1261+
* itself (packages/spec/src/system/metadata-form-registry.ts, the map, not
1262+
* the `*.form.ts` leaves it points at) decides which types get section/field
1263+
* labels at all. Editing either moves the same bundles PR #9113 paid for —
1264+
* but unlike the `.form.ts` leaves, neither file carries a filename the
1265+
* `.form.ts` convention (or any convention) distinguishes, so a KIND entry
1266+
* here would need to invent one for exactly two files.
1267+
*
1268+
* That is not the same shape as the two entries above: this is not a
1269+
* runtime-enumerated population at all, it is two SPECIFIC, KNOWN files —
1270+
* the shape `SURFACE_MODULE` and `check-type-check-coverage.mjs`'s
1271+
* `ROOT_PROGRAM_COUPLED_SCRIPT` already use. So it is closed there instead:
1272+
* `check-i18n-bundles.mjs` declares both paths as bare module-body coupling
1273+
* constants (`METADATA_TYPE_REGISTRY_MODULE` / `METADATA_FORM_REGISTRY_
1274+
* MODULE`), which the ORDINARY path-literal derivation now reads directly off
1275+
* that gate's own source — no `CHANGE_KIND_GATES` entry, no `matches`
1276+
* function, nothing here to keep in sync. See that pair's doc comment in
1277+
* check-i18n-bundles.mjs for the full reasoning, and this file's own
1278+
* self-test for the live pins that keep the constants honest as the coupling
1279+
* they are: manual, per-file, and silently rottable if nothing watched it.
1280+
*
12541281
* ## How these entries stay honest
12551282
*
12561283
* - Every `name` here is resolved against the families actually discovered in
@@ -2359,6 +2386,32 @@ function selfTest() {
23592386
// than a pair of matching strings.
23602387
t('the declared shared module exists', existsSync(join(ROOT, SHARED)));
23612388

2389+
// The same shape again, for the TYPE-registry edge of walkMetadataForms
2390+
// (#9144) — two specific, known files rather than a runtime-enumerated
2391+
// population, so they are closed as coupling constants in
2392+
// check-i18n-bundles.mjs rather than a third CHANGE_KIND_GATES entry. Both
2393+
// directions pinned LIVE: delete either constant and this reddens instead
2394+
// of the derivation going silently blind on that edge again.
2395+
const TYPE_REGISTRY = 'packages/spec/src/kernel/metadata-plugin.zod.ts';
2396+
const FORM_REGISTRY = 'packages/spec/src/system/metadata-form-registry.ts';
2397+
const i18nGateHints = readHints('scripts/check-i18n-bundles.mjs');
2398+
t('the i18n gate declares the type-level metadata registry module', covers(i18nGateHints, TYPE_REGISTRY));
2399+
t('the i18n gate declares the form registry module too (not just its *.form.ts leaves)', covers(i18nGateHints, FORM_REGISTRY));
2400+
const typeRegistryVerdict = classifyEntry({ files: ['scripts/check-i18n-bundles.mjs'], hints: i18nGateHints }, [TYPE_REGISTRY]);
2401+
const formRegistryVerdict = classifyEntry({ files: ['scripts/check-i18n-bundles.mjs'], hints: i18nGateHints }, [FORM_REGISTRY]);
2402+
t(
2403+
'so a card editing the type registry is MATCHED through that constant, not dropped as silent',
2404+
typeRegistryVerdict.verdict === 'matched' && typeRegistryVerdict.hits[0]?.hint === TYPE_REGISTRY,
2405+
);
2406+
t(
2407+
'and a card editing the form registry module is MATCHED through its own constant',
2408+
formRegistryVerdict.verdict === 'matched' && formRegistryVerdict.hits[0]?.hint === FORM_REGISTRY,
2409+
);
2410+
// Both declared paths are real files, so the four claims above are live
2411+
// rather than a pair of matching strings.
2412+
t('the declared type registry module exists', existsSync(join(ROOT, TYPE_REGISTRY)));
2413+
t('the declared form registry module exists', existsSync(join(ROOT, FORM_REGISTRY)));
2414+
23622415
// ── A family's OWN script files as match keys (#8509) ─────────────────────
23632416
//
23642417
// Both directions are the product, and both are pinned: a card editing a

0 commit comments

Comments
 (0)