check:studio-rail — a fixture self-test for the declared-label rule

The rule is extracted as a pure undeclaredLabels(manifest) so it can be tested
over a fixture without standing up Vite or needing a built tree, and runs before
that setup so `--self-test` works on a fresh clone.

Four planted cases, and the SECOND is the one that matters -- a rule that fails
everything is as useless as one that fails nothing:
  production + no label          must FAIL
  experimental + no label        must PASS   (the stage scope is the design)
  production + a real label      must PASS
  production + whitespace-only   must FAIL   (`label: "   "` satisfies a null
                                              check and still renders blank)

Wired as check:studio-rail:self-test. Verified the extraction changed no
behaviour: the live gate still reports the same 8 violations it did before.

It exists because the first version of this rule PASSED on a manifest that
contained the very defect it was written for -- humanize(id) meant the rendered
label was never empty, so nothing could fail.
This commit is contained in:
Doxa Maps 2026-09-28 07:39:52 +00:00
parent 112f4cee77
commit 114df6f6c4
2 changed files with 47 additions and 9 deletions

View file

@ -18,6 +18,51 @@ import { join, resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
/**
* Every PRODUCTION profile that declares no "label". Pure — takes a manifest, returns messages — so
* the self-test below can run it over a fixture without standing up Vite or a built tree.
*/
function undeclaredLabels(manifest) {
const out = []
for (const b of manifest.bundles || []) {
if (b.stage !== 'production') continue
for (const p of b.profiles || []) {
const declared = p.label
if (declared === null || declared === undefined || !String(declared).trim()) {
out.push(`${b.bundle}/${p.profile} is PRODUCTION but declares no "label" in its <profile>.meta.json — the rail falls back to a humanized id, which names nothing`)
}
}
}
return out
}
// ── SELF-TEST ────────────────────────────────────────────────────────────────
// ★ A PLANTED VIOLATION, because the first version of this rule PASSED on a manifest that contained
// the very defect it was written for. Two cases, and the SECOND is the one that matters: a rule that
// fails everything is as useless as one that fails nothing.
if (process.argv.includes('--self-test')) {
const fixture = { bundles: [
{ bundle: 'planted-prod', stage: 'production', profiles: [{ profile: 'nameless', label: null }] },
{ bundle: 'planted-exp', stage: 'experimental', profiles: [{ profile: 'nameless', label: null }] },
{ bundle: 'planted-ok', stage: 'production', profiles: [{ profile: 'named', label: 'A Real Name' }] },
// An EMPTY STRING is nameless too — `label: ""` would satisfy a null check and render blank.
{ bundle: 'planted-blank', stage: 'production', profiles: [{ profile: 'blank', label: ' ' }] },
] }
const hits = undeclaredLabels(fixture)
const cases = [
['production + no label FAILS', hits.some((h) => h.startsWith('planted-prod/'))],
['experimental + no label PASSES', !hits.some((h) => h.startsWith('planted-exp/'))],
['production + a real label PASSES', !hits.some((h) => h.startsWith('planted-ok/'))],
['production + whitespace-only FAILS', hits.some((h) => h.startsWith('planted-blank/'))],
]
let ok = 0
for (const [name, pass] of cases) { console.log(` ${pass ? '✓' : '✗'} ${name}`); if (pass) ok++ }
console.log(ok === cases.length
? `STUDIO-RAIL SELF-TEST: PASS — ${ok}/${cases.length}`
: `STUDIO-RAIL SELF-TEST: FAIL — ${ok}/${cases.length}`)
process.exit(ok === cases.length ? 0 : 1)
}
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
if (!existsSync(MANIFEST)) { console.log('STUDIO-RAIL: SKIP — no built manifest'); process.exit(0) }
@ -83,15 +128,7 @@ try {
// experimental — work in progress nobody has named yet, which is not a defect. Failing those
// would make this gate red for a condition the repo accepts, and a gate that is red for an
// accepted condition gets ignored. A profile earns the obligation when it reaches production.
for (const b of manifest.bundles) {
if (b.stage !== 'production') continue
for (const p of b.profiles || []) {
const declared = p.label
if (declared === null || declared === undefined || !String(declared).trim()) {
fail(`${b.bundle}/${p.profile} is PRODUCTION but declares no "label" in its <profile>.meta.json — the rail falls back to a humanized id, which names nothing`)
}
}
}
for (const m of undeclaredLabels(manifest)) fail(m)
const canon = secs.flatMap((s) => s.nodes.filter((n) => n.canonical))
for (const r of ctx.rows) { const c = canon.filter((n) => n.target === r.key).length; if (c !== 1) fail(`${r.key} has ${c} canonical nodes (want 1)`) }
const nProd = [...stageOf.values()].filter((v) => v === 'production').length

View file

@ -103,6 +103,7 @@
"check:engine-contract:poison": "node internal/check-gospel-engine-contract.mjs --poison",
"check:embed-contract:poison": "node internal/check-gospel-embed-contract.mjs --poison",
"check:map-attribution": "node internal/check-map-attribution.mjs",
"check:studio-rail:self-test": "node internal/check-studio-rail.mjs --self-test",
"check:studio-rail": "node internal/check-studio-rail.mjs",
"check:translator-contacts": "node internal/check-translator-contacts.mjs",
"check:translator-contacts:poison": "node internal/check-translator-contacts.mjs --poison",