doxa-maps/internal/check-capability-derived.mjs
Doxa Maps 8afea17c6b Capability is derived from what a plugin composes, and the preset-plugin standard is written down
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.
2026-09-27 23:14:15 +00:00

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.`)