check:no-orphan-gates sees gate FILES outside internal/: a plugin's own gate that nothing runs fails

The file walk read internal/ only, and the chain arm counts package.json NAMES, so a plugin-local
check-*.mjs that nobody registered was invisible to both: on 2026-09-29 six were run by nothing
(dx-shepherd's census; exp's four have since been registered). Every tracked gate-shaped file under
plugin-designs/ or library/ must now be run by PATH from a script or another file's code, carry
@one-shot, or sit in the shrink-only `files` baseline for its owner.

Two rules keep it honest. Comments are stripped before searching, since a file mentioned in prose is
not run by it. And a basename counts only where no other gate file shares it:
library/scripts/research/verify-map-paint.mjs (199 lines, run by nothing) shares its name with
internal/verify-map-paint.mjs (342 lines, what verify:map-paint runs), so a name match let one file's
registration vouch for the other. Self-tested on planted cases (unregistered, comment-only, shared
basename). On the real tree it finds 4, now baselined for their owners:
  library/scripts/research/verify-map-paint.mjs            likely a stale copy
  plugin-designs/doxa-research-map/scripts/check-reduced-motion-jumps.mjs
  plugin-designs/gospel-resources/scripts/verify-translator-org-links.mjs   a data tool? @one-shot or register
  plugin-designs/research-map-plus-grid/scripts/check-ui-strings-have-keys.mjs
The chain arm's writer now keeps every key of the shared baseline file.
This commit is contained in:
Doxa Maps 2026-09-29 13:57:09 +00:00
parent dbee30e827
commit 12078103d1
3 changed files with 80 additions and 3 deletions

View file

@ -30,11 +30,14 @@
* WHAT IT ASSERTS: every internal/*.mjs whose name or header marks it as a gate (check-*, verify-*,
* smoke-*, or a header containing "GATE") is either reachable or explicitly tagged @one-shot; and every
* check:/verify:/smoke: SCRIPT is run by a standing chain (by exact name), declared on demand with a
* reason, or in the shrink-only baseline of 2026-09-29's orphans (the second arm, below).
* reason, or in the shrink-only baseline of 2026-09-29's orphans (the second arm, below); and every
* gate-shaped FILE under plugin-designs/ or library/ is run by path from a script or another file's code
* (comments stripped), tagged @one-shot, or in that baseline's `files` list.
*
* node internal/check-no-orphan-gates.mjs check (the chain arm's self-test runs first, every time)
* node internal/check-no-orphan-gates.mjs --shrink drop baselined scripts since chained, declared or deleted
* node internal/check-no-orphan-gates.mjs --init write the orphan baseline, only if none exists
* node internal/check-no-orphan-gates.mjs --init-files add the `files` list, only if the baseline has none
*
* NEGATIVE-TESTED: removing the check:shell-scroll entry from package.json makes this fail by name.
*/
@ -113,6 +116,74 @@ if (orphans.length) {
process.exit(1)
}
// ── THE SAME QUESTION FOR GATE FILES OUTSIDE internal/ (2026-09-29) ─────────────────────────
// The walk above reads internal/ only, so a plugin's own gate that nobody registered was invisible:
// on 2026-09-29 six plugin-local check-*.mjs files were referenced by nothing. Every TRACKED file under
// plugin-designs/ or library/ named check-/verify-/smoke-*.mjs must be run by PATH from a package.json
// script, or referenced from another file's CODE, or carry @one-shot, or sit in the shrink-only baseline
// (orphan-checks-baseline.json `files`) for its owner.
// ⚠ PATHS, AND CODE ONLY. A basename counts only where no other gate file shares it:
// library/scripts/research/verify-map-paint.mjs shares its name with internal/verify-map-paint.mjs, which
// is what verify:map-paint runs, so a name match let one registration vouch for a different file. And
// comments are stripped first: a file mentioned in prose is not run by it (the lesson recorded above).
export function outerOrphans({ gateFiles, scriptCmds, codeByFile, oneShot = new Set(), baseline = [] }) {
const base = (p) => p.slice(p.lastIndexOf('/') + 1)
const count = new Map()
for (const p of gateFiles) count.set(base(p), (count.get(base(p)) || 0) + 1)
const out = []
for (const p of gateFiles) {
if (!/^(plugin-designs|library)\//.test(p) || oneShot.has(p)) continue
const unique = count.get(base(p)) === 1
const hit = (text) => text.includes(p) || (unique && text.includes(base(p)))
if (hit(scriptCmds)) continue
if ([...codeByFile].some(([f, code]) => f !== p && hit(code))) continue
out.push(p)
}
const bl = new Set(baseline)
return { unreached: out, fresh: out.filter((p) => !bl.has(p)), gone: [...bl].filter((p) => !out.includes(p)) }
}
const stripComments = (s) => s.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:'"`\\])\/\/[^\n]*/g, '$1')
{
// Self-test on planted cases: each rule must be able to fail.
const files = ['plugin-designs/a/scripts/check-x.mjs', 'plugin-designs/b/scripts/check-y.mjs', 'internal/verify-z.mjs', 'library/s/verify-z.mjs']
const run = (scripts, code) => outerOrphans({ gateFiles: files, scriptCmds: scripts, codeByFile: new Map(code) }).fresh
const allWired = 'node plugin-designs/a/scripts/check-x.mjs && node plugin-designs/b/scripts/check-y.mjs && node library/s/verify-z.mjs'
const cases = [
['everything registered by path passes', run(allWired, []), 0],
['a plugin gate nobody runs fails', run('node plugin-designs/a/scripts/check-x.mjs && node library/s/verify-z.mjs', []), 1],
['a gate run from another file\'s code passes', run('node plugin-designs/a/scripts/check-x.mjs && node library/s/verify-z.mjs', [['internal/all.mjs', "spawn('node', ['plugin-designs/b/scripts/check-y.mjs'])"]]), 0],
['a gate only MENTIONED in a comment fails', run('node plugin-designs/a/scripts/check-x.mjs && node library/s/verify-z.mjs', [['internal/all.mjs', stripComments('// see check-y.mjs')]]), 1],
['a shared basename cannot vouch for the other file', run('node plugin-designs/a/scripts/check-x.mjs && node plugin-designs/b/scripts/check-y.mjs && node internal/verify-z.mjs', []), 1],
]
const bad = cases.filter(([, got, want]) => (want === 0) !== (got.length === 0))
if (bad.length) { for (const [n, got] of bad) console.error(` ✗ self-test: ${n} -> ${JSON.stringify(got)}`); console.error('NO-ORPHAN-GATES: SELF-TEST FAILED — the file arm cannot be trusted'); process.exit(1) }
}
{
const gateFiles = execSync('git ls-files -- internal plugin-designs library', { cwd: ROOT, encoding: 'utf8' }).split('\n')
.filter((p) => /\/(check|verify|smoke)-[^/]*\.mjs$/.test(p) && !p.includes('/data/'))
const scriptCmds = Object.values(JSON.parse(pkgRaw).scripts || {}).join('\n')
const codeFiles = execSync('git ls-files -- "*.mjs" "*.js" "*.vue"', { cwd: ROOT, encoding: 'utf8' }).split('\n').filter((p) => p && !p.includes('node_modules'))
const codeByFile = new Map(codeFiles.map((p) => { try { return [p, stripComments(readFileSync(join(ROOT, p), 'utf8'))] } catch { return [p, ''] } }))
const oneShot = new Set(gateFiles.filter((p) => { try { return /^\s*(?:\*|\/\/)\s*@one-shot\b/m.test(readFileSync(join(ROOT, p), 'utf8').slice(0, 2600)) } catch { return false } }))
const ORPHANS_REL_F = 'internal/fixtures/orphan-checks-baseline.json'
const bl = (() => { try { return JSON.parse(readFileSync(join(ROOT, ORPHANS_REL_F), 'utf8')) } catch { return null } })()
const v = outerOrphans({ gateFiles, scriptCmds, codeByFile, oneShot, baseline: bl?.files || [] })
if (process.argv.includes('--init-files') || process.argv.includes('--shrink')) {
if (bl && (process.argv.includes('--shrink') || !bl.files)) {
const files = process.argv.includes('--init-files') && !bl.files ? v.unreached : (bl.files || []).filter((p) => !v.gone.includes(p))
writeFileSync(join(ROOT, ORPHANS_REL_F), JSON.stringify({ ...bl, files: files.sort() }, null, 1) + '\n')
console.log(`NO-ORPHAN-GATES: file baseline ${process.argv.includes('--init-files') ? 'written' : 'shrunk'}: ${files.length} file(s)`)
}
}
console.log(` gate files : ${gateFiles.length} tracked; outside internal/, ${v.unreached.length} run by nothing (${(bl?.files || []).length - v.gone.length} baselined for their owners)`)
if (v.fresh.length && !process.argv.includes('--init-files')) {
console.error(`NO-ORPHAN-GATES: FAIL — ${v.fresh.length} gate file(s) outside internal/ that nothing runs:`)
for (const p of v.fresh) console.error(` ✗ ${p}`)
console.error(' Register it by PATH in a package.json script (and chain it), or tag @one-shot if it was a one-bug diagnostic.')
process.exit(1)
}
}
// ── SECOND, DISTINCT QUESTION: does it RUN? ────────────────────────────────
// Reachable and wired are not the same property, and conflating them is how 70 of
// 100 check:* scripts came to sit outside every gate wave without anyone noticing —
@ -215,7 +286,7 @@ if (process.argv.includes('--init') || process.argv.includes('--shrink')) {
const init = process.argv.includes('--init')
if (init && orphanBase) { console.error(`NO-ORPHAN-GATES: --init refused, ${ORPHANS_REL} exists (use --shrink)`); process.exit(1) }
const keep = init ? v.unreached.filter((k) => !(k in onDemand)) : orphanBase.unreached.filter((k) => !v.gone.includes(k))
writeFileSync(join(ROOT, ORPHANS_REL), JSON.stringify({ about: 'check/verify/smoke scripts that no standing chain ran on 2026-09-29 and that nobody had declared on-demand. Each owner chains it, declares it in on-demand-checks.json with a reason, or deletes it. This list only shrinks (--shrink).', unreached: keep.sort() }, null, 1) + '\n')
writeFileSync(join(ROOT, ORPHANS_REL), JSON.stringify({ ...(orphanBase || {}), about: 'check/verify/smoke scripts that no standing chain ran on 2026-09-29 and that nobody had declared on-demand. Each owner chains it, declares it in on-demand-checks.json with a reason, or deletes it. This list only shrinks (--shrink).', unreached: keep.sort() }, null, 1) + '\n')
console.log(`NO-ORPHAN-GATES: orphan baseline ${init ? 'written' : 'shrunk'}: ${keep.length} script(s)`)
process.exit(0)
}

View file

@ -105,5 +105,11 @@
"verify:interop",
"verify:interop:self-test",
"verify:interop:self-test:camera-freeze"
],
"files": [
"library/scripts/research/verify-map-paint.mjs",
"plugin-designs/doxa-research-map/scripts/check-reduced-motion-jumps.mjs",
"plugin-designs/gospel-resources/scripts/verify-translator-org-links.mjs",
"plugin-designs/research-map-plus-grid/scripts/check-ui-strings-have-keys.mjs"
]
}

View file

@ -399,7 +399,7 @@
"check:approx-pin-note": "people-group pins say they are localities, not towns or boundaries, in ONE shared wording (library/components/legends/ApproxPinNote.vue): the colocation index finds groups sharing a coordinate, the note names them only for a shared point, never prints a raw coordinate, and renders from each locale's catalogue. Headless.",
"check:auto-engine": "BROWSER. The default engine (auto) is MapLibre, token or not: on one map leaf, no token AND a host token alone both draw with MapLibre, paint, and make zero api.mapbox.com requests; ?provider=mapbox with that token draws Mapbox and carries it. In gates:post, NOT MEASURED outside the browser slot.",
"check:no-new-driver-references": "shipped text (library, plugins, internal, docs, skills, README) neither quotes nor cites the Driver: a line that refers to the Driver and is not in internal/fixtures/driver-references-baseline.json FAILS. Forward-only: the baseline, keyed by file and line text with counts (not line numbers), may only shrink as owners sweep their areas (--shrink tidies it; it never grows). Matches the role word only, not a GPU or device driver. Node only.",
"check:no-orphan-gates": "every gate-shaped internal/*.mjs is reachable or tagged @one-shot; and every check:/verify:/smoke: script is run by a standing chain, matched by EXACT name (gates:pre, gates:post, the browser suite GATES, the build, and what those bun run), or declared in internal/fixtures/on-demand-checks.json with its reason, or in the shrink-only baseline of the 2026-09-29 orphans. A chained :self-test does not chain its gate. --shrink / --init maintain the baseline.",
"check:no-orphan-gates": "every gate-shaped internal/*.mjs is reachable or tagged @one-shot; and every check:/verify:/smoke: script is run by a standing chain, matched by EXACT name (gates:pre, gates:post, the browser suite GATES, the build, and what those bun run), or declared in internal/fixtures/on-demand-checks.json with its reason, or in the shrink-only baseline of the 2026-09-29 orphans. A chained :self-test does not chain its gate. --shrink / --init maintain the baseline. And every gate-shaped FILE under plugin-designs/ or library/ is run by PATH from a script or another file's code (comments stripped; a basename counts only when no other gate file shares it), tagged @one-shot, or in the baseline's `files` list.",
"check:wall-wysiwyg": "the Wall's screen IS its poster: the Bless view's on-screen poster and its print come from one layout, so for every preset (3x4ft, 2x3ft, 5x8ft) the sheets, the 2,107 card rects, the card text, the verse reference and the chrome agree. Two-sided on planted layout defects. Headless.",
"check:wall-print-watch": "the Wall's HD poster print never prints silently incomplete: with fake timers and a fake frame, the watch counts photos as they load, counts the ones that could not, and reports both. Two-sided on planted printWatch copies. Headless.",
"check:wall-no-bg-photos": "the Wall's store downloads no photos on its own (it used to test all 932, 132 MB, on every visit): the REAL store runs through Vite against the committed fixtures/pray-list-sample.json with fetch stubbed and every Image request counted. Two-sided with the old background test planted. Headless, no network.",