The capability gate reported five composes refs as "resolve to no known bundle/profile" —
bp-research-header, cities-internet-map, gospel-resources-map, my-upg-100-list-map,
research-dashboard-map. All five were correct data. `composes` accepts a third form besides
'<bundle>/<profile>' and a bare '<bundle>': a registered ELEMENT TAG. Nothing needed renaming and no
author's meta.json was wrong; the resolver was short. 5 unresolved -> 0.
Two things the element form forced, both measured:
· A SELF-REFERENCE is an internal child, not a nested plugin. big-picture composes
bp-research-header, its own element, which resolved back to big-picture (cap-4) and promoted the
bundle to cap-5. Excluding self keeps "nests other plugins" from meaning "has parts".
· A name covering several profiles resolves to the HIGHEST tier among them, and both non-slashed
forms now resolve the same way. A bare bundle name and that bundle's element tag both mean "this
bundle", so taking the first profile for one and the highest for the other made the answer depend
on discovery order. gospel-resources shows it: four single leaves and one cap-4 composite in one
bundle, and losing the 4 silently demotes whoever composes it.
The rule now lives in internal/_composed-tiers.mjs, imported by the generator that WRITES the tier and
the gate that CHECKS it. It was duplicated, and a rule this fiddly does not stay identical in two files;
the drift would have shown up as a gate passing a manifest the generator would refuse to write. The
data stays independent — the gate rebuilds the edges from the <profile>.meta.json files on disk and
checks the published manifest against them.
With the count at 0, an unresolved ref FAILS in both. While five were outstanding a warning was right:
they were correct data the resolver could not read, and failing would have blamed five authors for a
gap in the tooling. At zero, a typo and a genuine plain child still look identical from here, so the
one thing that must not happen is quietly reading one as the other.
Verified on the tree as it stands: 37 profiles, 25 bundles, 10 declaring composes edges — 10 agree,
0 disagree, 0 unresolved. Each fail arm negative-tested: a one-letter typo in a ref fails; a published
tier that disagrees with its own edges fails; the same composes shape pointing at another bundle's
cap-4 instead of its own element fails, which is what proves self-exclusion is load-bearing rather
than incidental.
98 lines
6.1 KiB
JavaScript
98 lines
6.1 KiB
JavaScript
/**
|
|
* _composed-tiers.mjs — the ONE place the composition tier is derived from `composes` edges.
|
|
*
|
|
* Shared on purpose. The generator WRITES this tier into the manifest and check-capability-derived
|
|
* CHECKS it, and for a while each held its own copy of the rule. A rule with three ref vocabularies
|
|
* and a self-exclusion will not stay identical in two files, and the drift has a specific shape: a
|
|
* gate that passes a manifest the generator would refuse to write. One implementation, two callers.
|
|
*
|
|
* What stays independent is the DATA, which is the half worth duplicating: the generator derives from
|
|
* the profiles it is about to emit, the gate rebuilds the same edges from the <profile>.meta.json files
|
|
* on disk and feeds them to this function against the PUBLISHED manifest. So a manifest that no longer
|
|
* matches its own sources still fails, while the rule itself cannot fork.
|
|
*
|
|
* Imports nothing — pure over the bundle rows, which is what makes it testable from either side.
|
|
*/
|
|
|
|
/**
|
|
* Derive the composition tier from the `composes` edges, and refuse to disagree with an author.
|
|
*
|
|
* ★ WHY DERIVE AT ALL (Driver, 2026-09-27). Tiers 3-5 were AUTHORED ONLY — discover.mjs infers just
|
|
* `single` and `parameterized`, so a composite with no meta.json shipped as capability 1. That is not
|
|
* hypothetical: research-dashboard did exactly that while nesting ten views, and the warning in
|
|
* detectProfileType exists because of it. A missing file is indistinguishable from a deliberate choice.
|
|
*
|
|
* THE RULE, measured against all 25 bundles before it was written: a profile that composes anything is
|
|
* cap-4 (nested-elements-mfe); if any of the things it composes is itself cap-4, it is cap-5
|
|
* (nested-mfe-spa). Keyed per PROFILE, and refs are accepted in BOTH published forms — 'bundle/profile'
|
|
* (doxa-maps-spa) and a bare 'bundle' (plugin-studio). Getting either of those wrong is not academic:
|
|
* keying on the bundle derived 11 false tiers, and resolving only the slashed form demoted plugin-studio
|
|
* from 5 to 4. With both handled: 10 composing profiles, 10 agree, 0 disagree.
|
|
*
|
|
* AN EXPLICIT type STAYS AUTHORITATIVE AND MUST AGREE. It is not overridden — it is CHECKED. A
|
|
* declaration that contradicts the structure is the interesting case: either the meta is stale or the
|
|
* composes list is, and both are worth a loud build failure rather than a silent pick between them.
|
|
*
|
|
* An UNRESOLVED ref is reported separately and never silently treated as "not cap-4", because a typo in
|
|
* composes and a genuinely non-composite child look identical from here.
|
|
*/
|
|
export function deriveComposedTiers(bundles) {
|
|
const TIER = { 'nested-mfe-spa': 5, 'nested-elements-mfe': 4, monolithic: 3, parameterized: 2, single: 1 }
|
|
const byKey = new Map(), byName = new Map(), byElement = new Map()
|
|
const reg = (m, k, bundle, type) => { if (!m.has(k)) m.set(k, []); m.get(k).push([bundle, type || null]) }
|
|
for (const b of bundles) {
|
|
for (const p of b.profiles || []) {
|
|
byKey.set(`${b.bundle}/${p.profile}`, p.type || null)
|
|
reg(byName, b.bundle, b.bundle, p.type)
|
|
reg(byName, p.profile, b.bundle, p.type)
|
|
if (p.element) reg(byElement, p.element, b.bundle, p.type)
|
|
}
|
|
for (const e of b.elements || []) reg(byElement, e, b.bundle, null)
|
|
}
|
|
// A name that covers SEVERAL profiles resolves to the HIGHEST tier among them, and both non-slashed
|
|
// vocabularies resolve the same way. They have to: a bare bundle name and that bundle's element tag
|
|
// both mean "this bundle", so taking the first profile for one and the highest for the other made
|
|
// the answer depend on discovery order. gospel-resources is the case that shows it — four `single`
|
|
// leaves and one cap-4 composite in one bundle, so "first" is a coin flip between 1 and 4, and
|
|
// losing the 4 silently demotes whoever composes it. Highest is also the truthful reading of a
|
|
// whole-bundle ref: nesting a bundle nests the most composed thing in it.
|
|
const best = (rows, selfBundle) => {
|
|
const others = rows.filter(([b, t]) => b !== selfBundle && t)
|
|
if (!others.length) return null
|
|
return others.sort((a, z) => (TIER[z[1]] || 0) - (TIER[a[1]] || 0))[0][1]
|
|
}
|
|
// ★ `composes` SPEAKS THREE VOCABULARIES, and only knowing two of them invents defects.
|
|
// Measured on the tree: 'bundle/profile' (doxa-maps-spa), a bare 'bundle' (plugin-studio's first four
|
|
// entries) and an ELEMENT TAG (its last three, plus big-picture and stack-demo). Resolving only the
|
|
// first two left FIVE refs "unresolved" and reported as possible typos — they were all element tags,
|
|
// written correctly by their authors. The data was right and the resolver was short.
|
|
//
|
|
// ★ AND A SELF-REFERENCE IS AN INTERNAL CHILD, NOT A NESTED PLUGIN. big-picture composes
|
|
// 'bp-research-header', which is its OWN element (src/components/BpResearchHeader.vue). Resolving that
|
|
// to the bundle registering it — itself, cap-4 — made big-picture look like it composes a cap-4 and
|
|
// promoted it to cap-5. Excluding self is what keeps "nests other plugins" from meaning "has parts".
|
|
const UNRESOLVED = Symbol('unresolved')
|
|
const resolve = (ref, selfBundle) => {
|
|
if (ref.includes('/')) {
|
|
if (!byKey.has(ref)) return UNRESOLVED
|
|
return ref.split('/')[0] === selfBundle ? null : byKey.get(ref)
|
|
}
|
|
if (byName.has(ref)) return best(byName.get(ref), selfBundle)
|
|
if (byElement.has(ref)) return best(byElement.get(ref), selfBundle)
|
|
return UNRESOLVED
|
|
}
|
|
const conflicts = [], unresolved = []
|
|
for (const b of bundles) for (const p of b.profiles || []) {
|
|
const own = p.composesOwn
|
|
if (!own || !own.length) continue
|
|
const kinds = own.map((ref) => {
|
|
const k = resolve(ref, b.bundle)
|
|
if (k === UNRESOLVED) 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) { p.type = derived; continue }
|
|
if (p.type !== derived) conflicts.push(`${b.bundle}/${p.profile}: declared tier "${p.type}" disagrees with the "${derived}" its composes edges derive`)
|
|
}
|
|
return { conflicts, unresolved }
|
|
}
|