Finding 1: a BufferSnapshot wipes the frontend's buffer-scoped
render state (spans, decorations, adornments, minimap summary,
completion popup), but the producer's per-buffer emission baselines
survived the switch -- on an unchanged A -> B -> A round trip,
last_summary[A]'s key still matched and the daemon emitted nothing,
so the frontend never regained A's themed minimap (or A's
StatusFacts: the band kept B's name) until an edit, republish, or
theme mutation happened to move the key.
The fix is the general contract, not a minimap special case:
SemanticRenderState::on_buffer_snapshot_sent(buffer_id) kills every
buffer-scoped baseline for that buffer (spans + style gate,
decorations, adornments, summary, status, search/menu prompts,
completion popup), called wherever the daemon writes a snapshot --
the active-buffer-follow path and the F29 upgrade broadcast; the
attach bootstrap constructs its session state fresh. Deliberately
surviving: the bufferless ThemeFacts pair, the global minibuffer
baseline, the per-frontend gutter mode, the revision-keyed diag
line cache, and other buffers' baselines.
Evidence: a producer round-trip acceptance test (themed summary and
StatusFacts return at the SAME generation; identical payload), a
real-daemon wire test driving A -> B -> A via dispatched keys
(runtime bite: times out against pre-fix daemon.rs), a Rust unit
pinning the reset's scope, and a GPU test where the re-shipped
summary restores the first visit's pixels exactly (frontend half --
no GPU code change, coverage only). The semantic_render.rs bite is
compile-fail (the hook is absent pre-fix), disclosed as weaker.
The protocol doc's composition section now states the snapshot
reset contract on both sides of the wire.
Finding 2: the acceptance-suite manifest header now lists the true
item split (1-19, 24-26, 28-29 here; 20-23, 27, 30 in the GPU
suite). Framing revision 6 folds the round; acceptance items 28-30.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Finding 1: accepting a FileStyleSummary drops the GPU minimap vertex
cache -- theme recolors and diagnostic republishes arrive at an
unchanged generation, and the cache keys only on (generation, dims,
scroll), so stale strokes survived until an edit/resize/scroll. The
daemon payload-suppresses identical summaries, so the invalidation
is precise. GPU test drives two same-generation summaries;
hand-bitten by reverting the single invalidation line (script-bite
is vacuous here: fix and test share main.rs).
Finding 2: lua_to_style propagates every Table::get error -- the
lookups run __index, so a raising metatable previously parsed as an
all-default style and the merge SUCCEEDED, committing valid siblings
against the Q#TH6 all-or-nothing contract. Boolean fields keep Lua
truthiness by design (mlua bool), so only raising lookups fail the
transaction. Acceptance reproduces the reviewer's trap shape;
runtime bite vs pre-fix mod.rs.
Finding 3: SemanticRenderState::for_peer records the negotiated
version; below v16 no ThemeFacts is produced and no ui.diag.* face
folds into the FileStyleSummary marks -- the summary is an ungated
pre-v16 channel, and a v15 peer must not get face-derived minimap
colors while its other severity surfaces stay unthemed. The summary
cache key zeroes its face-epoch component for such peers. Acceptance
drives v15/v16 producers side by side; compile-fail bite disclosed
(the test needs for_peer, absent pre-fix).
Finding 4: framing revision 5 weakens the canonical-severity claim
to what is true -- the daemon-RESOLVED color is canonical, while the
GPU's built-in squiggle/sign/counter defaults are historical bright
RGBs that differ from the minimap's converted Indexed marks, a
pre-existing divergence kept because unset faces must render
byte-identically to before.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Two theme.lock() calls inside one tuple expression self-deadlock:
the first guard lives to the end of the full expression, so the
second lock blocks the same thread forever. Observed as an 84-minute
hung lib suite; one lock per observation via a helper closure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
- §1: `main` @ 7d1b6d6; new arc bullet covering the grammars (every
LSP-configured language now has one), the extension → filetype →
filename → shebang detection chain and its user-extensible Lua
surfaces, the dockerfile/cmake LSP configs, and the substrate
(`highlights_query: &[&str]` base-first composition; fail-closed
`#is?`/`#is-not? local` handling in `compute_highlight_spans`).
Condensed the merged compile-mode/editops/auto-pairing blocks to
substrate essentials. Fixed the stale "#114 is the USER'S lane" note:
themes (Arc 4) is the user's active `theme-faces` lane.
- §3: folded the confirmed flakes (m6_1_pty, composition_overhead) into
the rerun-isolated note, plus the single-invocation sweep caveat.
- §6: new deferrals — locals-query processing, multi-language
injections (blocks notebooks / markdown code / HTML-in-JS), modeline
detection, JSON/YAML, byte-accurate cursor; a full Jupyter setup is a
real arc, not a one-shot.
Docs only. #118 is approved, merge pending. git diff --check clean.
Non-blocking review nit: the shadowing test only proved the `.builtin`
captures disappeared. Also assert both `console` occurrences retain an
ordinary `@variable` capture, so dropping the locals-predicate pattern
loses only the `.builtin` refinement, not all styling for the token.
Test-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
[P2] The shared JavaScript highlights query guards its builtin captures
(console, require, …) with `#is-not? local`, a PROPERTY predicate
(`Query::property_predicates`) that needs a scope map from the grammar's
LOCALS_QUERY — which pmacs does not run. `compute_highlight_spans` took
every capture, so a locally-shadowed `console`/`require` still surfaced
as `@variable.builtin`/`@function.builtin`; a theme distinguishing
`.builtin` would mis-style the shadowed local.
Full locals processing is substrate work; conservatively fail-closed
instead: drop captures whose pattern carries an `#is?`/`#is-not? local`
property predicate (the identifier falls back to its non-builtin
capture). The text predicates (`#eq?`/`#match?`/`#any-of?`, already
applied by the capture iterator) and `#set!` settings are untouched.
This is a general engine fix — it corrects the same latent mis-styling
for any grammar using the locals predicate, not just JS/TS.
- javascript_shadowed_builtin_is_not_mislabeled: a local `const console`
produces no `*.builtin` capture (directly observed to fail — two
`variable.builtin` captures — before the fix).
[P3] Comments this PR invalidated: `lsp.lua` no longer claims Python has
no grammar; `syntax.lua`'s `_has_language` gate comment uses a
still-grammarless example (an init.lua `shebangs.ruby`) instead of
python/javascript; and the rewritten Ruby shebang test's doc no longer
describes it as a Python test.
Gates: fmt; clippy -D warnings; test --lib; --features crdt;
m4_acceptance --skip basedpyright; GPU; full workspace sweep;
git diff --check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
These five languages already had LSP configs (basedpyright, gopls,
tsserver, taplo, zls) but shipped no tree-sitter grammar, so they
rendered with no lexical color. Fill the gap — 8 BUILTIN_LANGUAGES
entries across 6 crates:
- python (`tree-sitter-python`, root `module`), go (`tree-sitter-go`),
toml (`tree-sitter-toml-ng`), zig (`tree-sitter-zig`, +`.zon`) — each a
single self-contained highlights query.
- JavaScript/TypeScript family: `tree-sitter-javascript` parses both
`.js` and `.jsx`; `tree-sitter-typescript` ships two grammars
(LANGUAGE_TYPESCRIPT, LANGUAGE_TSX). The four entries — javascript,
javascriptreact, typescript, typescriptreact — mirror the LSP filetype
map so tsserver enables the JSX parser. Highlights inherit: the TS
query is a ~5-capture delta over JavaScript and JSX is a further
delta, so the entries compose base-first (js → jsx → ts), the same
pattern as `cuda` over C/C++ (typescript resolves ~22 capture classes,
typescriptreact ~24).
Each grammar's name equals its existing `pmacs.lsp.config.<name>` key,
so grammar detection (which wins over the filetype map) resolves the id
the server keys off — the file now gets BOTH highlighting and the right
server. No lsp.lua change needed. All crates ride `tree-sitter-language
0.1` with tree-sitter dev-only — no second core in the graph.
Bite-verified acceptance:
- gap_grammars_load_and_parse — each grammar's ABI accepted by the 0.26
core; a snippet parses without error at its root (covers both TS
grammars, incl. JSX).
- typescript_highlights_compose_the_javascript_base — the compiled
typescript/typescriptreact queries resolve >= 15 captures, not just the
~5-capture TS delta (the JS base is really composed in).
- builtin_languages_include_gap_grammars /
gap_grammar_extensions_resolve — entry presence + extension detection
across all 8 ids.
- m4_gap_grammars_align_with_lsp_configs — through the loaded runtime,
each path's grammar id matches an existing LSP config. Bite-verified
against pre-feature src/syntax.rs.
Ripple: two #116 shebang tests used python as their "has-LSP-but-no-
grammar" example, which this PR invalidates. Updated both — the .py +
`#!/bin/sh` precedence test now asserts a python grammar tree (not "no
tree"), and the grammarless-language-is-silent gate test switches to
`ruby` (genuinely grammarless) via a test-local shebang mapping.
Gates: fmt; clippy -D warnings; test --lib; --features crdt;
m4_acceptance --skip basedpyright; GPU; full workspace sweep;
git diff --check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
cmake-language-server does NOT pull a `workspace/configuration` section:
it reads `buildDirectory` from the `initialize` request's
`initializationOptions`, and drives its project model off CMake's File
API under `<buildDirectory>/.cmake/api/` (not `compile_commands.json`).
The `settings = { cmake = {} }` block — and the documented
`settings.cmake.buildDirectory` override — were therefore inert, leaving
conventional out-of-source project data unavailable.
Replace it with `init_options = { buildDirectory = "build" }` (the
conventional out-of-source dir; users override `init_options.buildDirectory`
from init.lua), and correct the comment. The wiring test now asserts
`config.cmake.init_options.buildDirectory == "build"` — bite-verified
against the pre-fix lsp.lua.
Gates: fmt; clippy -D warnings; --features crdt (1720); m4_acceptance
--skip basedpyright (109); GPU (59); full workspace sweep (zero
failures); git diff --check — all green. One `--lib` run flaked on
process::m6_1_pty_mode_lifecycle_started_then_exited (PTY-lifecycle
timing, the m6/m8 daemon-timing family); it passed in the crdt run, the
full sweep, and 4/4 isolated — unrelated to this Lua config change.
Change is Lua config + the acceptance assertion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
Files identified by their whole basename — Dockerfile, Makefile,
CMakeLists.txt, rc dotfiles — had no detection path (extension-, then
shebang-keyed). Add a filename layer and the three grammars behind it.
- **Grammars** (BUILTIN_LANGUAGES): dockerfile via `tree-sitter-containerfile`
(the ABI-current grammar; the old `tree-sitter-dockerfile` pins
`tree-sitter ^0.20` and would fork the graph — containerfile rides
`tree-sitter-language 0.1`, tree-sitter dev-only, like the others),
make via `tree-sitter-make`, cmake via `tree-sitter-cmake`. All ship
self-contained highlights (single fragment). Extensions:
`.dockerfile`/`.containerfile`, `.mk`/`.make`, `.cmake`.
- **Filename layer**: `pmacs.parse.language_from_filename(name)` backed by
an extensible `pmacs.parse.filenames` map, wired into the precedence
chain in both syntax.lua (grammar) and lsp.lua (LSP): grammar-ext →
filetype map → filename → shebang. A recognized extension still wins;
the basename map only fires when the extension misses. Seeds the three
filenames plus shell rc dotfiles (`.bashrc`/`.zshrc`/`PKGBUILD`/… →
bash) — highlighting them against the grammar shipped in #115.
- **LSP**: `config.dockerfile` (docker-langserver --stdio) and
`config.cmake` (cmake-language-server). Make has no server, so no
`config.make` — grammar highlight only. Extension filetype fallbacks
added for id stability.
Bite-verified acceptance:
- filename_grammars_load_and_parse — each grammar's ABI is accepted by
the tree-sitter 0.26 core and parses a representative snippet without
error (dockerfile/cmake root at source_file, make at makefile).
- builtin_languages_include_dockerfile_make_cmake /
language_for_path_resolves_dockerfile_make_cmake_extensions — entry
presence and extension detection.
- m4_filename_map_resolves_special_files — the basename map (incl. path
form and dotfiles→bash), config.dockerfile/cmake commands, and no
config.make. Bite-verified against pre-feature syntax.lua.
- m4_filename_extensionless_dockerfile_highlights — an extensionless
`Dockerfile` resolves to dockerfile for LSP and gets a dockerfile parse
tree; reachable only via the filename map. Bite-verified.
Gates: fmt; clippy -D warnings; test --lib; --features crdt;
m4_acceptance --skip basedpyright; GPU; full workspace sweep;
git diff --check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
Syntax.rs theme-handle line refs re-verified; the new shell grammar
introduces no ui-named capture, so the Q#TH2 reservation holds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Arc 4 stage 1 (docs/theme-faces-framing.md, revision 4). Faces are
theme entries under the reserved ui/ui.* namespace -- zero new Lua
API. Theme::face() resolves with the dotted-prefix walk but never
falls back to default_style; each face applies owns-surface within
its stage-1 component mask, identical on both frontends.
Substrate: two monotonic theme mutation counters (syntax/face) with
transactional set/merge/clear/default (parse before locking, commit
all-or-nothing, bump from the prior value); the StyleGate and the
minimap summary key on the counters -- fixing the pre-existing bug
where a mid-session pmacs.theme.set never re-shipped StyleSpans --
with the summary gaining payload-equality suppression that still
advances its key on computation.
Wire: InstanceMessage::ThemeFacts appended after CompletionPopup
(postcard discriminants are ordinal; a byte pin guards placement),
PROTOCOL_VERSION 15 -> 16, daemon-gated >= 16, one authoritative
table per attachment (None-seeded baselines), TUI silent-drop arm.
Grid: paint_frame resolves ui.modeline / ui.statusline /
ui.minibuffer(.candidate) / ui.gutter / ui.selection faces;
SearchView and DiagnosticView take the theme handle through the real
attachment paths (EditorCore injection, install_diag threading); the
canonical severity color resolves ui.diag.* with the Default ->
built-in policy that keeps the minimap presence encoding sound.
GPU: exact-name face table applied per draw with the Q#TH5 Default
mapping (plain text / window bg, reverse swap), local/peer wash
split, candidate-dropdown glyph site, and the status-band
shaping-cache invalidation without which a diag-face recolor with
constant counts kept stale counter colors.
Tests: 18-test acceptance suite (grid, wire, daemon gate, atomicity,
monotonicity, late join), 7 GPU headless tests incl. decoded vertex
colors, units for the face walk / transactional commits / producer
caches; protocol pins for v16 + the CompletionPopup byte pin.
Bites vs 3cbb9de (scripts/bite): semantic_render.rs (8 runtime test
failures), editor.rs (5 runtime), daemon.rs (v15 gate, runtime);
lua_bindings/mod.rs, pmacs-gpu/main.rs, search.rs, diag.rs, and
highlight.rs bite as compile failures (weaker evidence, disclosed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
The attached split-string payload (`-Spython3`, `--split-string=...`) can
itself begin with env options or VAR=value assignments before the
interpreter: `-S-i python3`, `-SFOO=bar python3`,
`--split-string=-u FOO python3`. Rather than taking the payload's first
word as the interpreter, re-inject the attached payload into the token
stream so it flows through the same option / operand / assignment state
machine as a separated payload. Adds the three cases as resolver tests.
Two follow-ups from review, both in builtin/runtime/syntax.lua.
1. [P2] Buffer switching bypassed the pinned grammar. after-edit already
reparsed the pinned language, but the after-switch reattach path
(attach_for_active_buffer) re-resolved from scratch — so open an
extensionless `#!/bin/sh` (bash), edit its shebang to lua, switch away
and back, and the grammar flipped to lua while the LSP side kept its
bash attachment (lsp.lua's after-switch reuses the existing record).
attach_for_active_buffer now reuses the language pinned at first attach
whenever a parse view already exists; only a first-seen buffer
resolves. A language change still needs a close/reopen, matching both
the after-edit behavior and how extensions work.
2. [P2] Attached `env -S`/`--split-string` forms failed. The walk skipped
the whole option token, but for split-string the interpreter rides
inside it: `-Spython3`, `-vSpython3` (after no-operand short flags
i/v/0), and `--split-string=python3` all resolved to nil (the last was
also eaten by the earlier `=` branch). The env walk now extracts the
interpreter from the attached value (`^-[iv0]*S(.+)$` /
`^--split-string=(.+)$`); the separated forms (`-S python3`) still work
by walking on to the next token.
Tests (bite-verified against the round-1 syntax.lua — both fail there;
scripts/bite HEAD builtin/runtime/syntax.lua):
- m4_shebang_edit_keeps_pinned_grammar now adds a switch-away/back cycle
(via pmacs.window.switch_buffer, which fires after-switch
synchronously) and asserts the tree stays bash.
- m4_shebang_resolver_maps_interpreters adds the attached split-string
cases (`-Spython3`, `--split-string=python3`, `-vSpython3`).
Gates: fmt; clippy -D warnings; m4_acceptance --skip basedpyright; GPU;
git diff --check green. Only-known-flake caveat as round 1
(editor::composition_overhead_under_ten_percent perf microbenchmark,
unrelated to this Lua change). Change is Lua-only plus the acceptance
tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
Three review findings, all in builtin/runtime/syntax.lua.
1. [P1] Syntax bypassed extension precedence and grammar availability.
attach_for_active_buffer resolved `language_for_path or shebang`, but
language_for_path knows only grammar-backed extensions — so a `.py`
file opening with `#!/bin/sh` fell through to the shebang and got a
bash parse tree, and an extensionless `#!/usr/bin/env python3` script
dispatched "python" (no grammar) and raised "unknown language". A new
resolve_active_language walks the full precedence chain — grammar
extension -> LSP filetype map -> shebang — consulting the shebang only
when the extension is unrecognized (a recognized non-grammar extension
like .py is authoritative). Dispatch is then gated on
pmacs.parse._has_language(lang), so grammarless languages are skipped
silently. The extension parts stay keyed on buf:name() (unchanged from
before), so path-less buffers that resolve a grammar by name — e.g.
generated markdown buffers — are unaffected.
2. [P2] Editing an open script's shebang left parsing/highlighting stale.
The after-edit path re-sniffed the mutable shebang: sh -> python
raised "unknown language" while leaving the old bash tree, and
sh -> lua swapped the parse tree under a highlight overlay still
holding the original grammar's query. Reparse now uses the language
pinned at first attach (parse_lang_by_buffer), never re-resolving —
a language change needs a close/reopen, as it does for extensions.
3. [P2] `env` options with operands were mistaken for interpreters.
`#!/usr/bin/env -u FOO python3` skipped `-u` but took `FOO`. The env
walk now skips the operand of the operand-consuming GNU-env options
(-u/--unset, -C/--chdir, -a/--argv0) before selecting the interpreter.
-S/--split-string stays excluded (its string carries the interpreter).
Tests (bite-verified against pre-fix syntax.lua — each fails without its
fix; scripts/bite HEAD builtin/runtime/syntax.lua):
- m4_shebang_does_not_override_extension now also asserts _has_view is
false (no bash grammar tree for a `.py` + `#!/bin/sh`), not only the
LSP language.
- m4_shebang_extensionless_grammarless_language_is_silent — extensionless
python resolves for LSP, gets no grammar view, and records no error.
- m4_shebang_edit_keeps_pinned_grammar — rewriting a `#!/bin/sh` script's
shebang to lua keeps the bash tree and reports no error.
- m4_shebang_resolver_maps_interpreters — added the env-operand cases
(`-u FOO`, `-C /tmp`, combined).
Gates: fmt; clippy -D warnings; m4_acceptance --skip basedpyright; GPU;
git diff --check all green. The only sweep failure is the pre-existing
editor::composition_overhead_under_ten_percent render microbenchmark
(ratio hovers at the 1.10 cutoff; flakes ~1/3 even isolated single-
threaded, already asserted-off on macOS) — a pure-Rust render loop this
Lua-only change cannot touch. Change is Lua-only plus the acceptance
tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
Extension detection missed extensionless scripts — `scripts/deploy`, git
hooks, `configure`, and `scripts/bite` itself — so they got neither
highlighting nor an LSP server. Add a first-line shebang fallback.
- New `pmacs.parse.language_from_shebang(buf)` (builtin/runtime/syntax.lua):
sniffs the first line (capped at 256 bytes), maps the interpreter's
basename to a language, and resolves the `#!/usr/bin/env python3`
indirection (skipping env's own `-S`/flags and `VAR=val` assignments).
Backed by `pmacs.parse.shebangs`, a user-extensible map seeded with the
interpreters pmacs can act on: sh-family -> bash, python* -> python,
node -> javascript, lua* -> lua.
- Wired as a strict *fallback* on both resolution paths: syntax.lua's
grammar attach (`language_for_path or language_from_shebang`) and
lsp.lua's `buffer_language` (grammar -> filetypes -> shebang). A
recognized extension always wins, so a `.py`/`.sh` file is never
re-classified by a stray shebang.
- Cross-language, not shell-only: `#!/usr/bin/env python` /`node` /`lua`
resolve too. Special filenames (`.bashrc`, `Dockerfile`, `Makefile`)
are intentionally deferred until there are grammars behind them.
Bite-verified acceptance (tests/m4_acceptance.rs):
- m4_shebang_resolver_maps_interpreters — the mapping incl. env
indirection and `env -S`; non-shebangs and unmapped interpreters
(ruby) resolve to nil.
- m4_shebang_extensionless_script_resolves_bash — opening an
extensionless `#!/bin/sh` script resolves to bash on BOTH paths:
lsp.lua's `active_buffer_language()` and a settled bash parse tree
(grammar attach). Reachable only via the shebang, since the file has
no extension.
- m4_shebang_does_not_override_extension — a `.py` file opening with
`#!/bin/sh` still resolves to python (extension precedence).
Gates green: fmt; clippy -D warnings; test --lib; --features crdt;
m4_acceptance --skip basedpyright; GPU; full workspace sweep;
git diff --check. Change is Lua-only plus the acceptance tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
Shell scripts already had LSP (bash-language-server + shellcheck/shfmt,
wired in builtin/runtime/lsp.lua), but no tree-sitter grammar, so their
text rendered without lexical color. Fill in the missing half.
- Bundle tree-sitter-bash (0.25) as a BUILTIN_LANGUAGES entry. Unlike
cuda, bash's highlights.scm is self-contained (no `; inherits:`
delta), so a single fragment suffices. The crate exports
LANGUAGE/HIGHLIGHT_QUERY over tree-sitter-language 0.1 — shared ABI
crate, no second tree-sitter in the graph.
- Extension set is wider than the `.sh`/`.bash` the LSP filetype map
covered: `.zsh`/`.ksh`/`.ash` are close-enough dialects and `.bats`
is bash. The grammar's language name is `bash`, matching the
`pmacs.lsp.config.bash` key, so opening any of these also auto-attaches
bash-language-server (shellcheck declines zsh, so `.zsh` diagnostics
may be sparse; highlighting is unaffected). lsp.lua's filetype map is
extended to the same set as the belt-and-suspenders fallback.
- Extensionless shebang scripts (`#!/bin/sh`) and rc dotfiles
(`.bashrc`) are intentionally NOT covered: detection is extension-keyed
and shebang/filename sniffing is a separate, deferred feature.
Bite-verified acceptance:
- bash_grammar_loads_and_parses_script — the 0.25 grammar's ABI is
accepted by the 0.26 core (set_language succeeds at runtime) and a
representative script (shebang, set, parameter expansion, function,
if) parses without error, rooting at `program`.
- builtin_languages_include_bash / language_for_path_resolves_bash_
extensions — entry presence and detection across the wider set.
- bash_highlights_compile_with_captures — the self-contained query
compiles against the grammar with real capture classes.
- m4_12_default_bundle_wires_bash — through the loaded runtime,
config.bash targets bash-language-server and both grammar detection
and the filetype fallback resolve the new extensions to `bash`.
Gates green: fmt; clippy -D warnings; test --lib; --features crdt;
m4_acceptance --skip basedpyright; GPU; full workspace sweep;
git diff --check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
Two functional gaps from review:
1. Standalone .cuh files got no clangd AST. clangd selects the
compiler language from the file extension, not the LSP languageId:
it knows .cu (-> -x cuda) but not .cuh, so a header with no compile
command fails with fe_expected_compiler_job. config.cuda now sets
init_options.fallbackFlags = { "-xcuda" }, which supplies -x cuda
for any file this server opens that lacks a compile_commands.json
entry (a real compile command still wins). This CUDA server only
ever serves .cu/.cuh, so the fallback cannot mis-flag C/C++.
2. The CUDA highlights query was only a delta. tree-sitter-cuda's
HIGHLIGHTS_QUERY opens with `; inherits: cpp` and defines only the
CUDA-specific captures (launch brackets, __global__/__device__) —
two capture classes. pmacs does not resolve `inherits:`, so ordinary
C/C++ syntax went unhighlighted. LanguageEntry.highlights_query is
now &[&str] (fragments joined base-first); the cuda entry carries
[c, cpp, cuda], compiling to ~16 capture classes. Fragments are
newline-joined, never bare-concatenated — a fragment can end mid
`; comment`, and abutting the next fragment's first token would
corrupt the query. Existing single-query grammars become one-element
slices (byte-identical effective query; no behavior change).
Tests:
- cuda_highlights_resolve_c_and_cpp_captures — asserts the COMPILED
cuda query carries the C base `@variable` capture and >= 8 capture
classes, not merely a non-empty query (the CUDA delta alone has 2 and
no `variable`, so this fails without the base prepend).
- builtin_languages_include_cuda — now asserts the entry composes the
c + cpp + cuda fragments.
- m4_12_default_bundle_wires_cuda — now asserts
config.cuda.init_options.fallbackFlags[1] == "-xcuda".
Gates green: fmt; clippy -D warnings; test --lib (1515); --features crdt
(1689); m4_acceptance --skip basedpyright (101); GPU (59); full
workspace sweep; git diff --check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
Section 1 snapshot: main @ 98323df, seven PR review rounds folded
(framing revision 13 + the user's atomic-teardown commit), the
shipped shape and the new substrate (process-group lifecycle,
buf:revision, errors.claim, observable parser finish with
emitted_style resync, and the buffer-attached style-span translator
stack). Themes (Arc 4) remains the standing next arc; PR #114
(cuda-lsp) is the user's own lane.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Opening a .cu/.cuh file previously resolved to no language, so no
server attached and there was no highlighting. Wire CUDA end to end,
mirroring the existing C/C++ path:
- Bundle tree-sitter-cuda (0.21) as a new BUILTIN_LANGUAGES entry
claiming .cu/.cuh, with its own HIGHLIGHTS_QUERY. A dedicated grammar
rather than reusing cpp: the C++ grammar errors on the
<<<grid, block>>> kernel-launch syntax. The crate rides
tree-sitter-language 0.1 (its tree-sitter dep is dev-only), so it
shares the ABI crate with the other grammars — no second tree-sitter
in the graph.
- pmacs.lsp.config.cuda targets clangd (the same binary that serves
C/C++; language_id "cuda" so clangd enters its CUDA parse mode), and
.cu/.cuh filetype fallbacks map to "cuda" to keep the LSP id stable
if the grammar is ever dropped. LspStyleView layers clangd's CUDA
semantic tokens on top, exactly as for C/C++.
Bite-verified acceptance:
- cuda_grammar_loads_and_parses_kernel_launch — proves the 0.21
grammar's ABI is accepted by the 0.26 core (set_language succeeds at
runtime, which the compile step cannot confirm) and that the entry
wired the CUDA grammar, not a cpp fallback: the <<<...>>> launch
parses without error, whereas the cpp grammar reports an error on the
same source (verified out of band).
- builtin_languages_include_cuda / language_for_path_resolves_cuda_
extensions — entry presence and .cu/.cuh detection.
- m4_12_default_bundle_wires_cuda — config.cuda targets clangd and the
filetype + grammar detection resolve to "cuda" through the loaded
runtime.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
Finding-by-finding (framing revision 13; bites via scripts/bite
against fe04aa4):
1. attach_style_overlay validates the handle. A handle's translator
follows edits to ITS buffer only, so attaching it to another
buffer created a render view showing spans nobody maintains —
rejected now, with the message naming the recorded owner and
pointing at add_style_overlay for the target buffer. A disposed
handle's translator is gone, so re-attachment resurrected
rendering with frozen coordinates — the disposed state is shared
across handle clones (FromLua clones) via Arc<AtomicBool> and
attachment after dispose() fails, pointing at add_style_overlay
for a fresh handle. Bite: r7f1 pins cross-buffer rejection,
same-buffer acceptance, dispose-then-attach rejection, and both
message shapes.
2. dispose() detaches the translator through the always-registered
SharedRegistry; only the window cleanup rides the optional
SharedCore. Pre-fix all cleanup lived inside the SharedCore
branch, so an install-only/headless host got success with the
translator left attached — paying on every edit for the buffer's
lifetime. Registry-only unit asserts the buffer's view count
returns to baseline (and stays there on double dispose); the
acceptance-crate twin r7f2 builds the same install-only host and
bites via the mod.rs swap (the in-crate unit vanishes with it).
Gates: fmt; clippy workspace all-targets; lib 1535; crdt lib 1709;
compile acceptance 65; crdt acceptance 3; m4 101; m6.4 15; m6.5 11;
m6.8 8; GPU 59; workspace sweep 2526/0; git diff --check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Finding-by-finding (framing revision 12; bites via scripts/bite
against a49adc2):
1. Render-view attachment is idempotent and split-complete. Overlays
expose overlay_identity (the span store's allocation address);
Window::ensure_overlay attaches a store-backed render view AT
MOST once per window — pre-fix every switch into the buffer
blindly pushed another copy onto EVERY matching window, so
passive panes accumulated duplicates, each cloning all spans and
rescanning the buffer per frame. A same-buffer split copies
clonable overlays to the new pane via clone_for_split (splits
fire no switch hook and started with an empty overlay list — the
new compilation pane rendered unstyled). Bites: the acceptance
test asserts both panes styled with exactly one attachment
IMMEDIATELY post-split (before any switch could heal the pane
through the attach-to-all path — the first draft asserted only
after bouncing and was vacuous against the split fix), then
re-asserts after three bounce cycles; fails against pre-fix
editor_core.rs (split half) and pre-fix mod.rs (accumulation
half) independently. Units pin ensure-once and split-copy/no-copy.
2. The translator ignores pure no-op edits (buffers deliberately
broadcast empty inserts/deletes for callers that count calls):
pre-fix each interior no-op split the containing span into two
adjacent fragments — unbounded list growth for repeated no-ops at
distinct positions, and a no-op at a UTF-8 continuation byte
minted a mid-codepoint span boundary. Units now cover genuine
EditOp::Insert (the round-5 "insertion" unit only replaced) and
no-ops at five interior positions including the continuation
byte; the Lua twin (r6f2) bites via the overlay.rs swap — as a
compile failure, since that file also carries the round-6
identity machinery (weaker evidence, per the bite script's
caveat; the in-crate unit pins the behavior directly).
3. StyleOverlayHandleLua retains the buffer and translator ViewId
and exposes idempotent dispose(): detaches the buffer-attached
translator (later edits stop paying for it) and removes every
window render view over the store. Documented lifetime contract:
one handle per buffer incarnation (the compile/REPL discipline)
needs no disposal — the buffer's death frees it; repeated
creation on a long-lived buffer must dispose retired handles.
Bite: r6f3 (translate → dispose → edit must NOT move the span,
render views gone, double-dispose safe) fails against pre-fix
mod.rs.
Gates: fmt; clippy workspace all-targets; lib 1534; crdt lib 1708;
compile acceptance 63; crdt acceptance 3; m4 101; m6.4 15; m6.5 11;
m6.8 8; GPU 59; workspace sweep 2523/0 (one m8-class flake, clean on
rerun); git diff --check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Finding-by-finding (framing revision 11; bites via scripts/bite
against 6793edc):
1. Style-span coordinate translation belongs to the BUFFER. A new
BufferStyleSpanTranslator is attached by
pmacs.buffer.add_style_overlay and sees every edit exactly once —
bypass writes, undo/redo, remote CRDT ops — independent of window
count or visibility; the window-attached BufferStyleOverlay
copies are render-only (on_edit removed). Pre-fix each attached
view translated the shared store: start_run's explicit attach
duplicated the after-switch hook's (switch_buffer fires it
synchronously), so the normal path shifted later spans TWICE per
byte-delta rewrite, splits multiplied further, and a hidden
buffer shifted ZERO times. The redundant attach is removed;
correctness no longer depends on attachment discipline. Bites:
per-cell rendered assertions active (red a, blue bc, CR, red é →
é red, b/c blue) and hidden (run finishes with the buffer in no
window; switch back renders true colors); three direct units pin
exactly-once with extra render views attached.
2. Translation preserves the untouched fragments of a partially
overlapped span: left of the replaced range keeps its styling,
right of it shifts by the length delta, only the rewritten bytes
lose theirs (the writer styles what it writes; inserted bytes
inherit nothing). Pre-fix any overlap dropped the WHOLE span —
red abc, SGR reset, CR, X left bc unstyled; zero translation
painted the default X red instead. Bite: exact (glyph, fg) cells
X=default, b/c=red — any_styled_cell cannot see either failure.
3. The per-CR/BS/erase-line whole-prefix scan is gone:
slot.line_start is tracked — advanced at every \n (append helper
+ the mid-line newline branch), read O(1) by the rewind paths,
reset on run start/resync/raw marker appends. Measured on 2 MB of
output + 3000 CR updates (release): 2.52s pre-fix → 0.67s
post-fix (remainder is fixture-bound; pre-fix cost grows with
buffer size). No correctness bite is possible for a pure perf fix
— the committed test pins the tracked value's behavior across
multi-line appends, batch-boundary CR, repeated CR, erase-line,
and recovery paths, and passes on both implementations by design.
Gates: fmt; clippy workspace all-targets; lib 1531; crdt lib 1705;
compile acceptance 60; crdt acceptance 3; m4 101; m6.4 15; m6.5 11;
m6.8 8; GPU 59; workspace sweep 2517/0; git diff --check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Finding-by-finding (framing revision 10; bites via scripts/bite
against b5bbce8):
1. CR rewrites are COLUMN-counted and newline-segmented, not
byte-counted. Each newline-free segment of a text event consumes
one existing codepoint per incoming codepoint (codepoints
approximate columns; double-width and combining characters count
as one — the documented stance), and LF is not an overwrite
column: a newline arriving mid-line drops the cursor to a fresh
line and the stale remainder survives in place (terminal
semantics). Pre-fix, abcdef\rX\n wrote "X\n" over "ab" — splitting
the line and leaving "cdef" as a ghost line the parser saw again
at EOF — and abc\ré ate two ASCII columns because é is two bytes.
Round-3's UTF-8 invariant holds per-segment: every edit's range
ends sit on codepoint boundaries, so the rope is valid after each
step and byte-native CRDT edits never reject. Bites: single-batch
(shorter rewrite, multibyte-over-ASCII, CRLF), split-feed with the
é split across batches, and a CRDT twin covering the segmented
multi-edit replication.
2. Alternate-screen exits resynchronize the effective style. The
parser now tracks the style the consumer LAST RECEIVED
(emitted_style; outside alt-screen it always equals
current_style). An ordinary ?1049l exit emits the resync SetStyle
whenever suppressed SGR changes drifted the two apart, and
finish() balances against emitted_style rather than
current_style — a suppressed SGR reset inside the alt screen left
the internal style default, so the old comparison saw nothing to
balance while the consumer stayed red. Consumer-mirror units for
both drift directions plus the no-drift no-event case; Lua twin
(r4f2) bites via the ansi.rs swap.
Gates: fmt; clippy workspace all-targets; lib 1528; crdt lib 1702;
compile acceptance 56; crdt acceptance 3; m4 101; m6.4 15; m6.8 8;
GPU 59; workspace sweep 2510/0; git diff --check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
The hostile-metatable half of r3f3 spawned /bin/true, which exists on
Linux but not macOS (true lives at /usr/bin/true there), so the spawn
failed with NotFound and the pcall absorbed it — failing the "raw
reads must not trip a raising __index" assert on both macOS flavors.
Use the suite's /bin/sh -c idiom instead. The r1f6 /bin/true specs
stay: their type errors fire in spec parsing before any exec, and the
asserts pin the message text, so the binary there is inert on every
platform.
Bite re-verified: against pre-fix src/lua_bindings/mod.rs the test
still fails at the pgid assert (metatable-provided group=true
honored), so the fixture change keeps its teeth.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Finding-by-finding (framing revision 9; bites via scripts/bite):
1. The CR/backspace renderer is UTF-8-safe: overwrite ranges consume
WHOLE existing codepoints (range end aligned forward past
continuation bytes) in ONE atomic replace of the complete text
event — never a split of either side — and backspace steps to the
previous codepoint boundary; out_pos stays on boundaries by
induction. Pre-fix, byte-counted splits left malformed bytes on
the plain rope, and under CRDT the byte-native edit rejected the
mid-codepoint range, aborting the pump after events_take had
consumed the batch (terminal event lost, record leaked). Bites:
default acceptance (é\rX, X\ré, é\bX with exact-content, marker,
clean-*errors*, baseline asserts) and a CRDT twin that pre-fix
times out never reaching its exit marker.
2. parser:finish()'s reset is observable: balancing events —
AlternateScreenExit for an unclosed enter, a default SetStyle for
a non-default running style (now also cleared; reset() preserved
it) — let consumers unwind mirrored state from the event stream
alone. New Rust unit applies events to consumer state; Lua twin
(r3f2) bites via the ansi.rs swap.
3. stdin/group spec fields are RAW reads: spec tables are plain
data, metatable-provided fields are deliberately not honored (the
compile.lua rawget posture), and a raising __index can no longer
be silently absorbed as group=false, quietly disabling
process-group isolation. Regression test pins both shapes:
metatable-provided group=true is ignored (pgid != pid), and a
hostile raising metatable spawns cleanly.
Gates: fmt, clippy workspace all-targets, lib 1526, crdt lib 1700,
compile acceptance 53, crdt acceptance 2, m4 101, GPU 59, workspace
sweep 2505/0, git diff --check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
macOS/luajit failed shutdown_force_kills_outstanding_ledger_groups
with "survivor alive pre-shutdown": on a slow scheduler the leader
(`( trap '' TERM; ... ) & echo $! > pidfile`) can exit before the
backgrounded subshell installs its trap, so the leader-exit
group-TERM kills the "survivor". Linux wins that race consistently;
macOS runners don't. The same race made three sibling tests
vacuously green when it fired (a dead survivor trivially satisfies
"survivor dies" and trivially bounds the drain).
Fix: a shared fixture (survivor_script / survivor_cmdline) writes a
readiness file immediately after `trap` and the leader busy-waits on
it before exiting — the trap is provably installed before any
group-TERM can be sent. Applied to the three process.rs unit
fixtures and the acc08/acc09 acceptance twins.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Two supervisor unit tests failed on the macOS CI matrix (both Lua
flavors; Linux green):
- pgid_of read /proc/<pid>/stat, which has no macOS equivalent — now
probes via `ps -o pgid=` (portable, still avoids widening the nix
feature set with `process` for getpgid).
- the setsid escape-hatch test requires util-linux's setsid(1),
absent on macOS — now skips per-test when setsid isn't on PATH
(the m6_5 selective-skip precedent); the escape hatch is a
Linux-production behavior and the other group-lifecycle tests
still run everywhere.
Also fixed while here: the acceptance suite's pid_alive was a /proc
existence check, which on macOS made every "descendant is dead"
assertion vacuously TRUE (passing, but toothless) — now a portable
`kill -0` probe, so the group-kill assertions bite on both OSes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Finding-by-finding (framing revision 8; bites via scripts/bite):
1. Rule validation is a stable, total snapshot: validated scalar
fields are copied into per-run plain tables via raw reads
(rawget; metatable-provided fields deliberately not honored), so
post-run mutation of the user's rule objects cannot alter an
in-flight run and a hostile __index is a counted skip, not an
error thrown through the pump mid-batch. The container traversal
is itself pcall-protected; traversal-raise semantics are
Lua-flavor-dependent (5.2+ ipairs consults __index, LuaJIT reads
raw) and the test pins both flavors.
2. Capture indexes must be FINITE (floor(math.huge) == math.huge, so
integrality alone passed it); math.huge is now a counted
malformed entry.
3. Shell-command never touches the rule table: no spurious
compile-rule warnings on M-!, and no rule-container state can
block a run that performs no parsing.
4. AnsiParser::finish() (and parser:finish()) now fully resets the
parser — in-flight CSI/OSC/escape state and alt-screen
suppression included — so a post-finish feed parses a fresh
stream. Three direct unit tests in ansi.rs plus a Lua-driven twin
in the acceptance suite (the twin exists because a scripts/bite
file swap replaces the in-file units along with the fix).
5. Comment corrections: fractional capture indexes read a distinct
absent key (not a neighboring capture); the group-coercion
comment describes truthiness, not false; the AnsiParserLua
rustdoc lists finish().
Bites: r2f1 (both shapes), r2f2, r2f3 fail against pre-fix
compile.lua; r2f4 fails against pre-fix ansi.rs. Gates: fmt, clippy
workspace all-targets, lib 1525, crdt lib 1699, compile acceptance
50, crdt acceptance 1, m4 101, GPU 59, workspace sweep 2501/0 (one
flaky-suite rerun per the standing m8 rule), git diff --check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Finding-by-finding (framing revision 7; every fix bite-verified via
scripts/bite against the pre-fix tree):
1. Stored coordinates must be finite integers, and both cursor walks
are movement-bounded — they clamp at EOF, and the column walk
clamps at the target row's EOL instead of marching onto later
rows. An astronomical %d+ capture can no longer hang the editor.
2. The grep panel gains the same immediate buffer.after-edit
recovery trigger as the compile slots: M-x buffer.undo after a
COMPLETED search is marked synchronously.
3. The rustc arrow rule uses the framing's ([^:]+) spelling — paths
with spaces capture whole.
4. All pattern captures are collected (index 4+ reads the real
capture, not nil-as-column-0); capture indexes must be positive
integers; a rule naming a column its match didn't produce rejects
the match.
5. emit_text_raw is module-local — a user global could shadow the
helper the terminal-event path depends on, and its error consumed
the terminal event before pump cleanup/forget ran.
6. stdin/group spec fields reject wrong Lua types as hard errors;
group is matched as a raw Value because mlua's bool conversion
applies Lua truthiness ("true" would silently coerce).
7. resync also nils the public line_start_byte — total pre-marker
anchor invalidation includes the byte anchor.
8. The inherited cwd resolves through
pmacs.instance.identity().working_directory; the header always
names a real path and relative error files get an explicit base.
9. New AnsiParser::finish() + parser:finish() (additions #5): a
truncated multibyte sequence at process EOF surfaces as U+FFFD
before the exit marker instead of vanishing.
10. The built-in default rules are a private deep copy — in-place
mutations of the public table no longer survive the "using
built-in defaults" degradation.
Eleven new tests (r1f1a/b–r1f10); bites: 9 fail against pre-fix
compile.lua, r1f2 against pre-fix default.lua, r1f6 against pre-fix
lua_bindings/mod.rs — all clean assertion failures. Gates: fmt,
clippy workspace all-targets, lib 1522, crdt lib 1696, compile
acceptance 45, crdt acceptance 1, m4 101, GPU 59, workspace sweep
2493/0, git diff --check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
cargo fmt over the new files; doc-markdown backticks; is_ok_and in
the recompile counter wait; m4_6's M-g n/p pin updated to the Q#CM5
takeover contract (error.next/error.previous with the diag commands
as the dispatchers' fallback — the test's no-attachment status
behavior is unchanged). Handoff §1: main @ 0efb5cd, compile-mode
branch in flight at framing revision 6, themes named as the
standing runner-up.
Gate results on this machine (laptop, basedpyright live): fmt,
clippy --workspace --all-targets, lib 1522, crdt lib 1696,
compile_mode_acceptance 34, compile_mode_crdt_acceptance 1,
m4_acceptance 101 (no skip), PMACS_REQUIRE_GPU gpu 59, workspace
sweep 2482/0, git diff --check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
tests/compile_mode_acceptance.rs (34 tests): spawn shape + header +
exit markers; read-only under dispatch; child-boundary stderr merge
in emission order; stdin EOF; group kill/leader-exit/escalation/
ledger bites incl. the redirected TERM-ignoring survivor and the
pipe-holding-descendant tick-latency bound; starter-rule parsing
with 0-based normalization and severity posture; sub-1 fail-closed;
severity override + malformed-rule containers; unterminated final
line; RET/n-p/M-g n/M-g p/C-x ` navigation pins with the diag
fallback; recompile + q-target discipline; supersede baseline; all
seven undo/redo chords table-driven; M-x undo after a completed run
recovering via buffer.after-edit; no-hook shrink and same-length
newline-moving replace with anchor epochs; ANSI SGR/CR with
rendered-cell attachment proof surviving RET-then-M-,; killed-buffer
teardown; grep locations panel, kill-mid-search + masking
prevention, root retention; shell-command M-!; round-trip pins.
tests/compile_mode_crdt_acceptance.rs: a chord-triggered full run
converges byte-identically on two replicas (mid-session generated-
buffer snapshot adoption), and a synthetic accepted replica edit
triggers the immediate recovery marker, converging across the
causal-reorder seam.
Fixes found by the suite: compile.lua's CR handling now scans the
current line start from the buffer (the REPL discipline) instead of
using the per-batch parse position — a same-batch CR previously let
a progress line overwrite earlier output; malformed Lua patterns
are rejected (and counted) at validation time via a probe match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
builtin/runtime/compile.lua (Q#CM1-CM6, CM8-CM11): streaming
intercept-read-only *compilation* / *shell-command* slots fed by a
Lua-side ANSI parser (SGR to overlay spans, CR/BS/erase progress
collapse); once-per-newline error parsing over a validated,
fail-closed rule table (rustc arrows, gcc/clang, Python, generic;
severity override + keyword sniff; sub-1 captures discarded);
buffer-revision external-edit guard with desync marker and anchor
epochs, checked before every producer write, before byte-anchor
use, and immediately via buffer.after-edit; unified error.next /
error.previous dispatcher with last-claim-wins sources and a
diagnostics fallback (M-g n/p unbind-then-rebind — hence the
loader's ordering contract after lsp.lua; C-x ` bound; M-! bound);
buffer-local RET/n/p/g/q/C-c C-k plus all seven undo/redo chords as
status no-ops; tombstoned pump teardown honoring forget's
terminated-only contract; q-target never captures a generated
buffer; overlay retained per incarnation, cleared per run,
re-attached from buffer.after-switch.
builtin/commands/default.lua (Q#CM7): project.search's
*search-results* becomes a first-class locations buffer — read-only
with bypass writes, RET/n/p/q + undo no-ops + round-trip input,
structured-match locations (line-1, match_start as col, paths
resolved against the search root), per-write revision checks so a
batch cannot mask an external edit, on_removed stream cancel +
guards for kill-mid-search, root retention across interactive
supersedes from inside the pathless panel, and an error-source
claim per search.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Supervisor (Q#CM3, framing additions 1-2): ProcessSpec gains
stdin="null" (Stdio::null, no writer thread, immediate EOF) and
group=true — process_group(0) spawn, group-directed fatal signals,
liveness-probed TERM-to-KILL reap ledger (insert-if-absent arming,
per-tick kill(-pgid,0) probe, GROUP_TERM_GRACE=500ms), leader-exit
group TERM before the final drain with in-drain deadline enforcement
plus ESRCH quiescence window and absolute cancel cap, poll-based
cancellable readers (nix poll feature added), shutdown ledger
force-kill + probe-to-ESRCH, maybe_restart gated once shut_down.
Both options are pipe-mode-only and rejected at spawn under PTY.
Nine unit tests cover framing acceptance 34, including the
TERM-ignoring redirected survivor, the pipe-holding descendant tick
bound, and setsid-escapee resource reclamation via the per-runtime
active-reader counter.
Bindings (additions 3-4): buf:revision() exposes the edit revision
(bumped by edit/undo/redo — unit-pinned); pmacs.editor.jump_back now
fires buffer.after-switch exactly when the jump changed buffers,
matching pmacs.window.switch_buffer; pmacs.process.spawn parses the
stdin/group spec keys.
Deviation from the framing letter, called out for review: the
active-reader counter field is always present (one Arc + two atomics
per reader lifetime) rather than cfg(test)-gated — gating the field
would spread cfg attributes through every construction site; only
the probe accessor is test-gated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Revision 6 after five pre-branch review rounds. Scope: M-x-style
compile.run with a streaming *compilation* buffer and error-regex
navigation; grep-mode as an upgrade of project.search; M-! shell
command. Shared machinery: streaming intercept-read-only output
buffer with a revision-guarded external-edit recovery protocol, and
a claim-based error-source dispatcher with diagnostics fallback.
Supervisor additions: stdin="null" and group=true pipe-mode spec
options — the latter a full lifecycle policy (process_group(0)
spawn, group-directed signals, liveness-probed TERM-to-KILL reap
ledger integrated into shutdown, bounded in-drain enforcement,
poll-based cancellable readers). Binding additions: buf:revision(),
jump_back buffer.after-switch parity.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
A trap-guarded one-file swap over read-only git show: run the named
tests against an older version of one file and exit 0 iff they FAIL
there (the fix bites); exit 1 flags a vacuous fix. The file is
restored on every exit path including interrupts; the ./ pathspec
prefix makes it work from any directory inside the repo. Never
touches git stash — the stash namespace is repo-global, shared
across worktrees and with humans (a #111-round mishap popped a
PR-#17-era user stash; hence the tool).
Dogfooded three ways against the merged #111 history: the round-1
fix commit bites (exit 0), the same tests against HEAD read as
vacuous (exit 1), and a docs/-relative invocation restores cleanly.
Handoff: section 1 snapshot (editops #111 merged, worktree folded),
section 5 lessons (repo-global stash; worktree-per-lane for
parallel work).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vF4gQVozBWi38y1SJiGfQ
Finding 1: codepoint recognition is now full UTF-8 scalar validation
(shared second-byte constraint table: overlongs, surrogates, and
beyond-U+10FFFF all fail), and transpose validates the scalar AT the
cursor trailing-bytes-included — a valid lead with non-continuation
trailing bytes fails closed, as does a length-consistent overlong or
out-of-range span behind the cursor. Zap's single-codepoint check
uses the same validator as defense-in-depth (minibuffer contents
arrive as Rust-side UTF-8; the buffer-facing checks are the
load-bearing ones).
Finding 2: capitalize is per-word across the span — Emacs
capitalize-region parity, verified against Emacs 30.2 ("hello WORLD"
-> "Hello World", "9abc a9bc" -> "9abc A9bc"); the one remaining
deviation is named and pinned: `_` is a word constituent in this
pack's ASCII class, so "foo_bar" -> "Foo_bar" versus Emacs's
"Foo_Bar".
Finding 3: an unexpected error caught by the trim-on-save outer
pcall is no longer discarded — it reports on the status line AND the
*errors* buffer via pmacs.error (the autosave sweep convention),
both pcall'd, still never vetoing the save.
All three fixes bite-verified: the five new/updated acceptance cases
fail against the pre-fix editops.lua (72 total now). Framing at
revision 6.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vF4gQVozBWi38y1SJiGfQ
Snapshot: main @ 4174f3e, Arc 2 closed with #110 (framing revision 6,
two pre-branch + three PR rounds). Next action is the compile-mode vs
themes decision discussion — frame the tradeoff, don't pick. Pairing
deferrals consolidated into §6; new substrate noted (buf:path,
pmacs.lsp.buffer_language, PMACS_FAKE_LSP_CHANGE_SINK,
TestDaemon::spawn_with_config).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB