Filed alongside ConductionNL/larpingapp#286, where a registered-but-unreferenced component shipped as unreachable UI long enough for its openspec task to be ticked over it. gates 22 and 53 are the two that exist to catch manifest cross-reference defects, and neither sees this one — in either direction.
Measured — with ajv resolved
Important, because this gate has a wiring failure mode that masks its verdict entirely. Without NODE_PATH set, on ConductionNL/larpingapp@development:
[gate-22] manifest-validation: FAIL — SCHEMA VALIDATION DID NOT HAPPEN — Ajv is not resolvable ...
[gate-53] effective-manifest-crossref: FAIL — SCHEMA VALIDATION DID NOT HAPPEN — the vendored
validator degraded to its structural lint (Ajv unresolvable mid-run)
With NODE_PATH=<app>/node_modules, same commit, both go PASS. Everything below is from the ajv-resolved runs — a red-for-wiring baseline would have told me nothing.
Direction 1 — orphaned registry entry (the #286 shape)
src/registry.js exports EventRoster: { kind: 'section', component: EventRoster }. No tabs[].component, no slots.*, no sections[].component anywhere in src/manifest.json names it. The component is resolvable and unreachable; the check-in surface it renders has no entry point.
[gate-53] effective-manifest-crossref: PASS
Direction 2 — manifest names a component that does not exist
Control on top of the #286 fix branch: changed one tab to name a component no registry exports.
{ "id": "checkin", "label": "Check-in", "icon": "AccountCheck",
"component": "ThisComponentDoesNotExistAnywhere" }
At runtime CnObjectSidebar.resolveTabComponent() logs Tab "checkin" component "..." not found in registry or customComponents and renders nothing — a blank tab.
[gate-22] manifest-validation: PASS
[gate-53] effective-manifest-crossref: PASS
Both silent.
Cause
scripts/lib/check_manifest_crossref.js never reads src/registry.js. Its joins are manifest→manifest (menu-route → page-id, open-page targets, deepLink correspondence, ADR-044 removals) and manifest→register-JSON (register/schema slug resolution). The one place the component registry comes up, it is explicitly declined:
// The modal registry is app code (src/registry.js et al.) the gate
warn('action-target', ptr, `open-modal action '...' targets '...' — modal registry is app code, not statically checkable`)
src/registry.js is app code, but it is not opaque — it is a fixed-shape ES module whose top-level export keys are statically extractable with a regex on the export default { block, which is how the app-local test in larpingapp#288 does it (it cannot import the module either, since the file pulls in .vue SFCs).
Why it is worth closing
This is the same failure shape the gate suite already treats as first-class elsewhere: gate-56 register-handler-resolution, gate-14 route-reachability (a route pointing at a method that does not exist), gate-57 orphaned-write-capability. A manifest naming a component that does not exist is precisely gate-14's defect one layer up, and an orphaned registry entry is gate-57's.
It also has the "zero callers has two opposite fixes" property, so the gate should report rather than prescribe: an orphan is either wired or deleted, and the gate cannot know which.
Suggested shape
In check_manifest_crossref.js, when src/registry.js exists:
- extract top-level keys from the
export default { ... } block;
- FAIL on any manifest
component / slot-override value with no matching key (direction 2 — unambiguously broken, renders nothing);
- WARN on any registry key with
kind in section/page/widget that no manifest position names (direction 1 — could be a legitimate deletion candidate, so not a hard fail).
Suppressible with the usual <gate> exclude <reason> for entries resolved by convention rather than by name.
Stopgap
larpingapp#288 adds tests/vitest/manifestRegistry.spec.js covering both directions app-locally, with a can-fail proof. Happy to port that logic into the gate if the shape above is acceptable.
Filed alongside ConductionNL/larpingapp#286, where a registered-but-unreferenced component shipped as unreachable UI long enough for its openspec task to be ticked over it. gates 22 and 53 are the two that exist to catch manifest cross-reference defects, and neither sees this one — in either direction.
Measured — with ajv resolved
Important, because this gate has a wiring failure mode that masks its verdict entirely. Without
NODE_PATHset, onConductionNL/larpingapp@development:With
NODE_PATH=<app>/node_modules, same commit, both go PASS. Everything below is from the ajv-resolved runs — a red-for-wiring baseline would have told me nothing.Direction 1 — orphaned registry entry (the #286 shape)
src/registry.jsexportsEventRoster: { kind: 'section', component: EventRoster }. Notabs[].component, noslots.*, nosections[].componentanywhere insrc/manifest.jsonnames it. The component is resolvable and unreachable; the check-in surface it renders has no entry point.Direction 2 — manifest names a component that does not exist
Control on top of the #286 fix branch: changed one tab to name a component no registry exports.
{ "id": "checkin", "label": "Check-in", "icon": "AccountCheck", "component": "ThisComponentDoesNotExistAnywhere" }At runtime
CnObjectSidebar.resolveTabComponent()logsTab "checkin" component "..." not found in registry or customComponentsand renders nothing — a blank tab.Both silent.
Cause
scripts/lib/check_manifest_crossref.jsnever readssrc/registry.js. Its joins are manifest→manifest (menu-route → page-id, open-page targets, deepLink correspondence, ADR-044 removals) and manifest→register-JSON (register/schema slug resolution). The one place the component registry comes up, it is explicitly declined:src/registry.jsis app code, but it is not opaque — it is a fixed-shape ES module whose top-level export keys are statically extractable with a regex on theexport default {block, which is how the app-local test in larpingapp#288 does it (it cannotimportthe module either, since the file pulls in.vueSFCs).Why it is worth closing
This is the same failure shape the gate suite already treats as first-class elsewhere: gate-56 register-handler-resolution, gate-14 route-reachability (a route pointing at a method that does not exist), gate-57 orphaned-write-capability. A manifest naming a component that does not exist is precisely gate-14's defect one layer up, and an orphaned registry entry is gate-57's.
It also has the "zero callers has two opposite fixes" property, so the gate should report rather than prescribe: an orphan is either wired or deleted, and the gate cannot know which.
Suggested shape
In
check_manifest_crossref.js, whensrc/registry.jsexists:export default { ... }block;component/ slot-override value with no matching key (direction 2 — unambiguously broken, renders nothing);kindinsection/page/widgetthat no manifest position names (direction 1 — could be a legitimate deletion candidate, so not a hard fail).Suppressible with the usual
<gate> exclude <reason>for entries resolved by convention rather than by name.Stopgap
larpingapp#288 adds
tests/vitest/manifestRegistry.spec.jscovering both directions app-locally, with a can-fail proof. Happy to port that logic into the gate if the shape above is acceptable.