From 1223fa7b459a21b51e316cb61a2fd74fc31192c9 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 07:52:50 +0000 Subject: [PATCH] =?UTF-8?q?check:studio-rail=20=E2=80=94=20scope=20the=20l?= =?UTF-8?q?abel=20rule=20by=20PROFILE=20stage,=20not=20bundle=20stage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit regions-plugin challenged the rule and was right. A BUNDLE reads `stage: production` when ANY ONE of its profiles is production, so scoping the loop by b.stage drags every experimental sibling in with it. Measured: ALL SEVEN violations it reported were false. cities-internet is a production bundle whose connectivity-list and data-tables are EXPERIMENTAL; gospel-resources likewise for charts / data-tables / resources-list; and my-upg-100-list's own poster-wall and upg-100-list are experimental too -- only upg-100-mfe is production, and it has carried a label all along. With p.stage the gate reports 0. THE SAME TRAP A THIRD TIME. I moved off stageOf (bundle-keyed) after catching it once, and moved to b.stage -- the same mistake wearing a different field name. The profile's own stage is the only one that answers "is this row production". THE SELF-TEST HAD A HOLE THAT MADE THE BUG INVISIBLE: every fixture bundle had exactly one profile, so bundle-stage and profile-stage always agreed and scoping by the wrong one could not be detected. A fixture where the two CANNOT disagree cannot test which one you read. Adds a mixed bundle whose production profile must FAIL and whose experimental profile must PASS; 6/6. The two meta files in be398ce8f stay. They were a fix for a non-problem -- both profiles are experimental -- but a named row is better than a humanized id, and they cost nothing. --- internal/check-studio-rail.mjs | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/internal/check-studio-rail.mjs b/internal/check-studio-rail.mjs index d3637a4c3..d87aa0c37 100644 --- a/internal/check-studio-rail.mjs +++ b/internal/check-studio-rail.mjs @@ -25,8 +25,17 @@ import { TREE_NAME } from './_tree-name.mjs' function undeclaredLabels(manifest) { const out = [] for (const b of manifest.bundles || []) { - if (b.stage !== 'production') continue for (const p of b.profiles || []) { + // ⚠ PROFILE STAGE, NOT BUNDLE STAGE — the same trap, a third time. A BUNDLE reads + // `stage: production` when ANY ONE of its profiles is production, so scoping the loop by + // b.stage drags every experimental sibling in with it. Measured: cities-internet is a + // production bundle whose connectivity-list and data-tables are EXPERIMENTAL, and + // gospel-resources likewise for charts / data-tables / resources-list. That was 5 false + // violations out of 7 (regions-plugin caught it, 2026-09-28). + // I had already moved off stageOf (bundle-keyed) once; moving to b.stage was the SAME + // mistake wearing a different field name. The profile's own stage is the only one that + // answers "is this row production". + if (p.stage !== 'production') continue 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`) @@ -42,11 +51,20 @@ function undeclaredLabels(manifest) { // 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' }] }, + { bundle: 'planted-prod', stage: 'production', profiles: [{ profile: 'nameless', stage: 'production', label: null }] }, + { bundle: 'planted-exp', stage: 'experimental', profiles: [{ profile: 'nameless', stage: 'experimental', label: null }] }, + { bundle: 'planted-ok', stage: 'production', profiles: [{ profile: 'named', stage: 'production', 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: ' ' }] }, + { bundle: 'planted-blank', stage: 'production', profiles: [{ profile: 'blank', stage: 'production', label: ' ' }] }, + // ★ THE CASE THE FIRST SELF-TEST MISSED, and the bug it would have caught. A BUNDLE reads + // production when ANY profile is, so a mixed bundle must flag ONLY its production profile. + // The earlier fixture gave every bundle one profile, so bundle-stage and profile-stage agreed + // and scoping by the wrong one was invisible. A fixture where the two CANNOT disagree cannot + // test which one you read. + { bundle: 'planted-mixed', stage: 'production', profiles: [ + { profile: 'prod-nameless', stage: 'production', label: null }, + { profile: 'exp-nameless', stage: 'experimental', label: null }, + ] }, ] } const hits = undeclaredLabels(fixture) const cases = [ @@ -54,6 +72,8 @@ if (process.argv.includes('--self-test')) { ['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/'))], + ['mixed bundle: its PRODUCTION profile FAILS', hits.some((h) => h.startsWith('planted-mixed/prod-nameless'))], + ['mixed bundle: its EXPERIMENTAL profile PASSES', !hits.some((h) => h.startsWith('planted-mixed/exp-nameless'))], ] let ok = 0 for (const [name, pass] of cases) { console.log(` ${pass ? '✓' : '✗'} ${name}`); if (pass) ok++ }