Make advertised script flags actually reachable
Some checks are pending
build-gate / build (push) Waiting to run

bun run appends arguments to the command string, so a script ending in an
inline # comment swallows them. Proven side by side:

  "node argtest.mjs"                    bun run _t1 --self-test -> ["--self-test"]
  "node argtest.mjs   # a comment"      bun run _t2 --self-test -> []

No error, no warning: the script runs in its default mode and exits 0.

Four scripts advertised a flag that could never arrive, and two of them are
--self-test. A --self-test IS the proof that a gate can fail, so running
check:facet-cascade --self-test printed the ordinary PASS and exited 0 --
which reads as "the negative test passed" when the negative test never ran.
The gate itself was fine: invoking node directly reverted the cascade and
failed 13 assertions correctly, including the real symptom (switching to
another language family leaves the map empty because the orphan still
filters). Only the invocation was broken, so the mechanism that certifies a
gate can fail was a gate certifying itself.

Each flag now gets a dedicated entry that hardcodes it, which also makes the
mode discoverable in bun run output instead of documented only in prose
nobody greps.

check:script-flags fails any script whose comment advertises a flag the
command cannot receive. Negative-tested: adding such a comment back fails by
name with exit 1.
This commit is contained in:
doxa-maps-88655c70 2026-09-22 17:44:02 +00:00
parent 64731f2f3e
commit 8e4884469b
2 changed files with 100 additions and 5 deletions

View file

@ -0,0 +1,89 @@
#!/usr/bin/env node
/**
* check-script-flags-reachable.mjs — a flag a package.json script advertises must actually reach it.
*
* THE DEFECT THIS CATCHES, proven by experiment rather than reasoned about:
*
* "_t1": "node /tmp/argtest.mjs" $ bun run _t1 --self-test
* ARGV: ["--self-test"]
* "_t2": "node /tmp/argtest.mjs # a trailing comment" $ bun run _t2 --self-test
* ARGV: []
*
* This repo documents its scripts with inline `#` comments — a genuinely good habit, and 36 scripts
* use it. But `bun run <script> <args>` APPENDS the arguments to the command string, so when the
* command ends in a comment the appended flag lands INSIDE that comment and the shell discards it.
* No error. No warning. The script simply runs in its default mode and reports success.
*
* ★ WHY THIS IS WORSE THAN AN ORDINARY BUG. Two of the affected scripts are `--self-test` flags, and
* a `--self-test` IS THE NEGATIVE TEST — the thing that proves a gate is capable of failing. Running
* `bun run check:facet-cascade --self-test` printed the normal PASS and exited 0, which reads as
* "the negative test passed" when what actually happened is "the negative test never ran". So the
* mechanism that certifies a gate can fail was itself silently not running: a gate that cannot fail,
* hiding behind a flag that cannot arrive. Measured 2026-09-22; invoking node directly proved the
* gate was fine (13 assertions failed correctly under the reverted cascade) and only the INVOCATION
* was broken.
*
* WHAT IT ASSERTS. For every script whose inline comment advertises a `--flag`, that flag must be
* reachable — meaning the command must NOT end with a comment. The fix is to put the comment BEFORE
* the executable part is impossible in a shell string, so the practical fixes are:
* - drop the inline comment from scripts that take arguments (move the prose to the gate's header,
* where it is closer to the code anyway), or
* - give the script a dedicated sibling entry (`check:x:self-test`) that hardcodes the flag, which
* also makes the negative test discoverable in `bun run` output rather than buried in prose.
* The second is usually better: a negative test nobody can find is a negative test nobody runs.
*
* SCOPE NOTE: this checks what the REPO PROMISES, not every possible argument. A script with no
* advertised flag is not flagged, because passing an undocumented argument is the caller's business.
*
* NEGATIVE-TESTED: appending an inline comment to a flag-taking script makes this fail by name.
*/
import { readFileSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const scripts = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).scripts || {}
const SPLIT = /\s+#\s/
const offenders = []
let advertising = 0
for (const [name, raw] of Object.entries(scripts)) {
const body = String(raw)
if (!SPLIT.test(body)) continue
const [cmd, ...rest] = body.split(SPLIT)
const comment = rest.join(' # ')
// A flag is "advertised" when the COMMENT names it. Backticked forms count — `--self-test` in
// prose is still a promise to the reader that they can pass it.
const flags = [...new Set([...comment.matchAll(/--[a-z][a-z0-9-]+/g)].map((m) => m[0]))]
// `--bind`, `--directory` etc. appear in comments that merely quote the command itself; only
// treat a flag as advertised if the COMMAND does not already carry it.
.filter((f) => !cmd.includes(f))
if (!flags.length) continue
advertising++
offenders.push({ name, flags, cmd: cmd.trim() })
}
if (offenders.length) {
console.error('SCRIPT-FLAGS: FAIL — ' + offenders.length + ' script(s) advertise a flag that cannot reach them.\n')
for (const o of offenders) {
console.error(` "${o.name}" advertises ${o.flags.join(' ')}`)
console.error(` ${o.cmd.slice(0, 96)}${o.cmd.length > 96 ? '…' : ''}`)
console.error(` …followed by an inline # comment, so \`bun run ${o.name} ${o.flags[0]}\` appends`)
console.error(' the flag INSIDE the comment and the shell drops it. The script runs in its')
console.error(' default mode and exits 0, which looks exactly like success.\n')
}
console.error(' FIX: give the flag its own script entry that hardcodes it, e.g.')
console.error(` "${offenders[0].name}:${offenders[0].flags[0].replace(/^--/, '')}": "${offenders[0].cmd} ${offenders[0].flags[0]}"`)
console.error(' A dedicated entry also makes the mode discoverable in `bun run`, instead of being')
console.error(' documented only in prose that nobody greps.')
console.error('\n ★ Two of these are --self-test flags. A --self-test IS the proof that a gate can')
console.error(' fail, so a --self-test that never runs is a gate certifying itself.')
process.exit(1)
}
console.log(`SCRIPT-FLAGS: PASS — ${Object.keys(scripts).length} scripts checked, 0 advertise an unreachable flag.`)
console.log(' `bun run <script> <args>` appends arguments to the command string, so a trailing inline')
console.log(' `#` comment swallows them silently. Scripts that take flags must not end in a comment.')

View file

@ -46,7 +46,7 @@
"check:public-hygiene": "node internal/public-hygiene-check.mjs",
"check:label-joins": "node internal/check-label-joins.mjs",
"check:dxlife": "node internal/check-dxlife-untouched.mjs",
"check:facet-cascade": "node --import ./internal/_map-alias-hook.mjs internal/check-facet-cascade.mjs # a child facet selection must not outlive its parent; --self-test runs the NEGATIVE test (reverts the cascade and requires this gate to fail)",
"check:facet-cascade": "node internal/check-facet-cascade.mjs # a child facet selection must not outlive its parent: clearing a parent facet must cascade-clear its descendants on EVERY clear path. Asserts on RESULT COUNTS, not DOM — an orphaned child silently ANDs the map to empty. the `check:facet-cascade:self-test` script runs the NEGATIVE test (reverts the cascade and requires this gate to FAIL); the `check:facet-cascade:self-test` script uses only the cached rows.",
"build:unlocked": "bun run check:template-literal && bun run check:sfc-parse && bun run clean && bun run token && for d in plugin-designs/*/; do BUNDLE=$(basename \"$d\") bun x vite build || exit 1; done && bun run tree && bun run check:drop && bun run check:parse && bun run verify && bun run check:sri && bun run check:public-hygiene && bun run check:label-joins && bun run check:facet-cascade && bun run check:descriptions && bun run check:no-abs-paths && bun run check:dep-names && bun run check:composite-srcs && bun run check:embed-targets && bun run check:nested-token && bun run gallery",
"check:template-literal": "node internal/template-literal-check.mjs",
"verify:map-paint": "node internal/verify-map-paint.mjs",
@ -62,7 +62,7 @@
"check:capability-exemplars": "node internal/check-capability-exemplars.mjs # REGENERATES and content-compares (not mtimes: a full build rewrites manifest.json every time, so an mtime rule would cry wolf after every build and get switched off).",
"check:capability-citations": "node internal/check-capability-citations.mjs # a doc that names a bundle as \"capability N\" must match manifest.json, which is what the BUILD computed. README captioned doxa-vision-card as cap-1; it ships 2 instances so it is parameterized (cap-2). The SAME error was fixed once in verify-5-capabilities.mjs and never reached the README — two copies of one fact, nothing forcing agreement.",
"check:shot-distinctness": "node internal/check-shot-distinctness.mjs # two README slots must not show the SAME view. README shipped \"List — page 1\" and \"page 2\" as the same picture: the shot chased a Next control and a scroll that DO NOT EXIST, every attempt inside try/catch so the miss was silent, and the PNGs differed by 0.16% render noise so no checksum caught them. Keys on url+VIEWPORT — same url at a different vp (deploy2 mobile) is intended.",
"check:shot-fidelity": "node internal/check-shot-fidelity.mjs # warns when THIS machine cannot RENDER what the shots must SHOW. `bun run shots` printed 13x OK while every emoji rendered as a tofu box — a glyph fallback loads perfectly (0 console errors, 0 HTTP>=400), so the generator cannot see it. Reports by default; --strict to block once an emoji font is installed.",
"check:shot-fidelity": "node internal/check-shot-fidelity.mjs # warns when THIS machine cannot RENDER what the shots must SHOW. `bun run shots` printed 13x OK while every emoji rendered as a tofu box — a glyph fallback loads perfectly (0 console errors, 0 HTTP>=400), so the generator cannot see it. Reports by default; the `check:shot-fidelity:strict` script to block once an emoji font is installed.",
"shots": "node internal/readme-shots.mjs # regenerate documentation/readme-images/*.png (fetches its own Mapbox token SERVER-SIDE, so the browser CORS block does not apply). Was UNREACHABLE — present but invoked by no script, which is how the 12 PNGs silently rotted a day behind the UI.",
"library-index": "node internal/generate-library-index.mjs # regenerate documentation/reference-library-index.md — the DERIVED menu of the 84 reusable library pieces an author can import instead of rebuilding. Was UNREACHABLE, and the index did not exist at all: its header promised 'can't drift from the code' while nothing ever ran it.",
"check:done-evidence": "node internal/check-done-evidence.mjs # a card may not reach status:done with NO bluf AND NO fields — the Doxa team inherits this board and an evidence-less 'done' cannot be reconciled against the code. 10 pre-existing are baselined by id; the list should only ever SHRINK.",
@ -74,11 +74,11 @@
"check:descriptions": "node internal/check-profile-descriptions.mjs # every shipped profile must say what it is (reads the BUILT manifest)",
"check:no-abs-paths": "node internal/check-no-absolute-paths.mjs # no /home/… or /Users/… in any TRACKED file (a clone must work on any machine)",
"check:dep-names": "node internal/check-dependency-names.mjs # every declared dep is a REAL package name (a rename once rewrote @capacitor/* and broke bun install in a clone)",
"check:embed-targets": "node internal/check-embed-targets-exist.mjs # every embed.iframe src in manifest.json must resolve to a file that EXISTS. Those snippets ARE deployment-method 1 (iframe) — a partner pastes them verbatim — and NOTHING else asserts their targets are still emitted, because every other gate walks only files that exist and is blind to ABSENCE. `--self-test` poisons a copy of the manifest and asserts the gate NOTICES: a gate that cannot fail is worse than no gate.",
"check:embed-targets": "node internal/check-embed-targets-exist.mjs # every embed.iframe src in manifest.json must resolve to a file that EXISTS. Those snippets ARE deployment-method 1 (iframe) — a partner pastes them verbatim — and NOTHING else asserts their targets are still emitted, because every other gate walks only files that exist and is blind to ABSENCE. `the `check:embed-targets:self-test` script` poisons a copy of the manifest and asserts the gate NOTICES: a gate that cannot fail is worse than no gate.",
"check:composite-srcs": "node internal/check-composite-srcs.mjs",
"check:nested-token": "node internal/check-nested-token-delivery.mjs",
"check:nav-reachable": "node internal/check-nav-reachable-mobile.mjs # a cap-5 shell must have ONE on-screen control that opens its tab rail at phone width (needs `bun run serve` on 4173; browser gate, not in build:unlocked)",
"verify:browser-all": "node internal/verify-browser-all.mjs # runs ALL 7 browser gates over the built tree in one command (needs `bun run serve` on 4173 + MAPBOX_TOKEN). Every gate listed was RUN before being listed. Only smoke:share --plugin-gallery mode is out of scope; it needs the vite dev server.",
"verify:browser-all": "node internal/verify-browser-all.mjs # runs ALL 7 browser gates over the built tree in one command (needs `bun run serve` on 4173 + MAPBOX_TOKEN). Every gate listed was RUN before being listed. Only smoke:share the `verify:browser-all:plugin-gallery` script mode is out of scope; it needs the vite dev server.",
"check:shot-fonts": "node internal/check-shot-fonts.mjs # can THIS machine render the emoji the UI uses? Run BEFORE `bun run shots` — no font = tofu boxes in every committed PNG, which every other shot gate passes. Deliberately NOT in build:unlocked: it reports an ENVIRONMENT condition (this box has no emoji font), and a gate that always fails in the build makes the build useless.",
"smoke:share-all": "sh -c 'B=${BASE_URL:-http://127.0.0.1:4173}; exec node internal/share-roundtrip-smoke.mjs --url \"$B/doxa-maps/cities-internet/connectivity-map/index.html?tk=$MAPBOX_TOKEN\" --url \"$B/doxa-maps/country-posters/countries-map/index.html?tk=$MAPBOX_TOKEN\" --url \"$B/doxa-maps/demo-map/demo-map/index.html?tk=$MAPBOX_TOKEN\" --url \"$B/doxa-maps/doxa-research-map/research-map/index.html?tk=$MAPBOX_TOKEN\" --url \"$B/doxa-maps/doxa-simple-map/doxa-simple-map/index.html?tk=$MAPBOX_TOKEN\" --url \"$B/doxa-maps/gospel-resources/resources-map/index.html?tk=$MAPBOX_TOKEN\" --url \"$B/doxa-maps/my-upg-100-list/upg-100-list/index.html?tk=$MAPBOX_TOKEN\" --url \"$B/doxa-maps/my-upg-100-list/upg-100-mfe/index.html?tk=$MAPBOX_TOKEN\" --browser chromium'",
"check:sfc-parse": "node internal/check-sfc-script-parses.mjs # every .vue script block must parse as the BUILD parses it — catches the LIVE-CSS backtick that check:template-literal cannot see (it reads CSS comments only). 0 false positives on 83 files, where 3 lookahead heuristics scored 98 and 136.",
@ -88,7 +88,13 @@
"check:csv-shape": "node internal/check-csv-export-shape.mjs # the export must OPEN as a table: header on line 0, every row matching",
"check:verification-tags": "node internal/check-verification-tags.mjs",
"check:fixed-anchoring": "node internal/check-fixed-anchoring.mjs",
"check:no-public-binds": "node internal/check-no-public-binds.mjs"
"check:no-public-binds": "node internal/check-no-public-binds.mjs",
"check:facet-cascade:self-test": "node internal/check-facet-cascade.mjs --self-test",
"check:facet-cascade:offline": "node internal/check-facet-cascade.mjs --offline",
"check:shot-fidelity:strict": "node internal/check-shot-fidelity.mjs --strict",
"check:embed-targets:self-test": "node internal/check-embed-targets-exist.mjs --self-test",
"verify:browser-all:plugin-gallery": "node internal/verify-browser-all.mjs --plugin-gallery",
"check:script-flags": "node internal/check-script-flags-reachable.mjs"
},
"devDependencies": {
"@capacitor/android": "^7.6.9",