The mechanical half, riding on the census in the previous commit.
* 342 in-process construction sites in 65 files now take
`new_with_roots` / `open_with_roots` with `iso::roots()`. The isolated
base is a pure function of `CARGO_TARGET_TMPDIR` — no counter, no
`OnceLock` — so two copies of the module in one binary agree instead of
racing, and the tree lives somewhere `cargo clean` owns rather than
leaking into `/tmp` once per run. It is shared deliberately:
materialization is content-gated and idempotent, so a per-test
directory would repeat it ~330 times per run for a byte-identical
result.
* `journey_acceptance` keeps the ambient `EditorState::open`, because
proving the production entry point has a caller is the whole of what
that ratchet is for. Rev 2's "isolated by the environment its binary is
launched with" was not a mechanism — cargo launches each test binary
with the caller's environment, and a binary cannot re-point its own
roots before its tests run. Each test is now a thin parent that
re-execs this binary for its own name with controlled roots, and the
child runs the body against production's call. Two pins guard it: the
child asserts all four roots resolve inside the controlled base, and
the suite asserts against its own source that it has not quietly taken
the seam. The parent also asserts the child ran `1 passed` — a stale
`--exact` filter would otherwise hollow the whole thing out silently.
* The shared spawners take all five storage variables.
`spawn_daemon_process_with_env` set `HOME` and `XDG_CONFIG_HOME` only;
`HOME` is a FALLBACK, so it isolates a root only while the matching
`XDG_*` is unset — the harness's apparent adequacy was a property of
one developer's environment. The PTY spawner backfills whichever of the
five its caller did not pin. The 10 direct `Command::new` daemon and
attach spawns get the same treatment.
Three suites had `mod common;` behind `#[cfg(feature = "crdt")]`;
`common::iso` is needed in every build, so those are ungated. Files that
already pull in `common` reach `iso` through a `use` rather than a second
`#[path]` declaration — loading one file as two modules is
`clippy::duplicate_mod`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Review round 1, findings 2-4 plus doc notes. Finding 1 landed in fd80bcb.
Finding 3 --- spec fields meaningless for the declared type are now
rejected. DEFINE_SPEC_FIELDS whitelists all nine keys for every type and
the kind parser only reads its own arm's fields, so
`{ type = "string", choices = {...} }` silently defined a string that
accepts anything (the author meant enum) and `min` on a boolean was
dropped. These are typo-shaped bugs the R50 whitelist structurally
cannot see: the key is spelled correctly, it is on the wrong type.
`check_fields_relevant_to_kind` closes it with a pointed error naming
the misplaced field, and a companion test pins that each field is still
accepted where it belongs, including `min`/`max` on number as well as
integer.
Finding 4 --- the after_buffer_removed purge had no end-to-end test.
Every existing test called ConfigRegistry::remove_buffer directly, so
deleting the three lines wired into mod.rs would have left the whole
suite green. The new acceptance test kills a buffer through
pmacs.buffer.remove (the real remove_buffer_and_fire route) and asserts
the locals are gone; bite-verified by removing the hunk and watching it
fail.
Finding 2 (the half with a natural buffer) --- editing.trim-on-save is
now resolved against the buffer being saved rather than the global
chain. Reading globally meant set_local was accepted, stored, and
reported by describe, then never consulted: a pin the user believes in
that does nothing, which is the shape F1 exists to prevent. Two tests,
one for the override and one for the global fallback the change could
have broken; the override test fails against the old global read.
Both new save tests initially passed VACUOUSLY and were rewritten:
pmacs.editor.save() is the raw save, while buffer.before-save fires
inside the buffer.save COMMAND (default.lua:224), and save() no-ops on
an unmodified buffer --- so the original form asserted on a file that
was never rewritten. They now insert content to dirty the buffer and go
through pmacs.command.invoke("buffer.save").
The other half of finding 2 --- a per-buffer autosave.interval-ms is
semantically meaningless yet still accepted --- is recorded as a named
deferral proposing a define-time `scope = "global"` flag, alongside
deferrals for bound-parse field naming and StartupOnly reset symmetry.
Also recorded: interval_ms(1e30) now raises instead of storing a
nonsense float, an improvement but a real divergence from "the wrapper's
shape stays exactly as it was".
Doc: the module header cited framing revision 2; the shipped doc is
revision 3, whose corrections are what the code implements.
Gates: fmt, clippy -D warnings, --lib (1691), --lib --features crdt
(1865), lua54 backend, config_registry_acceptance (16), editops (72),
autosave (29), PMACS_REQUIRE_GPU=1 pmacs-gpu (109), and the full
workspace sweep (2806 tests, exit 0). git diff --check clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A third registry beside CommandRegistry and HookRegistry, per
docs/config-registry-framing.md. Unblocks the per-buffer auto-pair
toggle, the first of the five backlog items the missing config surface
was gating.
Substrate (src/config_registry.rs):
* ConfigRegistry keyed by name with definition order preserved, R42
mandatory descriptions, R50 typo detection, duplicate rejection,
and SourceLocation provenance -- the command/hook vocabulary.
* Closed scalar kinds: boolean, integer, number, string, enum. Owned
Rust values; Lua tables, functions and userdata are never stored.
Integer exactness is checked by value, never math.type, so the
luajit and lua54 builds agree.
* Two scopes. get(name, buf) resolves buffer-local -> global ->
default; get(name) with no buffer resolves the global chain only
and never consults an ambient buffer. Buffer-locals live in a
registry-owned side table purged at after_buffer_removed, beside
the keymap purge already there.
* An override is ALWAYS stored, even when equal to the value it
shadows; only value_epoch and listener dispatch key on effective
change. Without this a buffer pinned to the current value stores
nothing and a later global set flips it -- the pin silently never
existed. equal_valued_local_override_is_still_stored_and_shields_buffer
fails against the naive reading.
Bindings (src/lua_bindings/config.rs):
* define/get/set/set_local/reset/is_set/describe/list/on_change.
Spec tables are read raw, so neither an unknown key nor a
metatable-provided value can smuggle a field in.
* Listeners commit inside the borrow, snapshot, drop the borrow, and
only then re-enter Lua -- verified by holding the borrow and
watching the test panic with "RefCell already borrowed". A raising
listener is logged without blocking later ones or rolling back, and
a depth bound turns an accidental cycle into a pointed error.
Listeners persist until explicitly disposed; there is no Gc path,
matching the rest of the codebase.
* StartupOnly freezes off the existing InitCompleteFlag at write
time, so this arc adds no editor.rs call at all.
Adopters, each defining its own key so SourceLocation names the owning
module: editing.auto-pair (pair.lua, read per-buffer against the typed
edit's SOURCE buffer), editing.trim-on-save (editops.lua),
autosave.interval-ms (autosave.lua). No public function is removed or
deprecated, and both migration wrappers keep their legacy coercion --
trim_on_save("yes") still enables, interval_ms(1500.7) still floors to
1500 -- coercing before handing the strict registry a conforming value.
M-x describe-setting renders into *help*, modeled on describe-command.
Framing revision 3 records four defects implementation found in the
document itself: acceptance 30 and 31 contradicted each other; the
planned builtin/runtime/config.lua had nothing to hold and would have
broken the source-location contract had it held the one helper it might
have; F5 asked define to police a call it cannot see, moved to
set_local; and list() ordering was underspecified.
No protocol change; SUPPORTED stays [6..18]. No wire surface. Zero
changes to src/editor.rs.
Gates: fmt, clippy -D warnings, --lib (1683), --lib --features crdt
(1857), the new config_registry_acceptance (13) plus auto_pair (45),
editops (72), autosave (29) and m9_6 (25), m4 --skip basedpyright
(114), PMACS_REQUIRE_GPU=1 pmacs-gpu (109), the lua54 backend build,
and the full workspace sweep (2795 tests, exit 0). git diff --check
clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>