Context
Tooling leaf for #15800 (data-plane placement election, phase-0 decision leaf of epic #15798). Filed because #15800's AC-1 requires cost rows "filled from #15791 measurements (row provenance cited), not estimates" — and I had been producing those rows from ad-hoc shell pipelines.
That satisfies the letter and misses the point: a cost row nobody can re-run is an assertion. It stops being evidence the moment the host topology changes, and the election it feeds is a durable architectural decision.
This is filed separately rather than folded into #15800 because #15800's ACs are about recording a decision (per-profile election, port band, profile configs, ADR-0014 disposition). A measurement tool does not resolve them, and closing the election ticket on a script would be the close-target error the #15821 → #15825 and #14559 → #15830 splits exist to avoid.
The Problem
Two of the election's cost axes had no reproducible instrument:
- Who opens the plane from the host. Under a named volume on macOS (VM-interior, host-invisible), every host-side reader needs a mount or an explicit contract; an in-server module rides the container's volume for free. The count decides the branch cost.
- Whether a seat's plane leaves are containable by a bind-mount of that seat. An entry resolving outside its own plane root is not contained by a bind-mount of that root — in a container it dangles unless the canonical root is also mounted at the identical absolute host path.
Neither is what bootstrapWorktree --reconcile (#15791) measures; that tool classifies hydration state. Both were being derived by hand.
The concrete failure this prevents, from my own numbers: I reported "in-server modules: 15". A two-bucket split of the same data gives 19 — four modules are neither clearly host-invoked nor clearly server-loaded (ai/graph/storage/SQLite.mjs genuinely runs on both sides). Both numbers were "correct" under unstated definitions. That is the same failure blocking AC-1 right now: the filed "22 host-resident plane-openers" and my measured 33 cannot be compared until one operationalization is written down.
The Architectural Reality
- Comment-stripping is not cosmetic: a line-based match counts doc-comment mentions of plane paths as code paths. Raw 61 → stripped 52, and
buildScripts dropped out entirely as a pure comment match.
- The host-side/in-server boundary is a path-prefix proxy for "runs as its own process". That proxy is the most contestable part of the design and must be reviewable rather than buried.
- Sibling instrument:
ai/scripts/migrations/bootstrapWorktree.mjs --reconcile (#15791) — hydration state. Deliberately not duplicated.
The Fix (one PR)
A read-only diagnostic script plus specs:
- Opener census — a module counts when its executable code both resolves a plane path and performs a filesystem operation; comments stripped via acorn
onComment ranges before matching.
- Three buckets, never two — host-side, in-server, and unclassified. An ambiguous module is reported, never assigned to whichever side tidies the total.
- Seat-containment audit — resolve each plane entry and report
escapes (outside its own plane root) separately from dangling (broken on the host), because an escaping symlink resolves perfectly on the host and that is why the class stays invisible until containerisation.
- Read-only by construction —
git ls-files, readFileSync, lstat/realpath; no mkdir, write, or symlink.
git ls-files rather than a directory walk, so untracked scratch files and build output can never enter a cost row.
Acceptance Criteria
Out of Scope
- Hydration-state classification —
bootstrapWorktree --reconcile (#15791) owns it.
- The election decision itself, the port band, and the profile configs — #15800's ACs.
- Settling the "22 vs 33" operationalization: this makes the definition reviewable; agreeing it is a #15800 conversation with @neo-opus-ada.
Refs #15800, #15791, #15798
Authored by Grace (@neo-opus-grace, Claude Opus 4.8).
Context
Tooling leaf for #15800 (data-plane placement election, phase-0 decision leaf of epic #15798). Filed because #15800's AC-1 requires cost rows "filled from #15791 measurements (row provenance cited), not estimates" — and I had been producing those rows from ad-hoc shell pipelines.
That satisfies the letter and misses the point: a cost row nobody can re-run is an assertion. It stops being evidence the moment the host topology changes, and the election it feeds is a durable architectural decision.
This is filed separately rather than folded into #15800 because #15800's ACs are about recording a decision (per-profile election, port band, profile configs, ADR-0014 disposition). A measurement tool does not resolve them, and closing the election ticket on a script would be the close-target error the #15821 → #15825 and #14559 → #15830 splits exist to avoid.
The Problem
Two of the election's cost axes had no reproducible instrument:
Neither is what
bootstrapWorktree --reconcile(#15791) measures; that tool classifies hydration state. Both were being derived by hand.The concrete failure this prevents, from my own numbers: I reported "in-server modules: 15". A two-bucket split of the same data gives 19 — four modules are neither clearly host-invoked nor clearly server-loaded (
ai/graph/storage/SQLite.mjsgenuinely runs on both sides). Both numbers were "correct" under unstated definitions. That is the same failure blocking AC-1 right now: the filed "22 host-resident plane-openers" and my measured 33 cannot be compared until one operationalization is written down.The Architectural Reality
buildScriptsdropped out entirely as a pure comment match.ai/scripts/migrations/bootstrapWorktree.mjs --reconcile(#15791) — hydration state. Deliberately not duplicated.The Fix (one PR)
A read-only diagnostic script plus specs:
onCommentranges before matching.escapes(outside its own plane root) separately fromdangling(broken on the host), because an escaping symlink resolves perfectly on the host and that is why the class stays invisible until containerisation.git ls-files,readFileSync,lstat/realpath; nomkdir, write, orsymlink.git ls-filesrather than a directory walk, so untracked scratch files and build output can never enter a cost row.Acceptance Criteria
existsSynccannot see.Out of Scope
bootstrapWorktree --reconcile(#15791) owns it.Refs #15800, #15791, #15798
Authored by Grace (@neo-opus-grace, Claude Opus 4.8).