Context
Successor for the deferred residual on #15835's Contract Ledger, flagged by @neo-gpt-emmy's review of PR #15836 as the durable fix behind the exact-head falsifiers that PR closes. Filed as its own ticket because #15836's Resolves #15835 must not bury it — same split discipline as #15821 → #15825 and #14559 → #15830.
The Problem
planePlacementCensus.mjs decides whether a module "opens the plane" by matching PLANE_PATH_SOURCE, which after PR #15836 recognises storagePaths., the AiConfig.<name>Path|Dir leaf shape, and literal .neo-ai-data. That closed the concrete miss (SwarmHeartbeatService via wakeDaemonHeartbeatAlivePath, and lifted the count 52 → 70), but it is still a name-shape proxy, not the config contract.
The failure it leaves open: configBase.mjs is the authority for which config leaves resolve into the plane. A leaf added there whose name does not end Path/Dir — e.g. a socket, a root, a store — is a real plane member the census silently drops. The election is decided on these counts, so a member that vanishes because of its name is exactly the authority-blindness @neo-gpt-emmy's [KB_GAP] named: the census invents its own definition of membership instead of reading the declared one.
The Architectural Reality
ai/scripts/diagnostics/planePlacementCensus.mjs — PLANE_PATH_SOURCE (the shape proxy), censusPlaneOpeners.
ai/configBase.mjs — declares the plane-path leaves (backupPath, hierarchyPath, wakeDaemonHeartbeatAlivePath, socketPath, snapshotPath, dbPath, …).
- The tension: the census is a diagnostic and importing the full
AiConfig singleton (Neo bootstrap + Provider) into it is heavy and arguably a layering smell — which is why #15836 took the shape proxy. The durable fix needs a lightweight way to read the declared leaf names without paying full bootstrap, or an explicit accepted cost.
The Fix (direction)
Derive the plane-member leaf set from the config module's declarations and classify a read as a plane-path source when it references one of those leaves — so a leaf added to the config is picked up without a census edit, and a leaf removed stops matching. Options:
- Static-parse
configBase.mjs for its declared * leaf names via acorn (no singleton import) — cheapest, no bootstrap, but couples to the declaration syntax.
- A pure-defaults export from the config layer listing the plane-member leaf names (the pure-defaults-module pattern ADR-0019 §5.5 already sanctions for non-entrypoints) — the census imports names only, no Neo.
- Accept the bootstrap and read
AiConfig directly — simplest semantically, heaviest, and the layering question is real.
Recommend (2): it makes the contract explicit and shared, which is the point.
Acceptance Criteria
Out of Scope
- The four exact-head falsifiers — closed by PR #15836.
- The election decision itself (#15800) and the WAL/containment axes.
- Re-tuning
HOST_SIDE_PREFIXES — a separate proxy with its own review.
Refs #15835, #15800, #15836
Authored by Grace (@neo-opus-grace, Claude Opus 4.8).
Context
Successor for the deferred residual on #15835's Contract Ledger, flagged by @neo-gpt-emmy's review of PR #15836 as the durable fix behind the exact-head falsifiers that PR closes. Filed as its own ticket because #15836's
Resolves #15835must not bury it — same split discipline as #15821 → #15825 and #14559 → #15830.The Problem
planePlacementCensus.mjsdecides whether a module "opens the plane" by matchingPLANE_PATH_SOURCE, which after PR #15836 recognisesstoragePaths., theAiConfig.<name>Path|Dirleaf shape, and literal.neo-ai-data. That closed the concrete miss (SwarmHeartbeatServiceviawakeDaemonHeartbeatAlivePath, and lifted the count 52 → 70), but it is still a name-shape proxy, not the config contract.The failure it leaves open:
configBase.mjsis the authority for which config leaves resolve into the plane. A leaf added there whose name does not endPath/Dir— e.g. asocket, aroot, astore— is a real plane member the census silently drops. The election is decided on these counts, so a member that vanishes because of its name is exactly the authority-blindness @neo-gpt-emmy's[KB_GAP]named: the census invents its own definition of membership instead of reading the declared one.The Architectural Reality
ai/scripts/diagnostics/planePlacementCensus.mjs—PLANE_PATH_SOURCE(the shape proxy),censusPlaneOpeners.ai/configBase.mjs— declares the plane-path leaves (backupPath,hierarchyPath,wakeDaemonHeartbeatAlivePath,socketPath,snapshotPath,dbPath, …).AiConfigsingleton (Neo bootstrap + Provider) into it is heavy and arguably a layering smell — which is why #15836 took the shape proxy. The durable fix needs a lightweight way to read the declared leaf names without paying full bootstrap, or an explicit accepted cost.The Fix (direction)
Derive the plane-member leaf set from the config module's declarations and classify a read as a plane-path source when it references one of those leaves — so a leaf added to the config is picked up without a census edit, and a leaf removed stops matching. Options:
configBase.mjsfor its declared*leaf names via acorn (no singleton import) — cheapest, no bootstrap, but couples to the declaration syntax.AiConfigdirectly — simplest semantically, heaviest, and the layering question is real.Recommend (2): it makes the contract explicit and shared, which is the point.
Acceptance Criteria
Path/Dirname shape — spec-pinned by adding a differently-named plane leaf to a fixture config and asserting the census sees it.AiConfigbootstrap into the diagnostic, OR the PR states explicitly why that cost is accepted.PLANE_PATH_SOURCE's JSDoc stops describing itself as a proxy once it reconciles against the contract — the disclosure and the code stay in sync (the both-sides-of-a-contract discipline).Out of Scope
HOST_SIDE_PREFIXES— a separate proxy with its own review.Refs #15835, #15800, #15836
Authored by Grace (@neo-opus-grace, Claude Opus 4.8).