generate-tree derives each profile's tier from its composes edges (composing anything is capability 4; composing a capability-4 plugin is capability 5). An explicit type is checked against the derivation and a contradiction fails the build naming both. check:capability-derived asserts every profile is 1-5 and agrees. documentation/PRESET-PLUGIN-STANDARD.md describes the one *.instances.json file a preset plugin declares.
92 lines
5.3 KiB
JavaScript
92 lines
5.3 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-capability-derived — every bundle lands on a real capability, 1-5, never 0.
|
|
*
|
|
* ★ THE DEFECT. Tiers 3-5 used to be AUTHORED ONLY: discover.mjs infers `single` and `parameterized`
|
|
* and nothing else, so a composite whose author forgot a <profile>.meta.json shipped as capability 1.
|
|
* research-dashboard did exactly that while nesting ten views. A missing file is indistinguishable
|
|
* from a deliberate choice, so nothing failed and the understatement propagated into the manifest,
|
|
* the gallery and the studio rail — every surface that claims a capability reads this one field.
|
|
*
|
|
* WHAT THIS ASSERTS, and why each half is here:
|
|
* 1. Every profile resolves to a capability 1-5. A 0 or a null means some surface will render a
|
|
* tier that does not exist; the Driver saw exactly that as "the Research Gallery shows as
|
|
* capability 0".
|
|
* 2. A profile that composes anything derives cap-4, or cap-5 if it composes a cap-4 — and an
|
|
* EXPLICIT type must AGREE with that. The declaration stays authoritative; it is not
|
|
* overridden, it is checked. A meta that contradicts the structure means the meta or the
|
|
* composes list is stale, and choosing silently between them is how a tier rots.
|
|
*
|
|
* ★ TWO TRAPS THIS GATE EXISTS BECAUSE OF, both found by running the rule before writing it:
|
|
* - `composes` is PER-PROFILE at source and HOISTED TO THE BUNDLE in the manifest. Deriving from
|
|
* the bundle row applies a composite's edges to its plain sibling leaves: gospel-resources has one
|
|
* composite and four single leaves, and all four derived as cap-4. 11 false failures.
|
|
* - refs appear in TWO FORMS in the same field: 'bundle/profile' and a bare 'bundle'. Resolving
|
|
* only the first demoted plugin-studio from 5 to 4.
|
|
* With both handled: 10 composing profiles, 10 agree, 0 disagree, on the tree as it stands.
|
|
*
|
|
* An UNRESOLVED ref is REPORTED, never counted as "not cap-4": a typo and a genuine non-composite
|
|
* child look identical from here, and quietly treating one as the other is how a tier silently drops.
|
|
*
|
|
* Headless, no browser. Exit 0 pass · 1 fail.
|
|
*/
|
|
import { readFileSync, existsSync } from 'node:fs'
|
|
import { resolve, dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { TREE_NAME } from './_tree-name.mjs'
|
|
|
|
const TAG = 'CAPABILITY-DERIVED'
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
const PT = { 'nested-mfe-spa': 5, 'nested-elements-mfe': 4, monolithic: 3, parameterized: 2, single: 1 }
|
|
const fails = []
|
|
|
|
const mfPath = join(ROOT, 'public', TREE_NAME, 'manifest.json')
|
|
if (!existsSync(mfPath)) { console.log(`${TAG}: SKIP — no built tree at ${TREE_NAME}.`); process.exit(0) }
|
|
const manifest = JSON.parse(readFileSync(mfPath, 'utf8'))
|
|
const bundles = manifest.bundles || []
|
|
|
|
// ── 1. every profile has a real capability ─────────────────────────────────────────────────────
|
|
let checked = 0
|
|
for (const b of bundles) for (const p of b.profiles || []) {
|
|
checked++
|
|
const cap = PT[p.type]
|
|
if (!cap) fails.push(`${b.bundle}/${p.profile}: type ${JSON.stringify(p.type)} is not one of the five tiers — every surface that shows a capability reads this.`)
|
|
}
|
|
|
|
// ── 2. a declared tier must agree with the edges it declares ────────────────────────────────────
|
|
const readMeta = (bundle, profile) => {
|
|
const f = join(ROOT, 'plugin-designs', bundle, `${profile}.meta.json`)
|
|
try { return existsSync(f) ? JSON.parse(readFileSync(f, 'utf8')) : null } catch { return null }
|
|
}
|
|
const byKey = new Map(), byName = new Map()
|
|
for (const b of bundles) for (const p of b.profiles || []) {
|
|
byKey.set(`${b.bundle}/${p.profile}`, p.type || null)
|
|
if (!byName.has(b.bundle)) byName.set(b.bundle, p.type || null)
|
|
if (!byName.has(p.profile)) byName.set(p.profile, p.type || null)
|
|
}
|
|
const resolveRef = (ref) => (ref.includes('/') ? byKey.get(ref) : byName.get(ref))
|
|
const unresolved = []
|
|
let composing = 0
|
|
for (const b of bundles) for (const p of b.profiles || []) {
|
|
const meta = readMeta(b.bundle, p.profile)
|
|
const own = Array.isArray(meta?.composes) ? meta.composes.map(String).filter(Boolean) : []
|
|
if (!own.length) continue
|
|
composing++
|
|
const kinds = own.map((ref) => { const k = resolveRef(ref); if (k === undefined) unresolved.push(`${b.bundle}/${p.profile} -> ${ref}`); return k })
|
|
const derived = kinds.some((k) => k === 'nested-elements-mfe') ? 'nested-mfe-spa' : 'nested-elements-mfe'
|
|
if (p.type !== derived) {
|
|
fails.push(`${b.bundle}/${p.profile}: declares "${p.type}" but its own composes edges derive "${derived}" — one of the two is stale.`)
|
|
}
|
|
}
|
|
|
|
console.log(` · ${checked} profile(s) across ${bundles.length} bundle(s); ${composing} declare composes edges`)
|
|
if (unresolved.length) {
|
|
console.log(` · ${unresolved.length} composes ref(s) resolve to no known bundle/profile. Reported, NOT treated as "not a composite" — a typo and a plain child look the same from here:`)
|
|
for (const u of unresolved) console.log(` ${u}`)
|
|
}
|
|
if (fails.length) {
|
|
console.error(`\n${TAG}: FAIL`)
|
|
for (const f of fails) console.error(` ✗ ${f}`)
|
|
process.exit(1)
|
|
}
|
|
console.log(`${TAG}: PASS — every profile lands on a real tier 1-5, and every declared tier agrees with its own composes edges.`)
|