From 9cae7fe16a3fde8b5f29bde22d0d374469ed6d47 Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:09:46 +0000 Subject: [PATCH] Compare every silently-failing side effect, not just strategy keys The gate caught one of the four things a baked composite must repeat. The other three were left to luck, and one of them had already gone wrong: the composite shipped mapbox-gl-rtl-text zero times while both leaf bundles shipped it once, so Arabic, Hebrew and Persian labels rendered reversed in the cap-5 shell. Arab World has 99 rows and Persian-Median 49, which makes that a wrong name rather than a style issue. Now compares createPinia, createAppI18n and installRtlTextPlugin alongside the registerStrategies keys: whatever the leaf entry calls, the composite that bakes its .vue must call too. provide('profileModules') is deliberately excluded. It is the fifth side effect and the only one that throws when missing, and it is also the only one safe to omit, because a baked composite does not use ProfileLoader. Requiring it would fail a composite that is correct. The rule is "repeat what silently disappears", not "repeat everything". Matched as calls -- name followed by "(" with the import line for that name stripped first -- because an indexOf on the bare name reads the import instead of the invocation, which is the trap that made the strategy parser return nothing and report a confident empty result. Negative-tested twice, and the first case reproduces the real historical bug: removing installRtlTextPlugin() fails by name and prints both sides' lists; removing createPinia() does the same. Pairs compared went 1 to 2. --- internal/check-composite-registration.mjs | 79 ++++++++++++++++++++--- 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/internal/check-composite-registration.mjs b/internal/check-composite-registration.mjs index d2610227c..4cde71ac6 100644 --- a/internal/check-composite-registration.mjs +++ b/internal/check-composite-registration.mjs @@ -118,6 +118,43 @@ const bundles = readdirSync(DESIGNS, { withFileTypes: true }) .filter((e) => e.isDirectory() && !e.name.startsWith('.')) .map((e) => e.name) +/** + * ★ THE SIDE EFFECTS A BAKED COMPOSITE MUST REPEAT, beyond the strategy keys. + * + * An entry file does five things. FOUR OF THEM FAIL SILENTLY if a composite forgets them, which is + * why counting on "it renders" is no evidence the entry's work was done: + * + * createPinia() silent — state simply is not shared + * createAppI18n() silent — an empty SyntaxError, and the subtree vanishes + * registerStrategies() silent — getColorStrategy() FALLS BACK instead of throwing, so the + * charts paint grey and read as a data problem + * installRtlTextPlugin() silent — Arabic/Hebrew/Persian labels render REVERSED. Measured: the + * composite shipped mapbox-gl-rtl-text 0 times while both leaf + * bundles shipped it once. Arab World (99 rows) and + * Persian-Median (49) are in scope, so that is a WRONG NAME. + * provide('profileModules') LOUD — an explicit throw, and the ONLY one safe to omit, because a + * baked composite does not use ProfileLoader.vue at all. + * + * So `provide` is deliberately NOT in this list: requiring it would fail a composite that is + * correct. The rule is "repeat what silently disappears", not "repeat everything". + * + * Matched as CALLS (name followed by `(`) so an import line does not count as a use — the same trap + * that made the strategy parser read the import instead of the invocation. + */ +const SILENT_SIDE_EFFECTS = ['createPinia', 'createAppI18n', 'installRtlTextPlugin'] + +function calledEffects(file) { + if (!existsSync(file)) return null + const src = readFileSync(file, 'utf8') + const out = new Set() + for (const name of SILENT_SIDE_EFFECTS) { + // Strip the import line for THIS name, then look for a real call. + const withoutImport = src.replace(new RegExp(`^import[^\\n]*\\b${name}\\b[^\\n]*$`, 'gm'), '') + if (new RegExp(`\\b${name}\\s*\\(`).test(withoutImport)) out.add(name) + } + return out +} + const errs = [] let comparisons = 0 @@ -137,17 +174,41 @@ for (const b of bundles) { const mine = strategyKeys(join(dir, 'index.js')) if (mine === null) continue // no entry file of its own; nothing to compare + const myEffects = calledEffects(join(dir, 'index.js')) || new Set() + for (const s of siblings) { const theirs = strategyKeys(join(DESIGNS, s, 'index.js')) - if (theirs === null || !theirs.size) continue // sibling registers nothing -> nothing to inherit - comparisons++ - const missing = [...theirs].filter((k) => !mine.has(k)) - if (missing.length) { - errs.push( - `${b} bakes ${s}/${s}.vue but does not register: ${missing.join(', ')}\n` + - ` ${s}/index.js registers [${[...theirs].join(', ')}]\n` + - ` ${b}/index.js registers [${[...mine].join(', ')} ]` - ) + const theirEffects = calledEffects(join(DESIGNS, s, 'index.js')) || new Set() + if (theirs === null) continue // sibling has no entry file; nothing to inherit + + // 1 — strategy keys + if (theirs.size) { + comparisons++ + const missing = [...theirs].filter((k) => !mine.has(k)) + if (missing.length) { + errs.push( + `${b} bakes ${s}/${s}.vue but does not register: ${missing.join(', ')}\n` + + ` ${s}/index.js registers [${[...theirs].join(', ')}]\n` + + ` ${b}/index.js registers [${[...mine].join(', ')} ]` + ) + } + } + + // 2 — the silent side effects. Same rule, wider surface: whatever the LEAF entry calls and + // whose absence would not announce itself, the composite must call too. + if (theirEffects.size) { + comparisons++ + const missingFx = [...theirEffects].filter((k) => !myEffects.has(k)) + if (missingFx.length) { + errs.push( + `${b} bakes ${s}/${s}.vue but never calls: ${missingFx.map((f) => f + '()').join(', ')}\n` + + ` ${s}/index.js calls [${[...theirEffects].join(', ')}]\n` + + ` ${b}/index.js calls [${[...myEffects].join(', ')} ]\n` + + ' Each of these fails SILENTLY when missing — no error, no warning, just a wrong\n' + + ' picture. installRtlTextPlugin was absent exactly this way and reversed every\n' + + ' Arabic, Hebrew and Persian label in the cap-5 shell.' + ) + } } } }