LearnNewsExamplesServices
Frontmatter
id15932
titlePLANE_MEMBER_PATHS is guarded by a pinned count, not the config tree
stateClosed
labels
bugaiarchitecture
assigneesneo-kimi-phoebe
createdAtJul 25, 2026, 8:46 PM
updatedAtJul 25, 2026, 10:23 PM
githubUrlhttps://github.com/neomjs/neo/issues/15932
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 25, 2026, 10:23 PM

PLANE_MEMBER_PATHS is guarded by a pinned count, not the config tree

Closed Backlog/active-chunk-9 bugaiarchitecture
neo-opus-grace
neo-opus-grace commented on Jul 25, 2026, 8:46 PM

Context

Raised by @tobiu while reviewing PR #15930: "ai/configBase.mjs contains PLANE_MEMBER_PATHS … is this something you recommend to keep, and if so why?"

Measuring it produced a sharper answer than the question assumed. Tier-1's list is currently accurate — 10 anchor-derived member leaves, 10 entries — so this is not a bug report about tier-1. But #15872 (@neo-fable-clio) already documents the same list failing in the memory-core copy: storagePaths.graphProd is absent from its 10-entry list, so the boot member-coherence clause never checks the plane's single most important durable artifact.

One list correct, a sibling list silently wrong, same construction. That makes this a class, and #15872 is its first confirmed instance rather than an isolated miss.

The Problem

Three hand-maintained lists (ai/configBase.mjs, ai/mcp/server/knowledge-base/configBase.mjs, ai/mcp/server/memory-core/configBase.mjs) enumerate, by string path, which config leaves resolve into the plane. ADR-0019 §10.5's boot assertion walks exactly what they claim.

The guard on those lists is entirely self-referential. From BaseServer.spec.mjs:

// census, not coherence: a silent PLANE_MEMBER_PATHS deletion must fail this line.
expect(TIER1_MEMBER_PATHS.length).toBe(10);
expect(server.getPlaneMembers().length).toBe(TIER1_MEMBER_PATHS.length);

Line 1 compares the list to a literal. Line 2 compares the list to itself resolved. Neither compares it to the config tree it claims to describe.

The failure that survives, and it is the direction that actually happens. Add a leaf whose default is path.resolve(planeDataRootDefault, …) and do not touch PLANE_MEMBER_PATHS: the array length is unchanged, both assertions pass, CI is green — and a real plane member sits outside the boot coherence assertion looking guarded. Adding a leaf is the common operation; editing the membership list is the one people forget. The guard covers the rare direction and misses the common one.

That is not hypothetical. It is what #15872 found, in the copy nobody was reading.

Second-order: expect(length).toBe(10) is a pinned member of a set the codebase will keep growing. Each new member turns the spec red, the author bumps 10 → 11, and the edit is green while proving nothing about correctness — the same anti-pattern corrected in #15887 / PR #15888, where an allowlist self-test pinned a member instead of asserting an invariant over the set.

The Architectural Reality

  • ai/configBase.mjs:1543 — tier-1 PLANE_MEMBER_PATHS (10 entries); 10 leaves derive from planeDataRootDefault (:137, :143, :157, :286, :602, :687, :699, :830, :1192, :1193), plus the anchor leaf itself at :88.
  • ai/mcp/server/knowledge-base/configBase.mjs:547, ai/mcp/server/memory-core/configBase.mjs:886 — the sibling lists.
  • test/playwright/unit/ai/mcp/server/BaseServer.spec.mjs:899 — the pinned-count census.
  • ai/planeConfig.mjscollectPlaneMembers({memberPaths, resolvedConfig, descriptorData}) and assertPlaneMemberCoherence; both take the claimed list as input and cannot know what it omits.
  • ai/mcp/server/BaseServer.mjs:581 — where the claim is consumed at boot.
  • ADR-0019 §10.5 — the member-coherence clause the lists feed.

The Fix

Make membership checkable against the config tree rather than asserted about itself.

Two shapes, and the ticket should pick one during intake rather than pre-committing here:

  1. Completeness invariant (smaller). A spec or lint asserting: every leaf whose resolved default lies under the plane anchor appears in its owning configBase's PLANE_MEMBER_PATHS, or is explicitly recorded as a non-member with a reason. Replaces toBe(10) with a statement that survives 10 → N and fails on the omission direction.
  2. Membership on the leaf (larger, likely better). leaf(default, env, type, {planeMember: true}), with the exported list derived from the descriptor tree. Declaration and membership become one act, so a plane-anchored leaf cannot be added without deciding. This also removes the duplication @tobiu was pointing at — the list stops being a second enumeration of the config and becomes a projection of it.

Why the list should not simply be deleted and fully derived: §10.5 distinguishes "resolves beneath dataRoot" from "explicitly placed" — a member legitimately relocated by its own env binding. That is declared intent, and a purely derived set cannot express it. Membership must stay declared; only its verification should become mechanical.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
PLANE_MEMBER_PATHS (×3) ADR-0019 §10.5 Stays a declared export; gains a mechanical completeness check against the descriptor tree n/a — no runtime behavior change if lists are already complete ADR-0019 §10.5 #15872 is a live counter-example proving the check would fire
BaseServer.spec.mjs census this ticket Pinned toBe(10) replaced by a set-level invariant n/a spec comment #15888 precedent for member-pin → invariant
leaf(..., metadata) (only if shape 2) ADR-0019 §5 item 2 Optional planeMember metadata key absent = non-member ADR-0019 §5 metadata.parse precedent from #15914

Decision Record impact

amends ADR 0019 — §10.5 currently specifies what a claimed member must satisfy; it does not require the claim to be complete. This adds the completeness half. No conflict with the accepted decision; it closes a gap the clause assumes away.

Acceptance Criteria

  • A mechanical check fails when a leaf whose default resolves under the plane anchor is absent from its owning PLANE_MEMBER_PATHS.
  • The check is proved by a red control: add a plane-anchored fixture leaf without listing it, observe failure; the check that was never red is not a proof.
  • expect(TIER1_MEMBER_PATHS.length).toBe(10) is replaced by a set-level invariant that does not require editing on every legitimate membership change.
  • All three lists are covered, not only tier-1 — a fix that covers one copy reproduces the defect in the other two.
  • The "explicitly placed" case (a member relocated by its own env binding) still passes; the check must not force every member under the anchor.
  • ADR-0019 §10.5 records the completeness half alongside the coherence half.

Out of Scope

  • The graphProd membership gap itself and the anchor/sibling divergences around it — #15872, assigned to @neo-fable-clio. This ticket is the mechanism that would have caught it; that ticket is the instance. Fixing the instance without the mechanism leaves the next omission silent.
  • Consumers re-deriving the canonical root — #15931.
  • The census script's name-shape proxy — #15842. Adjacent (both decide membership by something other than the config contract) but a different artifact and a different deliverable.

Avoided Traps

  • Deleting the list and deriving membership entirely. Loses §10.5's "explicitly placed" distinction, which is declared intent rather than a derivable property.
  • Bumping toBe(10) to toBe(11) when it next goes red. Green, cheap, and reproduces the defect forever — the member-pin trap from #15888.
  • Fixing tier-1 only because it is the file the question was asked about. Tier-1 is the one that is currently correct; the sibling copies are where the class already bit.

Related

#15872 (the instance, @neo-fable-clio), #15931 (consumer re-derivation), #15842 (census name-shape proxy), #15887 / PR #15888 (member-pin → invariant precedent), #15914 (leaf metadata precedent), ADR-0019 §10.5.

Live latest-open sweep: checked latest 20 open issues at 2026-07-25T18:47Z plus a targeted "plane member" search; #15872 and #15931 reviewed in full and are the instance/consumer halves, not this mechanism. A2A in-flight sweep: 8 most recent messages; active claims are film lanes, #15803 takeover, and review traffic — none on plane-membership verification.

Origin Session ID: 26e73986-66fa-4d28-9b02-6053541a5671

Retrieval Hint: "PLANE_MEMBER_PATHS completeness invariant pinned count plane member coherence"