Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work
Some checks are pending
build-gate / build (push) Waiting to run
Some checks are pending
build-gate / build (push) Waiting to run
This commit is contained in:
commit
c4a6d628b4
2 changed files with 91 additions and 50 deletions
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-library-index.mjs — the derived library menu must not fall behind library/.
|
||||
* check-library-index.mjs — the derived library menu must not DRIFT from library/.
|
||||
*
|
||||
* THE BUG THIS EXISTS TO PREVENT (real, 2026-09-21)
|
||||
* internal/generate-library-index.mjs opens with: "The index is DERIVED, never hand-edited — so it
|
||||
|
|
@ -8,29 +8,70 @@
|
|||
* no package.json entry, and documentation/reference-library-index.md DID NOT EXIST AT ALL. A documented
|
||||
* invariant enforced only by a human remembering is a wish, not an invariant.
|
||||
*
|
||||
* ★ IT ASKS THE REAL QUESTION NOW: "HAS THE MENU DRIFTED?", NOT "IS THIS FILE OLDER?"
|
||||
* This compared MTIMES until 2026-09-24, and an mtime is a proxy that answers a different question
|
||||
* than the one the gate's own name asks. MEASURED: `touch library/components/legends/
|
||||
* SemanticTreeLegend.vue` — zero content change — turned it RED. So did a `git checkout`, a rebase,
|
||||
* and any editor that rewrites a file on save. It cried wolf TWICE in a single build session while
|
||||
* the regenerated doc was byte-identical both times, and the only available response was to run the
|
||||
* generator and commit nothing.
|
||||
*
|
||||
* That is the SECOND time this exact class was fixed here. The first (2026-09-23, note below) was a
|
||||
* SCOPE error — the gate walked 2.3x more of library/ than the generator reads, so adding a comment
|
||||
* to a build script turned it red and named that script as the reason the component MENU was stale.
|
||||
* Narrowing the scope fixed that instance; the MECHANISM was still mtime, so the class survived
|
||||
* inside the narrowed scope. The lesson recorded then is the one that applies now, verbatim:
|
||||
* "A gate that fails for a change it can prove is irrelevant teaches the reader to regenerate on
|
||||
* reflex, which is how a staleness check stops being read."
|
||||
*
|
||||
* HOW: regenerate into a THROWAWAY TREE and diff the bytes. generate-library-index.mjs resolves its
|
||||
* ROOT from its own location (`join(fileURLToPath(new URL('.', import.meta.url)), '..')`) and
|
||||
* hardcodes its output path, so it cannot be told to write elsewhere — but copying the SCRIPT into a
|
||||
* temp tree whose `library/` is a SYMLINK to the real one relocates its ROOT without touching it.
|
||||
* The generator runs completely unmodified, reads the real sources, and writes somewhere disposable.
|
||||
* Nothing in the working tree is stashed or mutated, so an interrupted run leaves no mess.
|
||||
* VERIFIED byte-identical to a real `bun run library-index` before this gate was written.
|
||||
*
|
||||
* PRECONDITION, tested before relying on it: the generator is DETERMINISTIC — two runs a second
|
||||
* apart produce identical bytes (no timestamps, no directory-order dependence). A content diff would
|
||||
* be useless otherwise, and would have been a worse gate than the mtime one it replaces.
|
||||
*
|
||||
* WHY RAW SOURCE IS THE RIGHT ANCESTOR HERE (and was NOT for the screenshots)
|
||||
* check-shot-freshness compares a PNG against the BUILT BUNDLE, because a screenshot renders a
|
||||
* built leaf and an unbuilt edit is not yet visible in one. This index is different: it is
|
||||
* generated by SCANNING library/ directly, with no build step in between. So the honest ancestor
|
||||
* is the newest library/ source file. ★ THE ANCESTOR IS WHATEVER THE GENERATOR ACTUALLY READS —
|
||||
* copying the other gate's choice would have been wrong.
|
||||
* generated by SCANNING library/ directly, with no build step in between. ★ THE ANCESTOR IS
|
||||
* WHATEVER THE GENERATOR ACTUALLY READS — copying the other gate's choice would have been wrong.
|
||||
*
|
||||
* NEGATIVE-TESTED: `touch library/layouts/StackLayout.vue` makes this FAIL naming that file;
|
||||
* `bun run library-index` returns it to PASS.
|
||||
* ⚠ SCOPE NOTE, KEPT FROM THE MTIME VERSION because it still explains the generator: it reads
|
||||
* exactly three subdirectories — components/, composables/ and utils/ (see its section() calls).
|
||||
* 75 files are indexed, 173 are not — scripts/ 58, i18n/ 98, plus data/, layouts/, api/, constants/.
|
||||
* Comparing OUTPUT rather than input mtimes makes that scope self-enforcing: a change outside those
|
||||
* three directories cannot alter one byte of the generated menu, so this gate can no longer fire on
|
||||
* one. The INDEXED_DIRS list is gone with the mtime walk that needed it.
|
||||
*
|
||||
* NEGATIVE-TESTED: deleting a `- **Name**` row from the index makes this FAIL naming that row;
|
||||
* `touch`ing any library file does NOT (it is not drift); `bun run library-index` returns it to PASS.
|
||||
*/
|
||||
import { readdirSync, existsSync, statSync } from 'node:fs'
|
||||
import { existsSync, readFileSync, mkdtempSync, mkdirSync, copyFileSync, symlinkSync, rmSync } from 'node:fs'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const INDEX = join(ROOT, 'documentation', 'reference-library-index.md')
|
||||
const REL = join('documentation', 'reference-library-index.md')
|
||||
const INDEX = join(ROOT, REL)
|
||||
const LIB = join(ROOT, 'library')
|
||||
const GENERATOR = join(ROOT, 'internal', 'generate-library-index.mjs')
|
||||
|
||||
if (!existsSync(LIB)) {
|
||||
console.log('LIBRARY-INDEX: SKIP — no library/ directory.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (!existsSync(GENERATOR)) {
|
||||
console.log('LIBRARY-INDEX: SKIP — internal/generate-library-index.mjs is not present.')
|
||||
process.exit(0)
|
||||
}
|
||||
if (!existsSync(INDEX)) {
|
||||
console.error('LIBRARY-INDEX: FAIL — documentation/reference-library-index.md does not exist.')
|
||||
console.error(' It is the menu of reusable pieces an author reads INSTEAD of rebuilding them.')
|
||||
|
|
@ -38,47 +79,47 @@ if (!existsSync(INDEX)) {
|
|||
process.exit(1)
|
||||
}
|
||||
|
||||
// Newest source the generator actually scans.
|
||||
//
|
||||
// ★ AND THAT NOW MEANS WHAT IT SAYS (fixed 2026-09-23). This walked ALL of library/, while
|
||||
// generate-library-index.mjs reads exactly three subdirectories — components/, composables/ and
|
||||
// utils/ (see its section() calls). MEASURED: 75 files are indexed, 173 are not — scripts/ 58,
|
||||
// i18n/ 98, plus data/, layouts/, api/, constants/. So the gate watched 2.3x more than the thing it
|
||||
// guards, and a change anywhere in that surplus demanded a regeneration that could not alter one
|
||||
// byte of the output.
|
||||
//
|
||||
// It fired for real: adding a COMMENT to library/scripts/research/build-trade-languages.mjs turned
|
||||
// this gate red, naming a build script as the reason the component MENU was stale. A gate that
|
||||
// fails for a change it can prove is irrelevant teaches the reader to regenerate on reflex, which
|
||||
// is how a staleness check stops being read.
|
||||
//
|
||||
// Keeping the scope in step is the price of this design: if generate-library-index.mjs ever adds a
|
||||
// section(), add its directory here in the same commit.
|
||||
const INDEXED_DIRS = ['components', 'composables', 'utils']
|
||||
let newest = 0, newestFile = null
|
||||
const walk = (d) => {
|
||||
for (const e of readdirSync(d, { withFileTypes: true })) {
|
||||
if (e.name === 'node_modules' || e.name.startsWith('.')) continue
|
||||
const p = join(d, e.name)
|
||||
if (e.isDirectory()) { walk(p); continue }
|
||||
if (!/\.(vue|js|mjs)$/.test(e.name)) continue
|
||||
const m = statSync(p).mtimeMs
|
||||
if (m > newest) { newest = m; newestFile = p.slice(ROOT.length + 1) }
|
||||
// Regenerate into a throwaway tree: real script, real sources (symlinked), disposable output.
|
||||
let fresh = null
|
||||
let tmp = null
|
||||
try {
|
||||
tmp = mkdtempSync(join(tmpdir(), 'libidx-'))
|
||||
mkdirSync(join(tmp, 'internal'))
|
||||
mkdirSync(join(tmp, 'documentation'))
|
||||
copyFileSync(GENERATOR, join(tmp, 'internal', 'generate-library-index.mjs'))
|
||||
symlinkSync(LIB, join(tmp, 'library'), 'dir')
|
||||
execFileSync(process.execPath, [join(tmp, 'internal', 'generate-library-index.mjs')], { stdio: 'pipe' })
|
||||
fresh = readFileSync(join(tmp, REL), 'utf8')
|
||||
} catch (e) {
|
||||
// SKIP, never fail, if the harness itself could not run — a gate that goes red because a
|
||||
// temp directory was unwritable reports a product problem that is not one.
|
||||
console.log(`LIBRARY-INDEX: SKIP — could not regenerate for comparison (${String(e.message || e).slice(0, 90)}).`)
|
||||
process.exit(0)
|
||||
} finally {
|
||||
if (tmp) { try { rmSync(tmp, { recursive: true, force: true }) } catch { /* best effort */ } }
|
||||
}
|
||||
|
||||
const onDisk = readFileSync(INDEX, 'utf8')
|
||||
const rows = (s) => s.split('\n').filter((l) => l.startsWith('- **'))
|
||||
console.log(`LIBRARY-INDEX: ${rows(onDisk).length} catalogued item(s) vs a fresh regeneration.`)
|
||||
|
||||
if (onDisk !== fresh) {
|
||||
const a = onDisk.split('\n')
|
||||
const b = fresh.split('\n')
|
||||
const diffs = []
|
||||
for (let i = 0; i < Math.max(a.length, b.length) && diffs.length < 8; i++) {
|
||||
if (a[i] !== b[i]) diffs.push({ line: i + 1, file: a[i] ?? '(end of file)', real: b[i] ?? '(end of file)' })
|
||||
}
|
||||
}
|
||||
for (const d of INDEXED_DIRS) {
|
||||
const p = join(LIB, d)
|
||||
if (existsSync(p)) walk(p)
|
||||
}
|
||||
|
||||
const idxMs = statSync(INDEX).mtimeMs
|
||||
console.log(`LIBRARY-INDEX: index vs ${newestFile ? 'newest library source' : 'library/'}.`)
|
||||
|
||||
if (newest > idxMs) {
|
||||
console.error(`\nLIBRARY-INDEX: FAIL — the derived menu is OLDER than library/.`)
|
||||
console.error(` ✗ ${newestFile} is ${Math.round((newest - idxMs) / 1000)}s newer than the index`)
|
||||
console.error('\nLIBRARY-INDEX: FAIL — the derived menu no longer matches library/.')
|
||||
for (const d of diffs) {
|
||||
console.error(` line ${d.line}`)
|
||||
console.error(` file: ${d.file}`)
|
||||
console.error(` real: ${d.real}`)
|
||||
}
|
||||
const onlyCount = Math.abs(rows(onDisk).length - rows(fresh).length)
|
||||
if (onlyCount) console.error(` (${onlyCount} catalogued row(s) differ in number)`)
|
||||
console.error('\n The file says "DERIVED — cannot drift from the code"; that is only true if it is')
|
||||
console.error(' regenerated. Fix: bun run library-index')
|
||||
console.error(' regenerated. Fix: bun run library-index (then commit the regenerated file).')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('LIBRARY-INDEX: PASS — the derived menu is newer than every library/ source.')
|
||||
console.log('LIBRARY-INDEX: PASS — the derived menu matches what library/ generates today.')
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@
|
|||
"fix:sri": "node internal/fix-sri.mjs",
|
||||
"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 — 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 — 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 — this generator scans library/ directly, with no build step between). Fix: bun run library-index",
|
||||
"check:library-index": "node internal/check-library-index.mjs # the DERIVED library menu must not DRIFT from library/. REGENERATES the index into a throwaway tree (the generator, unmodified, over a symlinked library/) and diffs the BYTES — it used to compare mtimes, so `touch` or a rebase turned it red on a byte-identical doc. Fix: bun run library-index",
|
||||
"check:quoted-scripts": "node internal/check-quoted-scripts.mjs # documentation/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 — update the DOC.",
|
||||
"capability-exemplars": "node internal/generate-capability-exemplars.mjs # regenerate documentation/reference-capability-exemplars.md — the DERIVED answer to \"which bundle is the capability-N example?\", a question answered WRONGLY in three separate artifacts (a gate, the README, the /plugin-designer skill).",
|
||||
"check:a11y-button-names": "node internal/check-a11y-button-names.mjs # every VISIBLE button must have an accessible name (aria-label / aria-labelledby / visible text — a `title` is a TOOLTIP and does NOT count). Measures the RENDERED DOM through shadow roots: a source grep cannot answer this (132 `<button>` hits, most with visible text). Needs a browser, so NOT in build:unlocked and NOT in CI. ALL=1 sweeps every profile.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue