Commit Graph

2 Commits

Author SHA1 Message Date
Levi Neuwirth 9ea522f3cc fix(isolation): the isolation suite must not itself be ambient
Review round 1: `isolated_construction_is_init_complete` asserted its
paired half — that the *ambient* constructor is unchanged — with an
ambient `EditorState::new()` in an ordinary parent test. That reads the
developer's real `init.lua` and materializes packages into their real
data root: the exposure this suite exists to remove, committed by the
suite itself.

The claim is worth keeping, so it moves rather than dies. It now lives in
the re-exec'd positive control, which runs only as a child under a
hostile-by-construction environment. That is the one place an ambient
constructor is safe, and so it is where every ambient claim this suite
makes belongs.

**The ratchet did not catch this, and that is the more important half.**
`ambient_isolation_acceptance.rs` was on the allowlist for its positive
control, and a bare file-level exemption licenses the named file to grow
new ambient sites forever — which is exactly what happened. So every
exemption now carries its **exact permitted site count**, and a file with
more sites than it was reviewed with fails even while allowlisted. A
count that drops fails too, so the allowlist stays a census rather than
drifting into a ceiling nobody rechecks.

The count immediately earned itself: it rejected the number written from
memory for `journey_acceptance` (47) and reported the real one (26 — 19
`new()` + 7 `open(`, after the scanner drops two assertion-message
mentions and the assembled `concat!` needle).

Verified in both directions: restoring the removed ambient site fails the
ratchet with `2 site(s), allowlist says 1`; and with the ambient half
gone, both init-complete pins still fail under the `if roots.is_ambient()`
mutation, so neither has become a test that passes for the wrong reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 19:46:18 -04:00
Levi Neuwirth fcaf0b36fa test(isolation): census, hostile-environment proof, adoption ratchet
The census first, because it decides how large the mechanical edit is
(framing §7). Every occurrence was listed with its enclosing context and
read; a grep for the bare name over-counts, which is how revision 1
reported 18 by grepping `Editor::new` — a pattern that does not match the
real constructor.

  in-process   342 calls in 66 of 97 files
               (330 of 334 `EditorState::new()` occurrences; 4 are prose)
               (12 of 14 `EditorState::open(` occurrences; 2 are strings)
  spawned      14 real `pmacs` spawns in 8 files
               (of 36 `CARGO_BIN_EXE_pmacs` hits, 18 are the fake-LSP and
                fake-MCP siblings and 4 are path derivations for
                `pmacs-gpu`, not spawns)
  mixed        5 files are both, so sites — not files — are the unit

The full census, with per-site attribution, is the module doc of
`tests/ambient_isolation_acceptance.rs`.

Four things it pins:

* Isolated construction still finishes initialization, asserted twice —
  the flag, and the behaviour it gates (`pmacs.attach` must refuse).
  Falsified by wrapping the config block in `if roots.is_ambient()`;
  `m8_2_acceptance` does NOT catch that, because reopening an already-open
  init phase is a no-op.
* The writes land in the redirected data root — content produced, not an
  invariant preserved. A "the real root did not change" check would pass
  vacuously wherever it already holds identical bytes, since
  `write_if_changed` is content-gated.
* Bet 3, in two children with opposite jobs. The positive control proves
  the hostile environment IS hostile (an ambient editor loads its
  `init.lua` and writes its data root); without it the isolation half
  asserts nothing. The isolated child then stays green under the same
  environment and leaves its hostile root byte-identical.
* A durable adoption ratchet, not a one-time census: a source scan that
  fails when a new ambient constructor appears outside a named allowlist,
  plus a check that no allowlist entry has gone dead. Its scanner strips
  comments, strings and raw strings, and that stripping has its own pin —
  the corpus contains all three shapes, and a grep-shaped answer already
  cost this lane a review round.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 18:48:26 -04:00