gate the docs' quoted scripts block against package.json
README-SECTION-3 reproduces the scripts block verbatim and carries its own plea to re-transcribe after every package.json edit. It drifted anyway, twice: cap:sync still showed the bunx form that does not exist here, and build:unlocked was missing check:public-hygiene and check:label-joins. The second is the dangerous shape. An omission in a quoted list has no wrong character to notice, so a reader auditing what the build verifies concludes the secret scan is not in the chain. The warning was already there and did not work, because re-transcribing by hand is what gets skipped when the edit felt unrelated. Blocking in CI: it diffs two files already in the checkout, so a failure is always real. Negative-tested both ways, including a matcher that matches too little failing rather than passing vacuously.
This commit is contained in:
parent
2fa4bf992a
commit
a853081e3f
5 changed files with 133 additions and 2 deletions
|
|
@ -155,6 +155,15 @@ jobs:
|
|||
continue-on-error: true
|
||||
run: bun run check:shots
|
||||
|
||||
# BLOCKING, unlike the three report-only steps above: this compares two files that
|
||||
# are both already in the checkout, needs no network, no browser and no toolchain,
|
||||
# and is fully deterministic — so a failure is always a real defect, never runner
|
||||
# flake. The doc it guards QUOTES package.json verbatim and had silently drifted
|
||||
# TWICE, most recently hiding check:public-hygiene (the secret scan) from the
|
||||
# build chain a reader believes they are auditing.
|
||||
- name: Assert the docs' quoted scripts still match package.json
|
||||
run: bun run check:quoted-scripts
|
||||
|
||||
- name: Assert no local credential is being published
|
||||
run: |
|
||||
if [ -e public/token.local.js ]; then
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ The scripts these commands are grounded in (from this branch's `package.json`):
|
|||
// claiming to be ground truth, and this one had silently stopped being it.
|
||||
"dev": "bun x vite",
|
||||
"build": "bash internal/build-lock.sh",
|
||||
"build:unlocked": "bun run check:template-literal && 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 gallery",
|
||||
"build:unlocked": "bun run check:template-literal && 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 gallery",
|
||||
"preview": "bun x vite preview",
|
||||
"serve": "python3 -m http.server 4173 --bind 0.0.0.0 --directory public",
|
||||
"cap:sync": "bunx cap sync android",
|
||||
"cap:sync": "./node_modules/.bin/cap sync android",
|
||||
"cap:verify": "test -f capacitor/android/app/src/main/assets/public/index.html && echo \"OK webDir copied\" || (echo \"FAIL: empty assets/public -> blank white screen. Check webDir.\" && exit 1)",
|
||||
"apk:debug": "bun run build && bun run cap:sync && bun run cap:verify && cd capacitor/android && ./gradlew assembleDebug",
|
||||
"apk:install": "adb install -r capacitor/android/app/build/outputs/apk/debug/app-debug.apk",
|
||||
|
|
|
|||
|
|
@ -220,6 +220,16 @@ so running it inside the build would make it unfalsifiable. Run them before publ
|
|||
| `bun run check:done-evidence` | a backlog card marked `done` with no bluf **and** no fields — a status word the next team cannot reconcile against the code | reads `_qa-cycle/`, which is not in any repo CI clones |
|
||||
| `bun run verify:gate1` | the Gate-1 chain end-to-end against LIVE data: header filter → composite props → real grid re-filter → count back up | needs network; it is an acceptance test, not a build step |
|
||||
|
||||
**`bun run check:quoted-scripts` is the exception that proves the rule above** — it is the one
|
||||
doc-integrity gate that DOES run in CI (blocking), because it compares two files already in the
|
||||
checkout: it needs no browser, no network, and no mtime. It asserts that the `scripts` block
|
||||
quoted verbatim in `docs/README-SECTION-3-cli-commands.md` still matches `package.json`. That doc
|
||||
carries its own hand-written plea to re-transcribe after every edit, and drifted anyway — twice.
|
||||
The second drift omitted `check:public-hygiene` (the secret scan) from the build chain it
|
||||
documents, which is the dangerous shape: **an omission in a quoted list is invisible**, since
|
||||
there is no wrong character to notice. `package.json` is what runs, so the fix is always to update
|
||||
the doc.
|
||||
|
||||
`bun run verify:5-caps` proves all five capability tiers mount and render. It needs a token —
|
||||
`MAPBOX_TOKEN=$(curl -s <token-endpoint>) bun run verify:5-caps` — and it sets **both** `?tk=`
|
||||
(the top-level element) **and** `window.MAP_TOKEN` (a composite's nested children read the global,
|
||||
|
|
|
|||
111
internal/check-quoted-scripts.mjs
Normal file
111
internal/check-quoted-scripts.mjs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-quoted-scripts.mjs — a doc that QUOTES package.json must still match package.json.
|
||||
*
|
||||
* THE BUG THIS EXISTS TO PREVENT (real, twice)
|
||||
* docs/README-SECTION-3-cli-commands.md reproduces the `scripts` block verbatim so a reader can
|
||||
* see every command without opening package.json. It carries its own warning two lines above the
|
||||
* block: "If you edit package.json, re-transcribe this block — a doc that QUOTES a config is
|
||||
* claiming to be ground truth, and this one had silently stopped being it."
|
||||
*
|
||||
* It then silently stopped being it AGAIN. Two entries had drifted on 2026-09-21:
|
||||
* - `cap:sync` still showed `bunx cap sync android` — the exact command that does not exist in
|
||||
* this environment (exit 127) and that package.json had ALREADY been fixed away from.
|
||||
* - `build:unlocked` was missing `check:public-hygiene` and `check:label-joins`. That is the
|
||||
* worst shape of this defect: a reader auditing what the build verifies would conclude the
|
||||
* SECRET-SCAN is not in the chain. An omission in a quoted list is invisible — there is no
|
||||
* wrong character to notice, just a step that is not there.
|
||||
*
|
||||
* ★ WHY A GATE AND NOT A BETTER WARNING. The file already had the warning, written by someone who
|
||||
* had just been burned, and the warning did not work — because re-transcribing by hand is exactly
|
||||
* the kind of mechanical task a human or an agent skips when the edit felt unrelated. The rule was
|
||||
* enforced only by remembering. This is the same failure the library-index gate was built for:
|
||||
* a documented invariant with no mechanism behind it is a wish.
|
||||
*
|
||||
* WHY COMPARE AGAINST package.json AND NOT A SNAPSHOT
|
||||
* package.json is the thing that RUNS. The doc is the copy. So the authority is unambiguous and
|
||||
* the fix direction is always "update the doc" — never the reverse.
|
||||
*
|
||||
* WHAT IT TOLERATES DELIBERATELY
|
||||
* - Trailing ` # comment` in a package.json body: several scripts carry an inline explanation
|
||||
* that the doc block reasonably omits. Compared against the code half only.
|
||||
* - JSON escaping (\" in the doc's fenced block) and a trailing comma.
|
||||
* - Entries the doc chooses NOT to quote: this checks the ones it DOES quote, so the doc can
|
||||
* stay a curated subset. It reports the count so shrinkage is visible.
|
||||
*
|
||||
* ★ THE MATCHER MUST NOT SILENTLY MATCH NOTHING. The first version of this regex required each
|
||||
* line to END in a quote (`"$`). Nine of the ten entries end in `",` — a trailing comma — so it
|
||||
* matched exactly ONE (`phone`, the last entry, the only one without a comma) and reported
|
||||
* "0 drifted". That is a pass that means nothing, and it looks identical to a real clean run.
|
||||
* (My first guess at the cause was "the block is indented"; it is NOT indented — checking rather
|
||||
* than assuming is what found the trailing comma. A plausible explanation for a broken measurement
|
||||
* is still a fabricated one until verified.)
|
||||
* Hence MIN_EXPECTED below: too few matches is itself a FAILURE, not a quiet success.
|
||||
*
|
||||
* NEGATIVE-TESTED: reverting `cap:sync` in the doc to `bunx cap sync android` makes this FAIL
|
||||
* naming that entry and showing both sides; restoring it returns PASS. Breaking the regex so it
|
||||
* matches < MIN_EXPECTED also FAILs rather than passing vacuously.
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const PKG = join(ROOT, 'package.json')
|
||||
const DOC = join(ROOT, 'docs', 'README-SECTION-3-cli-commands.md')
|
||||
|
||||
// The doc quoted 10 entries when this gate was written. If the matcher suddenly finds far fewer,
|
||||
// the matcher is broken, not the doc.
|
||||
const MIN_EXPECTED = 6
|
||||
|
||||
if (!existsSync(DOC)) {
|
||||
console.log('QUOTED-SCRIPTS: SKIP — docs/README-SECTION-3-cli-commands.md not present.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const scripts = JSON.parse(readFileSync(PKG, 'utf8')).scripts || {}
|
||||
const doc = readFileSync(DOC, 'utf8')
|
||||
|
||||
// Indent-tolerant: the block is inside a fenced code block and is indented.
|
||||
const RE = /^[ \t]*"([a-z0-9:_-]+)":\s+"(.*?)",?[ \t]*$/gm
|
||||
|
||||
const drift = [], unknown = []
|
||||
let quoted = 0
|
||||
|
||||
for (const m of doc.matchAll(RE)) {
|
||||
const name = m[1]
|
||||
const shown = m[2].replace(/\\"/g, '"').replace(/,$/, '').trim()
|
||||
if (!(name in scripts)) { unknown.push(name); continue }
|
||||
quoted++
|
||||
const real = scripts[name]
|
||||
const realCode = real.split(/\s{2,}#/)[0].trim() // drop an inline ` # …` explanation
|
||||
if (shown !== real.trim() && shown !== realCode) drift.push({ name, shown, real: realCode })
|
||||
}
|
||||
|
||||
console.log(`QUOTED-SCRIPTS: ${quoted} quoted entr(ies) compared against package.json.`)
|
||||
|
||||
if (quoted < MIN_EXPECTED) {
|
||||
console.error(`\nQUOTED-SCRIPTS: FAIL — only ${quoted} entries matched (expected >= ${MIN_EXPECTED}).`)
|
||||
console.error(' The MATCHER is broken, not the doc. A checker that matches almost nothing')
|
||||
console.error(' reports "0 drifted" and looks exactly like a clean pass — fix the regex in')
|
||||
console.error(' internal/check-quoted-scripts.mjs before trusting this result.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (unknown.length) {
|
||||
console.log(` i quoted but not in package.json (ignored): ${unknown.join(', ')}`)
|
||||
}
|
||||
|
||||
if (drift.length) {
|
||||
console.error(`\nQUOTED-SCRIPTS: FAIL — ${drift.length} quoted script(s) no longer match package.json:`)
|
||||
for (const d of drift) {
|
||||
console.error(`\n ✗ ${d.name}`)
|
||||
console.error(` doc : ${d.shown}`)
|
||||
console.error(` real : ${d.real}`)
|
||||
}
|
||||
console.error('\n package.json is what RUNS; the doc is the copy — update the DOC to match.')
|
||||
console.error(' An omitted step in a quoted list is invisible: there is no wrong character to')
|
||||
console.error(' notice, just a check the reader believes is not in the chain.')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('QUOTED-SCRIPTS: PASS — every quoted script matches package.json.')
|
||||
|
|
@ -51,6 +51,7 @@
|
|||
"check:composite": "node internal/check-composite-freshness.mjs # a COMPOSITE is a COPY taken at its own build time: fails if a nested child's source is newer than the parent's bundle. NOT in build:unlocked on purpose \u2014 there everything was just built, so it could never catch the real case (a PARTIAL build). Run it before PUBLISH.",
|
||||
"check:shots": "node internal/check-shot-freshness.mjs # a README screenshot that predates the bundle it shows is a picture telling a lie \u2014 and it looks IDENTICAL to a current one. Compares each shot's PNG against the BUILT BUNDLE it renders (not raw sources: an unbuilt edit is not yet in any leaf). Fix: node internal/readme-shots.mjs",
|
||||
"check:library-index": "node internal/check-library-index.mjs # the DERIVED library menu must not fall behind library/. Compares the index against the newest library SOURCE (not a bundle \u2014 this generator scans library/ directly, with no build step between). Fix: bun run library-index",
|
||||
"check:quoted-scripts": "node internal/check-quoted-scripts.mjs # docs/README-SECTION-3-cli-commands.md QUOTES the scripts block verbatim; it had silently drifted TWICE (a stale bunx cap:sync, and build:unlocked missing check:public-hygiene + check:label-joins). package.json is what RUNS \u2014 update the DOC.",
|
||||
"shots": "node internal/readme-shots.mjs # regenerate docs/readme-images/*.png (fetches its own Mapbox token SERVER-SIDE, so the browser CORS block does not apply). Was UNREACHABLE \u2014 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 docs/reference-library-index.md \u2014 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 \u2014 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.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue