From 114df6f6c4cdce273564493e60a26dc82a4cd6c3 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 07:39:52 +0000 Subject: [PATCH] =?UTF-8?q?check:studio-rail=20=E2=80=94=20a=20fixture=20s?= =?UTF-8?q?elf-test=20for=20the=20declared-label=20rule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- internal/check-studio-rail.mjs | 55 ++++++++++++++++++++++++++++------ package.json | 1 + 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/internal/check-studio-rail.mjs b/internal/check-studio-rail.mjs index 2eb967ac7..d3637a4c3 100644 --- a/internal/check-studio-rail.mjs +++ b/internal/check-studio-rail.mjs @@ -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 .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 .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 diff --git a/package.json b/package.json index a994b2937..7f79196e1 100644 --- a/package.json +++ b/package.json @@ -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",