doxa-maps/internal/check-element-bootstrap.mjs
Doxa Maps 5ab4af2e44 Five plugins were silently monolingual, because the bootstrap was copied sixteen times
Goal (c), and it turned into a goal (d) fix. Every profile element is
defineCustomElement(ProfileLoader, { configureApp }), and the configureApp has four
steps. The fourth, app.provide('appI18n', i18n), is the one that gets dropped.

ProfileLoader injects that instance (ProfileLoader.vue:176) to drive its locale from
profile-config. Without the provide the watcher holds null and the drive NEVER RUNS --
while setRuntimeLocale() on the next line still fires, so tStatic strings DO follow the
host. The element renders a convincing MIXTURE of the reader's language and English, and
nothing throws, because the t() shim falls back to its key. It reads as "a few strings
not translated yet".

SHIPPING THAT TODAY: country-posters, key-cities, gospel-resources, cities-internet and
my-upg-100-list. regions-plugin censused all 19 entry files independently and found the
same five and no sixth. The measurement is regions-dashboard's, from when they hit it:
a Spanish catalogue present, es set, and every t() string still English.

All sixteen entries now call library/elements/defineMapElement.js, so the five are fixed
by construction rather than by five separate edits. Behaviour is otherwise identical --
same options, same element names, same per-plugin extras. wall-of-people-groups needed
the helper to hand its pinia back, because it takes a store off its own instance and a
helper that hid pinia would have given it a second one: two stores and a silent state
split.

★ AND THE CENSUS THAT RECOMMENDED THIS SAW FOUR OF SIXTEEN. My detector hashed function
BODIES; eleven sites are not `function buildXElement()` at all, they are
`const El = defineCustomElement(...)`. I wrote "every number here is a lower bound" in
the census as a caveat and it was the headline within the hour -- a 4x undercount of the
item it recommended. So this gate greps for the SHAPE, not for a known list: a check
written around the four that matched would have passed the twelve that did not.

check-element-bootstrap has two arms. No entry may build a profile element by hand, and
the helper itself must provide appI18n -- PLANTED as a negative case, so the regression
cannot return by editing library instead of a plugin. It also fails if it finds fewer
than ten entries, because a detection that stops matching reports the same green as a
tree with no problems.

THE PLANT CAUGHT TWO BUGS IN THIS GATE BEFORE IT CAUGHT ANYTHING ELSE. The predicate
matched the helper's DOCSTRING, so it would have passed a helper that only mentions the
provide in prose -- comments are stripped now, because a check that reads comments is
reading the author's intentions rather than the program. And the plant itself used /m
instead of /gm, so it deleted the documentation and left the code, then reported that it
could not detect an absence that was not there.
2026-09-28 17:40:34 +00:00

102 lines
5.9 KiB
JavaScript

#!/usr/bin/env node
/**
* check-element-bootstrap — every plugin that ships a custom element must build it the same way.
*
* ★ THE BUG THIS EXISTS FOR WAS LIVE IN FIVE PLUGINS AT ONCE. A profile element is
* defineCustomElement(ProfileLoader, { configureApp }) and the configureApp has four steps; the fourth,
* `app.provide('appI18n', i18n)`, is the one that gets dropped. ProfileLoader injects that instance
* (ProfileLoader.vue:176) to drive its locale from profile-config, so without it the drive never runs
* and every useI18n()/t() string is stuck at the boot locale — while setRuntimeLocale() on the next line
* still fires, so tStatic strings DO follow the host. The element renders a convincing MIXTURE of the
* reader's language and English.
*
* Nothing throws, because the t() shim falls back to its key. So it reads as "a few strings not
* translated yet" and it survived in country-posters, key-cities, gospel-resources, cities-internet and
* my-upg-100-list simultaneously — confirmed independently by regions-plugin, who censused all 19 entry
* files and found the same five and no sixth.
*
* ★ AND A CENSUS THAT COUNTED FUNCTION BODIES SAW FOUR OF SIXTEEN. That is why this gate greps for the
* SHAPE (`defineCustomElement(ProfileLoader`) rather than for a known list: eleven of the sites are not
* `function buildXElement()` at all, they are `const El = defineCustomElement(...)`, and a check written
* around the four that matched would have passed the twelve that did not.
*
* TWO ARMS:
* A. no plugin entry may call defineCustomElement(ProfileLoader, …) directly — use the helper;
* B. the helper itself must provide appI18n, PLANTED as a negative case, so the regression cannot
* return by editing library instead of a plugin.
*
* Exit 0 pass · 1 fail.
*/
import { readFileSync, existsSync, readdirSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const TAG = 'ELEMENT-BOOTSTRAP'
const HELPER = 'library/elements/defineMapElement.js'
const fails = []
// Stated exemptions. An element that wraps its OWN root component rather than ProfileLoader is a
// different thing and is not in scope: it has no ProfileLoader to inject anything.
const EXEMPT = new Map([])
// ── ARM A: no entry builds a profile element by hand ─────────────────────────────────────────────
const PD = join(ROOT, 'plugin-designs')
let entries = 0, viaHelper = 0
for (const p of readdirSync(PD)) {
const f = join(PD, p, 'index.js')
if (!existsSync(f)) continue
const src = readFileSync(f, 'utf8')
const buildsProfileElement = /defineCustomElement\s*\(\s*ProfileLoader/.test(src)
const usesHelper = /defineMapElement\s*\(/.test(src)
if (!buildsProfileElement && !usesHelper) continue
entries++
if (usesHelper) viaHelper++
if (buildsProfileElement) {
if (EXEMPT.has(p)) continue
fails.push(`${p}/index.js calls defineCustomElement(ProfileLoader, …) directly.\n`
+ ` Use ${HELPER}. Building it by hand is how five plugins shipped without\n`
+ " provide('appI18n') and rendered a mixture of the reader's language and English.")
}
}
if (entries < 10) {
fails.push(`only ${entries} profile-element entries found — the detection stopped matching and this `
+ 'gate would pass while checking almost nothing.')
}
// ── ARM B: the helper provides appI18n, proved by PLANTING its absence ───────────────────────────
const helperSrc = existsSync(join(ROOT, HELPER)) ? readFileSync(join(ROOT, HELPER), 'utf8') : ''
if (!helperSrc) {
fails.push(`${HELPER} does not exist — every entry above is pointing at nothing.`)
} else {
// ★ COMMENTS STRIPPED FIRST, and this gate's own negative case is what caught the need. The helper's
// docstring QUOTES the provide line in prose, so a predicate over the raw source matched the
// documentation and reported the code present even after the code was removed — it would have passed
// a helper that only talked about providing appI18n. A check that reads comments is reading the
// author's intentions, not the program.
const code = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
const provides = (s) => /app\.provide\(\s*['"]appI18n['"]\s*,/.test(code(s))
const installs = (s) => /app\.use\(\s*i18n\s*\)|app\.use\(\s*createAppI18n\(\)\s*\)/.test(code(s))
if (!provides(helperSrc)) {
fails.push(`${HELPER} installs i18n but never provides it. ProfileLoader injects 'appI18n' to drive\n`
+ ' the locale; without the provide the element is permanently at its boot locale, silently.')
}
if (!installs(helperSrc)) fails.push(`${HELPER} does not install an i18n instance at all.`)
// THE PLANT: the same predicate, run against a bootstrap with the provide removed, MUST report it.
// /gm, not /m: the docstring quotes the line too, so removing only the FIRST match deleted the
// DOCUMENTATION and left the code — the plant then "failed to detect" a provide that was still there.
// Caught by this assertion failing on a correct helper, which is the plant doing its job on itself.
const planted = helperSrc.replace(/^.*app\.provide\(\s*['"]appI18n['"].*$/gm, '')
if (provides(planted)) {
fails.push('the check for the provide cannot detect its absence — it is measuring nothing. '
+ 'A gate that passes its own negative case is worse than no gate.')
}
}
if (fails.length) {
console.error(`${TAG}: FAIL`)
for (const f of fails) console.error(` ✗ ${f}`)
process.exit(1)
}
console.log(`${TAG}: PASS — ${viaHelper}/${entries} profile elements are built through ${HELPER}, which provides `
+ "appI18n; an element built without it is detected, so the 'permanently English' regression cannot return quietly.")