From 112f4cee77e82808bcfa357efcf5fcf3f7b19e05 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 07:37:16 +0000 Subject: [PATCH] =?UTF-8?q?check:studio-rail=20=E2=80=94=20a=20production?= =?UTF-8?q?=20profile=20must=20DECLARE=20its=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rail showed tiles nobody had named while this gate stayed green. The obvious fix is a no-op, and writing it first is how that came out. THE NO-OP: testing the rail's label for null/empty can never fail, because rowsFromManifest falls back to humanize(id) (plugin-studio.vue:421,429). A profile that declares nothing still arrives as "Doxa Simple Map" -- not empty, and not equal to the bare id either, so the existing bare-id rule misses it too. humanize() defeats BOTH rules. That is why country-posters could ship a tile nobody had named. So the property is the DECLARATION, read from the manifest, not the rendered label: does this profile's .meta.json carry a "label"? A humanized id is a reasonable DEFAULT and a bad NAME -- "Doxa Simple Map" tells a reader nothing the folder did not already say. PRODUCTION-STAGE ONLY, deliberately: 15 profiles declare no label today and 14 are experimental -- work in progress nobody has named yet, which is not a defect. A gate that is red for a condition the repo accepts gets ignored. Also declares the two labels my own plugin was missing: upg-100-list becomes "UPG-100 — Zoned Map" and poster-wall "UPG-100 — Poster Wall". ⚠ THIS GATE IS RED ON MAIN TODAY, 8 violations across 4 plugins and 4 owners (cities-internet 2, doxa-simple-map 1, gospel-resources 3, my-upg-100-list 2). It must NOT land before those labels do, or it blocks every other agent's landing for a defect that is not theirs. The two meta files here fix my share; they reach the manifest only through the full build, which is the build owner's. internal/ is the build owner's under rule 5B -- source sha to studio, sequenced after the other three owners declare their labels. --- internal/check-studio-rail.mjs | 26 +++++++++++++++++++ .../my-upg-100-list/poster-wall.meta.json | 4 +++ .../my-upg-100-list/upg-100-list.meta.json | 4 +++ 3 files changed, 34 insertions(+) create mode 100644 plugin-designs/my-upg-100-list/poster-wall.meta.json create mode 100644 plugin-designs/my-upg-100-list/upg-100-list.meta.json diff --git a/internal/check-studio-rail.mjs b/internal/check-studio-rail.mjs index 78862bed2..2eb967ac7 100644 --- a/internal/check-studio-rail.mjs +++ b/internal/check-studio-rail.mjs @@ -66,6 +66,32 @@ try { for (const b of byBundle.keys()) if (!seen.has(b)) fail(`bundle ${b} is missing from the rail`) // A row is NAMED, never shown as a bare id (the Driver: Gospel x Internet read just "board"). for (const r of ctx.rows) if (r.label === r.profile || r.label === r.bundle) fail(`${r.key} shows a bare id ("${r.label}") — give its .meta.json a "label"`) + // ★ AND A PRODUCTION PROFILE MUST DECLARE ITS NAME, not inherit one. + // + // ⚠ THE OBVIOUS VERSION OF THIS RULE IS A NO-OP, and writing it first is how I found out. Testing + // the RAIL's label for null/empty can never fail: rowsFromManifest falls back to humanize(id) + // (plugin-studio.vue:421,429), so a profile that declares NOTHING still arrives here as + // "Doxa Simple Map" — not empty, and not equal to the bare id either, so the bare-id rule above + // misses it too. humanize() defeats BOTH rules, which is why country-posters could ship a tile + // nobody had named while this gate stayed green. + // + // So the property is the DECLARATION, read from the manifest, not the rendered label: does this + // profile's .meta.json carry a "label"? A humanized id is a reasonable DEFAULT and a bad + // NAME — "Doxa Simple Map" tells a reader nothing the folder did not already say. + // + // ⚠ PRODUCTION-STAGE ONLY, deliberately. 15 profiles declare no label today and 14 are + // 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`) + } + } + } 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/plugin-designs/my-upg-100-list/poster-wall.meta.json b/plugin-designs/my-upg-100-list/poster-wall.meta.json new file mode 100644 index 000000000..d6b02a7cd --- /dev/null +++ b/plugin-designs/my-upg-100-list/poster-wall.meta.json @@ -0,0 +1,4 @@ +{ + "_comment": "The printable half of UPG-100: a wall of prayer-card posters, one per 100-list entry, with the three print paths. Declared so the Studio rail names it rather than falling back to a humanized id (check:studio-rail).", + "label": "UPG-100 — Poster Wall" +} diff --git a/plugin-designs/my-upg-100-list/upg-100-list.meta.json b/plugin-designs/my-upg-100-list/upg-100-list.meta.json new file mode 100644 index 000000000..a454674eb --- /dev/null +++ b/plugin-designs/my-upg-100-list/upg-100-list.meta.json @@ -0,0 +1,4 @@ +{ + "_comment": "The map half of UPG-100: the zoned list of unengaged people groups, with the semantic-tree legend and the packet/poster exports. Declared so the Studio rail names it rather than falling back to a humanized id (check:studio-rail).", + "label": "UPG-100 — Zoned Map" +}