Commit Graph

401 Commits

Author SHA1 Message Date
Levi Neuwirth bcec61e020 feat(highlight): grammars for python/go/typescript/javascript/toml/zig
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
2026-07-14 17:29:54 +01:00
Levi Neuwirth 7d1b6d6a33
Merge pull request #117 from levineuwirth/filename-grammars
feat(highlight): filename detection + Dockerfile/Make/CMake grammars
2026-07-14 16:06:48 +00:00
Levi Neuwirth ff2ce0f197 fix(lsp): PR #117 round 1 — CMake config via initializationOptions
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
2026-07-14 17:03:05 +01:00
Levi Neuwirth 7646dda583 feat(highlight): filename detection + Dockerfile/Make/CMake grammars
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
2026-07-14 16:40:50 +01:00
Levi Neuwirth 13dbadd0b6
Merge pull request #116 from levineuwirth/shebang-detection
feat(highlight): shebang-based language detection for extensionless scripts
2026-07-14 15:00:58 +00:00
Levi Neuwirth 7479213c3f fix(highlight): env -S payload is a full arg list, not a bare interpreter
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.
2026-07-14 16:00:47 +01:00
Levi Neuwirth 558d00020f fix(highlight): PR #116 round 2 — pin grammar across switch, attached env -S
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
2026-07-14 15:20:22 +01:00
Levi Neuwirth f300b77533 fix(highlight): PR #116 round 1 — shebang precedence, pinned grammar, env operands
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
2026-07-14 15:02:59 +01:00
Levi Neuwirth 4a60e4c858 feat(highlight): shebang-based language detection for extensionless scripts
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
2026-07-14 14:33:06 +01:00
Levi Neuwirth 9067eb3d0c
Merge pull request #115 from levineuwirth/shell-grammar
feat(highlight): shell/bash tree-sitter grammar for the shell family
2026-07-14 13:15:05 +00:00
Levi Neuwirth 6fd7db81fe feat(highlight): shell/bash tree-sitter grammar for the shell family
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
2026-07-14 13:50:21 +01:00
Levi Neuwirth 3cbb9dedd0
Merge pull request #114 from levineuwirth/cuda-lsp
feat(lsp): CUDA support — clangd + bundled tree-sitter grammar
2026-07-14 11:04:09 +00:00
Levi Neuwirth ea3641bba2 fix(lsp): PR #114 round 1 — .cuh AST via fallbackFlags, real C/C++ highlights
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
2026-07-14 11:55:58 +01:00
Levi Neuwirth 017f10e6e6 docs: sync handoff — compile-mode #113 merged, Arc 5 stage 1 landed
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
2026-07-14 11:11:10 +01:00
Levi Neuwirth 98323df140
Merge pull request #113 from levineuwirth/compile-mode
feat(compile): compile-mode, unified next-error, grep-mode upgrade, shell-command (Arc 5 stage 1)
2026-07-14 10:09:07 +00:00
Levi Neuwirth 11075914f3 feat(lsp): CUDA support — clangd + bundled tree-sitter grammar
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
2026-07-14 10:58:36 +01:00
Levi Neuwirth 7562d83198 fix(compile): make overlay teardown atomic 2026-07-14 10:57:14 +01:00
Levi Neuwirth b6e44f21d6 fix(compile): PR #113 round 7 — validated overlay attachment, registry-only dispose
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
2026-07-14 10:48:13 +01:00
Levi Neuwirth fe04aa481b fix(compile): PR #113 round 6 — idempotent split-complete attachment, no-op edit guard, handle disposal
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
2026-07-14 10:17:56 +01:00
Levi Neuwirth a49adc2589 fix(compile): PR #113 round 5 — buffer-level span translation, fragment preservation, tracked line start
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
2026-07-13 22:07:01 +01:00
Levi Neuwirth 6793edcfc7 fix(compile): PR #113 round 4 — column-counted CR rewrites, alt-screen style resync
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
2026-07-13 17:55:08 +01:00
Levi Neuwirth b5bbce899a test(compile): portable r3f3 fixture — macOS has no /bin/true
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
2026-07-13 17:31:12 +01:00
Levi Neuwirth b76c46603a fix(compile): PR #113 round 3 — UTF-8-safe renderer, observable parser reset, raw spec reads
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
2026-07-13 17:17:02 +01:00
Levi Neuwirth 6902959b09 test(compile): readiness-gate the survivor fixtures — macOS CI race
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
2026-07-13 16:46:45 +01:00
Levi Neuwirth 50abf91c15 test(compile): portable process probes — macOS CI
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
2026-07-13 16:35:32 +01:00
Levi Neuwirth 37fac4324a fix(compile): PR #113 round 2 — rule snapshots, finite indexes, shell isolation, parser reset
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
2026-07-13 16:30:41 +01:00
Levi Neuwirth d67d30bb64 fix(compile): PR #113 round 1 — coordinates, recovery, rules, types, EOF
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
2026-07-13 16:03:09 +01:00
Levi Neuwirth a2b12dc9d6 docs+test: gate fixes and handoff snapshot (compile-mode in flight)
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
2026-07-13 15:23:20 +01:00
Levi Neuwirth 53854ed803 test(compile): acceptance suites — framing items 1-33 dispatch-driven, 35 two-replica
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
2026-07-13 15:11:43 +01:00
Levi Neuwirth e20d5eaaad feat(compile): compile-mode, shell-command, and the grep-mode upgrade
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
2026-07-13 14:45:51 +01:00
Levi Neuwirth a7d5a6fedf feat(process): group lifecycle + null stdin; buf:revision(); jump_back hook parity
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
2026-07-13 14:36:20 +01:00
Levi Neuwirth 9a9774fc39 docs: compile-mode framing (Arc 5 stage 1)
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
2026-07-13 14:05:05 +01:00
Levi Neuwirth 0efb5cdd90
Merge pull request #112 from levineuwirth/bite-script
dev: scripts/bite — machine-checkable bite-verification
2026-07-12 16:14:52 +00:00
Levi Neuwirth 3284b89a5b dev: scripts/bite — machine-checkable bite-verification
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
2026-07-12 17:09:41 +01:00
Levi Neuwirth f0a05c53af
Merge pull request #111 from levineuwirth/editops
feat(edit): editing-conveniences pack (editops)
2026-07-12 15:52:56 +00:00
Levi Neuwirth 87e88da024 fix(edit): PR #111 round 1 — scalar-valid UTF-8, per-word capitalize, trim error reporting
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
2026-07-12 16:34:18 +01:00
Levi Neuwirth f0a07f41c5 Merge remote-tracking branch 'githubsucks/main' into editops
# Conflicts:
#	docs/agent-handoff.md
2026-07-12 16:33:51 +01:00
Levi Neuwirth 5a49d0de5d docs: sync handoff — auto-pairing #110 merged, Arc 2 complete
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
2026-07-12 16:28:50 +01:00
Levi Neuwirth 4174f3e8c2
Merge pull request #110 from levineuwirth/auto-pair
feat(edit): auto-pairing (Arc 2)
2026-07-12 15:27:25 +00:00
Levi Neuwirth 3085195794 test(edit): PR #110 round 3 — pin raw-byte predicate posture and top-level sets guard
Coverage pins only, no code changes.

Finding 1 (low): the predicate's raw-byte posture is now pinned from
the buffer side — `(` typed immediately before a lone 0xFF inserts no
closer. Verified non-vacuous: reverting char_at to nil-on-malformed
makes the test fail (nil reads as end-of-buffer and pairs before the
junk).

Finding 2 (low): the top-level container guard is pinned alongside
the per-entry cases — `pmacs.pair.sets = 42` pairs nothing and leaves
*errors* clean.

Framing synced to revision 6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
2026-07-12 16:20:01 +01:00
Levi Neuwirth ceaeb81386 fix(edit): PR #110 round 2 — UTF-8 well-formedness, source-buffer relevance, non-table sets
Finding 1 (medium): pair entries validate full UTF-8 well-formedness
(Unicode Table 3-7), not just lead-byte length — continuation-byte
shape on every trailing byte, overlong encodings (C0/C1, E0 80-9F,
F0 80-8F), UTF-16 surrogates (ED A0-BF), and beyond-U+10FFFF (F5+,
F4 90+) all disqualify, so "(\xC2x" can no longer inject invalid
bytes as a closer. char_at shares the validator and returns the raw
byte for malformed buffer content: the predicate treats junk as
word-like (no pairing before it), never as EOL. Bite:
malformed_utf8_pair_entries_are_rejected (four ill-formed shapes).

Finding 2 (low): relevance and reporting resolve against the SOURCE
buffer the record names, not whatever buffer a context-switching
command left active. New pmacs.lsp.buffer_language(buf) is the
parameterized primitive (active_buffer_language delegates), backed by
a new buf:path() query on buffer handles. Bites: rust→python `'` now
stays silent; python→rust `'` now reports "source context changed".

Finding 3 (low): non-table set containers degrade
language→default→empty instead of throwing from the after-edit
callback on every keystroke. Bites: a string default pairs nothing
with a clean *errors* buffer; a junk language entry falls back to the
default set.

Framing synced to revision 5 (Q#AP2 well-formedness + container
degradation + source-buffer resolution, Q#AP3 predicate junk-byte
posture, acceptance list).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
2026-07-12 15:54:16 +01:00
Levi Neuwirth f7b5005e3d docs: handoff snapshot (editops lane)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vF4gQVozBWi38y1SJiGfQ
2026-07-12 15:37:59 +01:00
Levi Neuwirth 781cd95fe2 feat(edit): editing-conveniences pack (editops)
builtin/runtime/editops.lua: goto-line (M-g g / M-g M-g), case ops
(M-u/M-l/M-c), transpose chars/words (C-t/M-t), zap-to-char (M-z) +
zap-up-to-char, line move/duplicate/join (M-up/M-down/M-^), region
sort/reverse/dedupe, delete-trailing-whitespace + opt-in
trim_on_save. All edits ride the Q#EC2 guarded single-replace
discipline (snapshot, exact effective-triple check, context guard,
right-gravity transformed-cursor repair, unconditional selection
clear); word/case ops are explicit-byte-range ASCII (locale-proof);
transpose-words matches the empirical Emacs 30.2 boundary table.

killring.lua: zap commands join KILL_CHAIN; new exports kill_range
(validated, chain-aware, typed failure returns), break_chain([fid]),
and the Q#EC6 pending-prompt marker (arm/commit; arm-time
abandoned-marker break; kill_push force-fresh on an uncommitted
marker; detach cleanup) closing the silent-session-replacement hole.
Zap guards its origin frontend and re-verifies this_command at
accept time; commit_kill_prompt() reports armament so a consumed
marker fails closed.

editor.rs: editops.lua loader entry before saveplace.lua (the Q#EC9
before-save registration-order contract).

tests/editops_acceptance.rs: 68 dispatch-driven cases — RET/C-g
completed minibuffer sessions, the boundary-state pin, origin-guard
and silent-replacement matrices, the nine-position transpose table,
intercept discipline (reject/transform/context-switch/zero-length
anchor), trim sweep semantics, and trim-on-save veto interactions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vF4gQVozBWi38y1SJiGfQ
2026-07-12 15:33:17 +01:00
Levi Neuwirth b0bbc86792 fix(edit): PR #110 round 1 — revision postcondition, relevance gate, strict pair parsing
Finding 1 (medium): the typed-edit record now pins the edited
buffer's revision after the completing edit; typed_edit_finish
re-reads it at dispatch end and drops the record if the command
edited again — a redefined buffer.self-insert that replaces the typed
char (cursor unmoved) no longer leaves a stale-but-clean record, so
`(`-then-replace-with-`[` yields `[`, not `[)`. Bite:
post_insert_mutation_by_the_command_kills_the_record.

Finding 2 (medium): pair-set relevance is established before the
clean/context gates, so a transformed or relocated character outside
the active set stays silent instead of drawing an auto-pair report.
Bite: transformed_non_pair_char_stays_silent.

Finding 3 (medium): split_pair parses EXACTLY two codepoints and
rejects trailing bytes — a "()x" (or "«»x") entry is skipped
entirely, never honored as `(` → `)x`; valid multibyte pairs ("«»")
pair and skip at byte-correct cursors. Bites:
malformed_pair_entries_are_skipped_not_partially_honored,
multibyte_pair_entries_pair_and_skip.

Finding 4 (low): the record-capture seam is gated behind the opt-in
pmacs.pair._capture_records test facility, off by default — no
consumed record is retained in production, restoring the Q#AP9
ephemerality the seam had defeated. Seam-reading tests opt in;
record_capture_is_off_by_default pins the default.

Finding 5 (low): the equal-revision source-context-switch twin is
covered — the fan-out is skipped by the active-buffer revision
compare, pairing fails closed silently, and no report is possible;
the framing scopes the context-change report as best-effort until the
buffer-aware edit epoch lands.

Framing synced to revision 4 (Q#AP2 entry rule, Q#AP3 relevance-first
+ best-effort report scope, Q#AP9 revision postcondition + capture
facility + the dispatch-path intercept borrow note, acceptance list).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
2026-07-12 15:17:33 +01:00
Levi Neuwirth 9879b4637f docs: editing-conveniences framing (Lua parallel lane)
Framing for the editops pack: goto-line, case ops, transpose,
zap-to-char (kill-chain member with origin guard + pending-prompt
marker), line move/duplicate/join, region sort/reverse/dedupe, and
delete-trailing-whitespace with opt-in trim-on-save. Five review
revisions recorded in the doc; approved to branch at revision 4,
revision 5 adopts the reviewer's optional commit_kill_prompt
hardening.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vF4gQVozBWi38y1SJiGfQ
2026-07-12 15:15:57 +01:00
Levi Neuwirth 223e26420b feat(edit): auto-pairing (Arc 2)
Typing an opener inserts the closer with the cursor between; typing a
closer over its twin steps over it. Q#AP1: the nine built-in pair
chars leave both optimistic classifiers (shared charset in
pmacs-protocol) and round-trip through dispatch, so the opener and the
hook's closer are adjacent daemon-peer undo units, dispatch CUA
type-over applies, and skip never paints a transient duplicate.

Q#AP9: exact one-shot typed-edit provenance. EditorCore's
apply_active_edit now returns the effective Edit; the dispatch
fallback arms a per-frontend record (codepoint + requested vs
effective ranges + post-cursor + clean verdict) that insert primitives
complete and the daemon's optimistic CRDT arm builds directly. The
record is takeable exactly once via pmacs.editor.take_typed_edit()
during the one after-edit fan-out, then cleared — paste, programmatic
edits, manual hook runs, nested re-runs, rejected edits, and stale
this_command all observe nil, and transformed / relocated /
context-switched source self-inserts fail closed with a status.

pair.lua (loaded BEFORE lsp.lua — ordering contract in editor.rs):
per-language pmacs.pair.sets with a conservative default (no ' or `),
EOL/whitespace/closer insertion predicate, reactive skip-over-close,
rejected/transformed intercept outcomes with context-guarded
translate-and-clamp cursor repair.

Acceptance: 32 dispatch-driven cases (predicate, skip, per-language
sets, non-typed provenance incl. production-shaped paste, type-over,
undo/redo grain, intercept outcomes on both the source and reaction
edits, context-switch probe, record lifecycle, frontend isolation) +
first-didChange ordering against the fake LSP's sighelp mode via a
new PMACS_FAKE_LSP_CHANGE_SINK replay file. Six two-replica CRDT
cases pin dispatch-route convergence with cursor-between, undo/redo
walking the pair on both replicas, both mixed-history undo models as
named substrate limits, and the optimistic custom-char route
(closer-broadcast-before-opener convergence, degraded cross-peer
undo). TestDaemon gains spawn_with_config for init.lua-extended pair
sets.

Framing: docs/auto-pairing-framing.md (revision 3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
2026-07-11 17:11:56 +01:00
Levi Neuwirth ac8c46f2d3 docs: auto-pairing framing (Arc 2) + handoff snapshot
Auto-pairing framing at revision 3 (two pre-branch review rounds):
pair chars leave both optimistic classifiers and round-trip through
dispatch (adjacent daemon-peer undo units, real type-over, no
transient skip paint); reaction hook pair.lua loads before lsp.lua
so the synchronous sighelp didChange flush ships the closer; exact
one-shot typed-edit provenance (pmacs.editor.take_typed_edit) gates
pairing fail-closed against transformed, relocated, or
context-switched source self-inserts and non-typed origins.

Handoff: main @ 7e127ab (auto-indent #109 merged), auto-pairing in
flight on this branch, Arc 2 deferrals consolidated into §6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
2026-07-11 16:11:51 +01:00
Levi Neuwirth 7e127ab836
Merge pull request #109 from levineuwirth/auto-indent
feat(edit): auto-indent on newline (Arc 2)
2026-07-10 20:26:04 +00:00
Levi Neuwirth c008c7d1b2 docs: sync auto-indent review metadata 2026-07-10 15:57:38 -04:00
Levi Neuwirth 180343e6e3 fix(edit): PR #109 round 1 — shared search invalidation, daemon anchor clear, bounded indent scan
Finding 1: the empty-anchor optimistic residual was never GPU-only
(the TUI mirror tracks no selection state; its gate checks cursor
freshness/EOL only). The fix moves daemon-side: handle_remote_crdt_op
clears a selection whose anchor equals the pre-edit cursor (= empty)
before applying the source cursor update; nonempty selections stand.
Covers both frontends. The TUI gate's missing type-over check
(nonempty selection at EOL) is a named deferral.

Finding 2: Q#AI8 invalidation is one helper
(search_invalidate_for_edit) invoked from all four edit paths --
apply_active_edit, notify_buffer_edit, and now undo/redo, which
received precise Edits but invalidated nothing. rebuild_views_for is
named as a lower-frequency bypass (deferral).

Finding 3: acceptance matrix trued up -- added active-search
fail-closed + retype recovery, delete translation on both paths,
undo/redo staleness + origin tests; modal contexts narrowed to what
this suite pins (query-replace/menu/completion ride their own
suites).

Finding 4: indent extraction is a forward-chunked scan stopping at
the first non-whitespace byte -- Enter at the end of a giant
minified line no longer materializes the line. Functional pin at
64 KiB.

Both medium fixes are bite-verified (tests fail with the fix
disabled).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ATiKMwJ4864d82D39EvsU6
2026-07-10 15:46:36 -04:00