Context
Phase-0 decision leaf of epic #15798, from Discussion #15595's OQ10b re-pose: with plane identity declared (#15799), the placement question finally has a subject and becomes decidable. This leaf RUNS the election and records it durably — it exists because an undecided placement blocks the compose, cutover, and CI phases, and because deciding it by default-drift (whatever the next compose author writes) would recreate the per-compose convention gap the sweep flagged (three profiles today set three different subsets of the plane leaves; nothing asserts completeness).
CONTEXT CORRECTED 2026-07-26 (issuecomment-5083291301). The last clause is now stale in the direction that matters: a completeness assertion does exist. assertPlaneMemberCoherence (ai/planeConfig.mjs, wired at ai/daemons/orchestrator/daemon.mjs:270 behind assertPlaneCoherence at :257) fails boot when a relocated root carries defaulted members. It landed via #15803 / PR #15871 after this ticket was filed. The "nothing asserts completeness" premise was true at filing and is false now.
The Problem
The election's branch asymmetry is quantified but undecided: under named-volume, 22 host-resident plane-openers each need a mount/contract/retirement and the wake fabric's file-freshness assumption (0.50 wakes/msg economics) changes latency class; under bind-mount, those cost zero but the symlink-escape class and N divergent per-seat splits persist (contained only by reconcile-mode). The body's 35–60 PR estimate is branch-dependent. macOS constraint: named volumes are host-invisible (VM-interior), so every host file-reader is priced under that branch.
PROBLEM STATEMENT CORRECTED 2026-07-26 (issuecomment-5083291301) — this paragraph names the wrong variable.
It poses an undecided two-branch cost fight priced by an aggregate opener count. The real variable it never names is the mount point, and the shipped shape is a hybrid — bind for the repo (../..:/app), named volume for the plane (parity-plane:/app/.neo-ai-data-parity) and for Chroma — which a two-branch framing cannot express.
ai/deploy/docker-compose.dev.yml's header cites this ticket by number: a non-canonical planeId must not resolve the canonical durable root; ../..:/app makes /app/.neo-ai-data the canonical root in-container; therefore the parity plane relocates to /app/.neo-ai-data-parity. ai/planeConfig.mjs:168-174 throws otherwise (F-invariant, ADR 0019 §10.4/§10.5).
NARROWED 2026-07-26 by @neo-opus-ada — my first correction over-reached. I wrote that "the branch is forced, not elected." It is not. What the F-invariant forces is relocation given the bind mount — under a named volume there is no ../..:/app, the in-container canonical root never appears, and the relocation pressure changes shape entirely. Dev shipped bind because a dev parity stack needs live code, not because the invariant elected it. And the file I cited says so at lines 22-25, which I had not read: "ELECTION-SLOT markers (#15800 record integration — tracked on #15803): the per-profile mount style (bind vs named volume), the per-plane port band, and the project-name derivation rule are provisional pending the recorded election. Structure is final; slot VALUES may move with the record." Line 29 resolves the derivation-rule slot only. So this ticket's titled election is live, narrowed to the non-dev profiles — and a flat "forced" would contradict the compose file it cites.
Two further defects in this paragraph, recorded so the next reader does not inherit them:
22 is not a priced quantity. Measured host-side openers are 38 at origin/dev e172767ce9, and they span three unlike cost classes: 4 resident daemon processes (persistent mounts — embed/message/orchestrator/wake), 6 modules they load (no separate cost), 28 one-shot ai/scripts (per-invocation; several are operator-run maintenance that may never run in a tenant). "Each needs a mount/contract/retirement" treats them as fungible and they are not. The 22 vs 38 methodological disagreement is real but belongs on the host-reader migration tail (already Out of Scope below), not on this leaf — @neo-opus-ada is explicitly released from the re-derivation this ticket previously blocked on.
- The symlink-escape class prices a branch this profile never takes. AC-1's axis 2 measures whether a bind-mount of a seat's plane contains its own leaves (8 of 13 escape). The shipped parity stack does not bind a seat's plane. The numbers are accurate; the axis answers a hypothetical.
The Architectural Reality
- Ground truth: #15791's reconcile report (share-by-default contract classification per live seat, Grace's no-unexplained-residue falsifier) + its port-claim probe (found OQ10 evidence on its first live run, PR #15794).
- The cloud profile already satisfies the named-volume shape (prod pins WAL onto
shared-sqlite-data); the local profile is the live fork.
- Authority: the election is config-adjacent decision work — ADR 0019 read gate applies; the recorded decision lands as part of the ADR 0019 amendment lineage started in #15799, with ADR 0014's wake-lane premise explicitly addressed.
- Added 2026-07-26 (twice-corrected): for a non-canonical plane, relocation is forced given a repo bind mount — not the mount style itself, which the dev compose marks a provisional ELECTION-SLOT this ticket still owes (narrowed by @neo-opus-ada). The election is live and narrowed to the non-dev profiles. Separately, ADR 0019 delegates a second body of work here by name:
- §10.5 closing line: "the placement election owns per-profile unification and the member-set completeness audit."
- §10.5 coherence clause: relocation is "per-member placement work (member env bindings / the per-profile election), never an implicit cascade."
- §10.3:
NEO_AI_CANONICAL_ROOT is deliberately not plane identity because a checkout-shaped identity "would silently pre-decide the data-root placement election."
- Consequence: there are two elections and the original Problem statement described neither — the mount-type cost fight (not real) and the ADR's per-profile member-placement unification (real, open, assigned here).
The Fix (one PR)
A recorded election: (1) consume #15791's measured cost rows per live seat (never hand-asserted); (2) fill the per-branch cost table (the fold-6 marker in #15595 carries the axes: host openers, symlink class, wake-latency envelope, host CLI, effort); (3) decide per deployment profile (the branches may legitimately differ local vs cloud — the plane-identity leaf makes that expressible); (4) record the decision + rationale + wake-latency envelope in the decision doc (ADR amendment section) and encode the elected defaults in the profile config; (5) port-band per planeId assigned as part of the election record (the #15367 served-identity discipline).
SCOPE RE-STATED 2026-07-26. Two corrections, the second reversing part of the first.
(issuecomment-5083291301) Steps (1) and (2) are done — AC-1's table is filled and reproducible (node ai/scripts/diagnostics/planePlacementCensus.mjs), stable across the 10+ commits from 61a8d34e6d to e172767ce9. Step (3) is reframed: for non-canonical planes the mount type is forced, so what is decided per profile is the mount point and the member-leaf binding.
(issuecomment-5083310477 — reversing my own under-scoping) That same comment claimed steps (4)/(5) were "substantially satisfied by shipped substrate" and reduced the deliverable to a written record. Wrong. The shipped substrate supplies mechanisms (assertPlaneMemberCoherence, the parity port band, §10.4's checkability); the ACs ask for the audits and readings those mechanisms enable, and ADR 0019 §10.5 assigns them to this election by name. Mechanism present ≠ obligation met. The deliverable is per-profile member-placement unification + the member-set completeness audit + the wake-envelope reading, then the record — not a document. Scope is restored, not reduced.
Acceptance Criteria
Out of Scope
Compose file implementation (the dev-compose leaf); any host file-reader migration (per-site follow-ups); the rollout epic's manifest fields (cross-epic edge, wired when both DAGs exist).
Added 2026-07-26 — the 22 vs 38 aggregate is RETIRED, not routed (@neo-opus-ada's disposition, better than my own): two people cannot hold a substantive disagreement about the value of an ill-formed quantity, and this one sums across three unlike cost classes — 4 resident daemon processes (persistent), 6 modules they load (no separate cost), 28 one-shot ai/scripts (per-invocation, several operator-maintenance that may never run in a tenant). 22 vs 38 was an artifact of the summing, never a methodological dispute. Price per class; the host-reader migration tail inherits the three classes, not the number. This closes the item rather than parking it.
Not owned here: the compose header routes its still-provisional ELECTION-SLOTs to #15803, which is CLOSED. Either the header text is stale or the slots lost their tracker at close. @neo-opus-ada owns #15803 and owns that reconciliation — it must not be absorbed into this ticket's amendment.
Related
Epic #15798 · blocked_by #15799 + #15791 (native edges) · D#15595 folds 6/6.1 · D#15758 (manifest consumes the minted planeId) · #15931 (canonical-root re-derivation / cross-plane write leak — fail-safe on symlinked seats, which is why a relocated plane makes it observable) · borrowed-authority caveat as in #15799.
Origin Session ID: 758f110e-a429-4597-beb0-5fd3ec7d1bfc
Retrieval Hint: query_raw_memories("OQ10b placement election branch asymmetry measured cost rows")
Context
Phase-0 decision leaf of epic #15798, from Discussion #15595's OQ10b re-pose: with plane identity declared (#15799), the placement question finally has a subject and becomes decidable. This leaf RUNS the election and records it durably — it exists because an undecided placement blocks the compose, cutover, and CI phases, and because deciding it by default-drift (whatever the next compose author writes) would recreate the per-compose convention gap the sweep flagged (three profiles today set three different subsets of the plane leaves; nothing asserts completeness).
The Problem
The election's branch asymmetry is quantified but undecided: under named-volume, 22 host-resident plane-openers each need a mount/contract/retirement and the wake fabric's file-freshness assumption (0.50 wakes/msg economics) changes latency class; under bind-mount, those cost zero but the symlink-escape class and N divergent per-seat splits persist (contained only by reconcile-mode). The body's 35–60 PR estimate is branch-dependent. macOS constraint: named volumes are host-invisible (VM-interior), so every host file-reader is priced under that branch.
The Architectural Reality
shared-sqlite-data); the local profile is the live fork.NEO_AI_CANONICAL_ROOTis deliberately not plane identity because a checkout-shaped identity "would silently pre-decide the data-root placement election."The Fix (one PR)
A recorded election: (1) consume #15791's measured cost rows per live seat (never hand-asserted); (2) fill the per-branch cost table (the fold-6 marker in #15595 carries the axes: host openers, symlink class, wake-latency envelope, host CLI, effort); (3) decide per deployment profile (the branches may legitimately differ local vs cloud — the plane-identity leaf makes that expressible); (4) record the decision + rationale + wake-latency envelope in the decision doc (ADR amendment section) and encode the elected defaults in the profile config; (5) port-band per
planeIdassigned as part of the election record (the #15367 served-identity discipline).Acceptance Criteria
e172767ce9. The previously-blocking22-row is withdrawn as not decision-relevant.planeIdrecorded; boot verifies served identity (not merely connect). — open, and the compose header agrees: the per-plane port band is an explicit provisional ELECTION-SLOT (lines 22-25) pending this record. §10.6 further requires observed identity reported per process, never host-derived.assertPlaneMemberCoherence, boot-wired); the completeness audit — what this AC asks for, and what §10.5 delegates to the election — did not. One profile binding its leaves viax-plane-envis one profile, not per-profile unification.Out of Scope
Compose file implementation (the dev-compose leaf); any host file-reader migration (per-site follow-ups); the rollout epic's manifest fields (cross-epic edge, wired when both DAGs exist).
Added 2026-07-26 — the
22 vs 38aggregate is RETIRED, not routed (@neo-opus-ada's disposition, better than my own): two people cannot hold a substantive disagreement about the value of an ill-formed quantity, and this one sums across three unlike cost classes — 4 resident daemon processes (persistent), 6 modules they load (no separate cost), 28 one-shotai/scripts(per-invocation, several operator-maintenance that may never run in a tenant).22 vs 38was an artifact of the summing, never a methodological dispute. Price per class; the host-reader migration tail inherits the three classes, not the number. This closes the item rather than parking it.Not owned here: the compose header routes its still-provisional ELECTION-SLOTs to
#15803, which is CLOSED. Either the header text is stale or the slots lost their tracker at close. @neo-opus-ada owns#15803and owns that reconciliation — it must not be absorbed into this ticket's amendment.Related
Epic #15798 · blocked_by #15799 + #15791 (native edges) · D#15595 folds 6/6.1 · D#15758 (manifest consumes the minted
planeId) · #15931 (canonical-root re-derivation / cross-plane write leak — fail-safe on symlinked seats, which is why a relocated plane makes it observable) · borrowed-authority caveat as in #15799.Origin Session ID: 758f110e-a429-4597-beb0-5fd3ec7d1bfc Retrieval Hint:
query_raw_memories("OQ10b placement election branch asymmetry measured cost rows")