doxa-maps/internal/check-studio-rail.mjs
Doxa Maps f294159b64
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Update from source 2b826795
2026-09-27 06:32:22 +00:00

82 lines
5.9 KiB
JavaScript

#!/usr/bin/env node
/**
* check-studio-rail — the Plugin Studio's rail is laid out like plugin-designs/. Headless.
*
* The agent's ask (2026-09-26): one FOLDER per BUNDLE with its ELEMENTS inside, bundles sorted by
* capability, highest first. Runs the studio's REAL treeSections / compositionTrees / rowsFromManifest
* (loaded through Vite, no browser) against the BUILT manifest and asserts:
* - PRODUCTION bundles (manifest bundle.stage, from their .meta.json) sit in capability sections that run
* CAP 5 → 1, each in the section of its HIGHEST capability, once; every EXPERIMENTAL bundle sits in one
* final Experimental section, highest capability first (the agent, 2026-09-27);
* - bundles within a section are in name order;
* - a bundle with several elements is ONE folder holding all of them; a single-element bundle is its element;
* - every studio row has exactly ONE canonical node (one identity per plugin; parts only target it).
* EXIT 0 pass · 1 fail · 0 with SKIP when there is no built manifest
*/
import { readFileSync, existsSync } from 'node:fs'
import { join, resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
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) }
const { createServer } = await import('vite')
const { default: vue } = await import('@vitejs/plugin-vue')
const server = await createServer({ root: ROOT, configFile: false, logLevel: 'error', appType: 'custom',
server: { middlewareMode: true, hmr: false, watch: null }, plugins: [vue({ customElement: true })],
resolve: { alias: { '@map': join(ROOT, 'library') } }, define: { __DOXA_TREE_NAME__: JSON.stringify(TREE_NAME) } })
globalThis.window = globalThis
globalThis.localStorage = { getItem: () => null, setItem() {} }
let bad = 0
const fail = (m) => { bad++; console.log(' FAIL ' + m) }
try {
const comp = (await server.ssrLoadModule('/plugin-designs/plugin-studio/plugin-studio.vue')).default
const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'))
const ctx = { manifestBundles: manifest.bundles, tr: (k) => k, cfg: {}, base: '' }
ctx.rows = comp.methods.rowsFromManifest.call(ctx, manifest)
ctx.resolveRef = (ref, fb) => comp.methods.resolveRef.call(ctx, ref, fb)
ctx.compositionTrees = comp.computed.compositionTrees.call(ctx)
const secs = comp.computed.treeSections.call(ctx)
const stageOf = new Map(manifest.bundles.map((b) => [b.bundle, b.stage === 'production' ? 'production' : 'experimental']))
const expIdx = secs.findIndex((s) => s.id === 'stage-experimental')
if (expIdx !== -1 && expIdx !== secs.length - 1) fail('the Experimental section is not last')
const capSecs = secs.filter((s) => s.id !== 'stage-experimental')
const caps = capSecs.map((s) => s.cap)
if (caps.join() !== [...caps].sort((a, b) => b - a).join()) fail(`sections not CAP 5 → 1: ${caps.join(',')}`)
const byBundle = new Map()
for (const r of ctx.rows) { if (!byBundle.has(r.bundle)) byBundle.set(r.bundle, []); byBundle.get(r.bundle).push(r) }
const seen = new Map()
for (const s of secs) {
const tops = s.nodes.filter((n) => n.depth === 0)
const names = tops.map((n) => (n.folder ? n.label : ctx.rows.find((r) => r.key === n.target)?.bundle))
const isExp = s.id === 'stage-experimental'
const topCap = (b) => Math.max(...(byBundle.get(b) || []).map((r) => r.cap))
const want = isExp ? [...names].sort((a, b) => topCap(b) - topCap(a) || a.localeCompare(b)) : [...names].sort((a, b) => a.localeCompare(b))
if (names.join() !== want.join()) fail(`${isExp ? 'Experimental' : 'CAP ' + s.cap}: bundles not in order (${names.join(', ')})`)
for (const b of names) {
const st = stageOf.get(b)
if (isExp && st === 'production') fail(`production bundle ${b} is filed under Experimental`)
if (!isExp && st !== 'production') fail(`experimental bundle ${b} is filed under CAP ${s.cap}, not Experimental`)
}
for (const [i, n] of tops.entries()) {
const b = names[i]; const rows = byBundle.get(b) || []
if (seen.has(b)) fail(`bundle ${b} appears twice (CAP ${seen.get(b)} and CAP ${s.cap})`)
seen.set(b, s.cap)
const top = Math.max(...rows.map((r) => r.cap))
if (!isExp && top !== s.cap) fail(`bundle ${b} (highest CAP ${top}) filed under CAP ${s.cap}`)
if (rows.length > 1) {
if (!n.folder) fail(`bundle ${b} has ${rows.length} elements but is not a folder`)
const kids = s.nodes.filter((k) => k.parent === n.id && k.canonical).map((k) => k.target).sort()
if (kids.join() !== rows.map((r) => r.key).sort().join()) fail(`folder ${b} holds ${kids.length} of its ${rows.length} elements`)
} else if (n.folder) fail(`single-element bundle ${b} is shown as a folder`)
}
}
for (const b of byBundle.keys()) if (!seen.has(b)) fail(`bundle ${b} is missing from the rail`)
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
console.log(`STUDIO-RAIL: ${byBundle.size} bundle(s) (${nProd} production, ${byBundle.size - nProd} experimental), ${ctx.rows.length} element(s), ${secs.length} section(s); folders: ${secs.flatMap((s) => s.nodes.filter((n) => n.folder).map((n) => n.label + '(' + n.kids + ')')).join(', ')}`)
} finally { await server.close() }
if (bad) { console.log(`STUDIO-RAIL: FAIL — ${bad} problem(s).`); process.exit(1) }
console.log('STUDIO-RAIL: PASS — one folder per bundle with its elements inside; production bundles grouped by their highest capability (5 → 1) in name order, experimental bundles in one final Experimental section; one identity per plugin.')