Plugin Studio: every plugin sits under its capability 1-5; no Experimental section

Every plugin is capability 1 to 5, so the rail files each bundle under its
highest capability again. A bundle not yet in production says Experimental on
its own rows. check:studio-rail now asserts there is no section outside 1-5.
This commit is contained in:
Doxa Maps 2026-09-27 22:14:22 +00:00
parent 22440899d5
commit 7d3cfca897
2 changed files with 16 additions and 33 deletions

View file

@ -5,9 +5,9 @@
* The Driver'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 Driver, 2026-09-27);
* - EVERY bundle sits in a capability section (they run CAP 5 → 1), in the section of its HIGHEST
* capability, once; there is no Experimental section: every plugin is capability 1-5 (the Driver,
* 2026-09-27), and a non-production bundle says so on its own rows;
* - 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).
@ -39,10 +39,8 @@ try {
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)
for (const s of secs) if (!(s.cap >= 1 && s.cap <= 5)) fail(`section ${s.id} is not a capability 1-5 section (cap ${s.cap})`)
const caps = secs.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) }
@ -50,21 +48,14 @@ try {
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`)
}
const want = [...names].sort((a, b) => a.localeCompare(b))
if (names.join() !== want.join()) fail(`CAP ${s.cap}: bundles not in order (${names.join(', ')})`)
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 (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()
@ -79,4 +70,4 @@ try {
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.')
console.log('STUDIO-RAIL: PASS — one folder per bundle with its elements inside; every bundle grouped by its highest capability (5 → 1) in name order, no Experimental section; one identity per plugin.')

View file

@ -616,24 +616,19 @@ export default defineComponent({
const treeOf = new Map(this.compositionTrees.map(t => [t.nodes[0].selects, t]))
const byBundle = new Map()
for (const r of this.rows) { if (!byBundle.has(r.bundle)) byBundle.set(r.bundle, []); byBundle.get(r.bundle).push(r) }
// ★ PRODUCTION vs EXPERIMENTAL (the Driver, 2026-09-27): a public plugin that does not yet work as a
// production map is listed under Experimental until it does. The stage is the bundle's own
// .meta.json "stage", carried by the manifest, so this file keeps no list and the gallery reads the
// same field. Production bundles keep their capability sections; every experimental bundle goes in
// one Experimental section at the end, highest capability first.
const bundles = [...byBundle.entries()].map(([bundle, rows]) => ({ bundle, rows, cap: Math.max(...rows.map(r => r.cap)), stage: rows[0].stage }))
const experimental = bundles.filter(b => b.stage !== 'production').sort((x, y) => y.cap - x.cap || x.bundle.localeCompare(y.bundle))
const production = bundles.filter(b => b.stage === 'production')
const groups = [5, 4, 3, 2, 1].map(cap => ({ cap, group: production.filter(b => b.cap === cap).sort((x, y) => x.bundle.localeCompare(y.bundle)) }))
groups.push({ cap: 'experimental', group: experimental })
// ★ EVERY PLUGIN IS CAPABILITY 1-5 (the Driver, 2026-09-27, replacing the Experimental section he
// asked for earlier the same day): each bundle sits in the section of its highest capability. A bundle
// whose .meta.json "stage" is not production says so on its own rows instead; the gallery reads the
// same field.
const bundles = [...byBundle.entries()].map(([bundle, rows]) => ({ bundle, rows, cap: Math.max(...rows.map(r => r.cap)) }))
const groups = [5, 4, 3, 2, 1].map(cap => ({ cap, group: bundles.filter(b => b.cap === cap).sort((x, y) => x.bundle.localeCompare(y.bundle)) }))
return groups.map(({ cap, group }) => {
if (!group.length) return null
const nodes = []
const pushElement = (r, depth, parent) => {
const t = treeOf.get(r.key)
nodes.push({
// In the Experimental section the heading carries no tier, so each row says its own.
id: r.key, parent, depth, label: r.label, desc: cap === 'experimental' ? 'CAP ' + r.cap + ' · ' + r.tag : r.tag, icon: r.icon, target: r.key, canonical: true,
id: r.key, parent, depth, label: r.label, desc: r.stage === 'production' ? r.tag : this.tr('stageExperimental') + ' · ' + r.tag, icon: r.icon, target: r.key, canonical: true,
kids: t ? t.nodes[0].kids : 0, title: r.label + ' · <' + r.tag + '> · CAP ' + r.cap + ' ' + this.tr('cap' + r.cap)
})
if (!t) return
@ -661,9 +656,6 @@ export default defineComponent({
})
for (const r of rows) pushElement(r, 1, fid)
}
// The Experimental section is a STAGE, not a tier: it has no capability number of its own (it read
// "CAP 0", and the Driver took the Research Gallery for a capability-0 plugin).
if (cap === 'experimental') return { id: 'stage-experimental', cap: 0, badge: 'EXP', short: 'E', label: this.tr('stageExperimental'), title: this.tr('stageExperimental'), icon: 'layers', nodes }
return { id: 'cap-' + cap, cap, label: this.tr('cap' + cap), title: this.tr('cap' + cap) + ' · ' + (MACHINE_TYPE[cap] || ''), icon: CAP_ICON[cap] || 'embed', nodes }
}).filter(Boolean)
},