Commit Graph

658 Commits

Author SHA1 Message Date
Levi Neuwirth b8a296f0a3 fix(persistence): only recover/discard may release unclaimed crash data
Addresses the PR #100 review round 2. Q#AS12's ownership rule guarded the
sweep but not the RELEASE paths, so three doors were still open.

The rule is now total: exactly two things may release an unclaimed
recovery file --- recover-file (which adopts it) and discard-recovery
(explicit user intent). Not a sweep, not a save, not a kill.

- HIGH: buffer.after-save called _discard_buffer unconditionally, which
  removed the live buffer's current-path key without checking ownership.
  Repro: session 1 autosaves and crashes; session 2 opens the file, does
  not recover, then saves --- the crash artifact was deleted. Same door
  was open on kill. discard_buffer now removes ONLY keys this session
  owns. The unclaimed copy survives (reported Stale, so never
  auto-offered, but still recoverable/discardable). The on-disk file holds
  the new work; the crash copy holds work never written anywhere, so
  deleting it was the same data loss by a different door.

- MEDIUM/LOW: _adopt only recorded the path in `owned`, not an
  association with the buffer. A removal callback fires after the buffer
  has left the registry, so discard_buffer had no path to read and no
  `written` entry to fall back on --- recover-then-kill leaked the copy
  and it was offered again. adopt now takes the BUFFER and records a
  `written` entry at the revision whose contents the file holds. That is
  correct twice over: the skip cache declines to rewrite an identical
  copy, and a kill can find and retire it.

- LOW: _discard(path) removed the file and unowned the hash but left
  matching `written` entries, so a still-dirty buffer hit the unchanged
  (path_hash, revision) fast path and went unprotected until its next
  edit. discard_path now clears those entries; the next sweep re-protects
  immediately.

Tests (autosave_acceptance now 24):
saving_without_recovering_preserves_unclaimed_crash_data,
killing_without_recovering_preserves_unclaimed_crash_data,
recover_then_kill_retires_the_adopted_recovery,
discard_recovery_lets_the_next_sweep_reprotect_immediately.

Gates: fmt + workspace clippy clean; lib 1499; crdt 1670; autosave 24;
desktop 11; persistence 5; GPU 58; git diff --check clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 11:08:33 -04:00
Levi Neuwirth 1205329c34 fix(persistence): never clobber unclaimed crash data; buffer-keyed cleanup
Addresses the PR #100 review.

- HIGH data loss: sweep could overwrite an existing crash recovery before
  the user ran recover-file. Reopen a file after a crash, edit it, and the
  next autosave wrote the current buffer over the recovery key --- losing
  exactly what autosave exists to protect. New ownership rule (Q#AS12): a
  per-session `owned` set records which path hashes THIS session wrote or
  adopted. A recovery file at a key we do not own is unclaimed crash data;
  the sweep refuses to write that buffer, counts it `blocked`, and says so
  ("autosave paused for N file(s) with unclaimed recovery"). recover-file
  ADOPTS the copy once its contents are in the buffer; discard-recovery
  removes it. Either resumes normal autosave. sweep() now returns
  (written, blocked).

- MEDIUM cleanup missed paths autosave can write. Kill/save cleanup now
  goes through `discard_buffer(BufferId)`, which removes BOTH the buffer's
  current-path key and the key its last sweep actually wrote (they differ
  after a rename --- an LSP WorkspaceEdit changes the path while the
  BufferId stays; a path-captured callback deleted the wrong key). And a
  sweep-time GC deletes the recovery of any buffer that left the registry,
  which is the backstop for argv `[new file]` buffers: they fire no
  after-load, so no removal callback is ever registered for them.

- LOW/MEDIUM recover-file pinned only on the active path. Two buffers can
  visit one path (pmacs.buffer.from_file does not dedup), so focus drift
  could recover into the wrong buffer. It now captures and compares the
  origin buffer handle as well as the path.

- LOW write_private left a pre-existing lax autosave/ directory alone. The
  birth-mode only applies to dirs that call creates, so a 0755 autosave/
  from an older run still leaked recovery-file names, sizes, and mtimes
  despite 0600 contents. It is now tightened to 0700 --- but never `base`
  itself, which is shared with history/recentf/desktop and may predate us.
  New `state::exists` (an existence check, no read) backs the ownership
  gate.

Tests (autosave_acceptance now 20): sweep_never_overwrites_unclaimed_
crash_recovery (blocked, crash copy byte-identical, adopt resumes),
discarding_an_unclaimed_recovery_unblocks_the_sweep,
killing_a_new_file_buffer_gcs_its_recovery,
saving_after_a_rename_removes_the_recovery_written_under_the_old_path,
a_pre_existing_lax_autosave_dir_is_tightened.

Gates: fmt + workspace clippy clean; lib 1499; crdt 1670; autosave 20;
desktop 11; persistence 5; m4 90; m7_8 5; GPU 58; git diff --check clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 10:52:10 -04:00
Levi Neuwirth a6d8c60faf docs: autosave + crash-recovery framing (Arc 3 phase 3)
Q#AS1-11: hybrid (Rust sweep+guard, Lua cadence/config/UX);
process.after-tick + monotonic_ms cadence (no parked worker thread,
live-reconfigurable interval); validated-setter config
(pmacs.autosave.interval_ms, default 30s, 1s floor); one atomic
header-line+bytes envelope with NULLABLE origin meta (new files);
Fresh/Stale/Corrupt/None status table; pull-based aggregated notify on
the tick (covers argv [new file] buffers that fire no hook); explicit
hook.run(buffer.after-edit) after recover-file's replace; path-aware
(path_hash, revision) skip cache; shared sha256_hex extraction;
private 0700/0600 storage as a precondition for default-on.
Incorporates the 2026-07-09 review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 10:10:06 -04:00
Levi Neuwirth e56c3055f2 fix(persistence): reliable daemon gate, unarm, per-pane after-load
Addresses the PR #99 review:

- HIGH daemon local-only was not reliable: run_daemon sets DaemonMode
  only after EditorState::new() has run init.lua, so desktop_mode(true)
  in init saw is_daemon()==false and the raw bindings were ungated. Now
  save_session/restore_session early-return in Rust when the DaemonMode
  marker is present — set right after the daemon's new(), so it holds for
  every save/restore that can run after startup (before-quit hook, manual
  commands, direct binding calls).

- MEDIUM desktop_mode(false) could not unarm startup restore: arm_restore
  is now a boolean (arm_restore(on)) that sets/removes the marker, and
  desktop_mode(on) calls arm_restore(on). enable-then-disable no longer
  restores.

- MEDIUM/LOW same-file multi-pane missed per-window overlays: restore now
  fires buffer.after-load once PER LEAF (per window), not once per buffer.
  Syntax attaches its overlay to the active window, so each pane gets its
  own; LSP attach_buffer is idempotent, so the same file in two panes
  attaches LSP once but syntax to both.

- MEDIUM hidden restored buffers: documented as registry-only in v1 (they
  are live/openable/in recentf, but do not fire after-load, so they
  attach syntax on first visit via after-switch and LSP when next shown).
  Full initial attach for hidden buffers is deferred. Noted in the
  framing + a code comment.

- LOW trailing whitespace in docs/desktop-save-framing.md.

Tests (desktop_acceptance now 11): same_file_..._fires_per_pane asserts
after-load fires twice for two panes of one file; daemon_mode_disables_
save_and_restore; disabling_desktop_mode_unarms_restore.

Gates: fmt + workspace clippy clean; lib 1487; crdt 1658; desktop 11;
persistence 5; m4 90; GPU 58; git diff --check clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 22:27:24 -04:00
Levi Neuwirth 72563e93cf docs: desktop-save framing (Arc 3 phase 2)
Q#DS1-10: all-Rust pmacs.session.* + thin desktop.lua (opt-in). Serde
mirror (SavedDesktop with all-buffers list + layout tree + active_leaf
preorder index); SHA-256 session key; get_or_load_buffer helper;
activate-leaf-then-fire-after-load restore ordering; prune all old
LOCAL windows; RunLocal-arm startup trigger; local-only save+restore
(daemon deferred); active-focus fallback. Incorporates the 2026-07-08
review (both rounds).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 18:50:57 -04:00
Levi Neuwirth 349f13f674 docs: persistence framing (Arc 3)
Q#PS1-9: hybrid (4 thin Rust primitives + Lua policy). state_dir() +
path-confined pmacs.state.{read,write,remove,path} (empty-XDG fix, not
a pure refactor); per-buffer file_path; goto_byte/set_view_top;
pmacs.session.save_desktop/restore_desktop (Rust serde — layout mirror
+ active-leaf preorder index). Line-based state (no Lua JSON codec).
Deferred restore (armed by desktop_mode, triggered post-file-routing
only when no file arg). Encoded session key (name:<enc>/cwd:<hash>).
recentf MRU on after-load + after-switch. Default-on saveplace/recentf
with enable-knobs + cfg(test) write-inertness. Incorporates the
2026-07-08 review findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 17:33:25 -04:00
Levi Neuwirth a118f0a3a2 fix(query-replace): pin the session to its origin buffer (wrong-buffer guard)
Review High (merge blocker): query-replace searched the origin buffer's
bytes but applied edits and moved the cursor through apply_active_edit /
search_place_cursor, which target whatever is ACTIVE. Focus can drift
mid-session — a click into another split, a key from another frontend,
both changing the active buffer outside the shadow — so a match found in
the origin buffer could be applied to an unrelated one. Buffer
corruption.

Fix: query_replace_on_origin() verifies the active buffer still equals
the session's origin buffer before every edit; on mismatch it ABORTS
without editing (clears the highlight, drops the session, status
'query-replace aborted: active buffer changed'), so an origin match can
never land in a foreign buffer. Guards replace/skip/all/replace-and-quit.
The dispatcher's after-edit revision compare now targets the ORIGIN
buffer (query_replace_origin_buffer + buffer_revision) not the active
one, so a drift-abort — which edits nothing — never spuriously fires
the hook. The forward-search clamp uses the origin bytes' length, not
active_buffer_len.

Also (review Low/med): query_replace_active() added to the
completion-popup modal-close guard, so a popup opened via the direct
Lua start (ed.query_replace_start) can't linger rendered-but-unreachable
while QR swallows keys.

Tests: core drift-abort (both buffers untouched) + end-to-end
focus-drift regression; ! fires after-edit exactly once for the batch;
RET/Esc/C-g quit paths (keep replacements); DEL skips. Acceptance
header corrected to match actual coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 17:07:08 -04:00
Levi Neuwirth 27aaf0ef30 docs: query-replace framing (Arc 2 interleave)
Q#QR1-10: zero-protocol-change (reuses v15 StatusFacts.message band,
SearchMatchActive decorations, dispatch_idle search gate); distinct
QueryReplaceSession + 5th dispatcher shadow with the after-edit hook
precedent; Emacs search-forward-after-each-replace (cached regex,
zero-width filter, invalid-at-start refusal); chained minibuffer.read
with separate from/to history buckets + empty-to=deletion; per-match
core.status prompt; SearchMatchActive highlight-and-reveal; quit keeps
replacements (only nothing-matched restores origin). Incorporates the
2026-07-08 review findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 15:51:33 -04:00
Levi Neuwirth c49daa0bab docs(panels): correct as-built accuracy — position encoding landed, refresh drifted refs
Three PR #96 review findings (documentation accuracy):

- Q#P7 coordinates section claimed panels inherit a byte==UTF-16 wire
  assumption with position-encoding hardening deferred. False as-built:
  the transport layer negotiates general.positionEncoding and converts
  every Position at the request/response boundary (PositionEncoding +
  char_to_byte/byte_to_char, src/lsp.rs), so location rows reach Lua as
  byte offsets. Reworded to record what landed; the true residual is
  the codepoint-vs-byte cursor walk in move_active_cursor_to (shared
  with go_to_definition, not introduced by panels).
- Intro described pre-arc behavior in present tense (references throw
  rows away, code actions apply acts[1] blind, ...). Marked as the
  pre-arc baseline with a status banner + inline as-built pointers.
- Drifted hard-coded line refs (editor_core.rs:2052-2071,
  lsp.lua:658-662, lsp.lua:1187-1213) replaced with symbol names.

Also fixed the move_active_cursor_to comment in lsp.lua itself — it
was the same 'v0.2 hardening' false trail the doc's stale ref pointed
at, now naming the real residual (codepoint-walk, not wire encoding).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 14:29:23 -04:00
Levi Neuwirth c9e56a7509 docs(panels): score Arc 1b bets + as-built (bet #3 false, findings, process note)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 14:10:40 -04:00
Levi Neuwirth d1a7683c2b docs: LSP panels framing (Arc 1b)
Q#P1-P7: panels are buffers (a shared listview runtime module
generalizing the *buffer-list* idiom — zero protocol change, both
frontends render them for free); switch-in-place presentation with q
restore (the GPU cannot show splits); read-only via intercept with its
limits recorded; the Q#P6 round-trip buffer seam so semantic frontends
never optimistic-apply into panels (RET visits instead of inserting a
newline); references list, outline, minibuffer code-action picker,
hover-doc panel; byte==UTF-16 caveat inherited, not multiplied.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:08:43 -04:00
Levi Neuwirth 6ac677ea24 docs(completion): phase-2 as-built notes (Q#C6 narrowing, window routing, v15 riders)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 19:57:10 -04:00
Levi Neuwirth 61a31b3ad4 fix(completion): address TUI-validation findings (LSP query gaps, scoping, prefix keys, window scope)
Five findings from the manual validation pass, all in-branch:

1. LSP-only words never queried the server: the auto-open path fired
   request_completion only when the sync providers already produced
   rows. An empty sweep now leaves a pending session and the request
   always fires; isIncomplete responses re-request on further typing.
   Corollary: attachment_for_request now flushes-if-attached but NEVER
   attaches -- the first cut wrapped attached_for_active, which spawns
   servers on demand, i.e. per-keystroke spawn attempts in unattached
   buffers (wedged the parallel m4 suite; serial ran 3x slower).
   Attachment stays buffer-open policy.

2. Cross-buffer LSP leak: the built-in provider's no-uri fallback was
   the legacy global store drain, so scratch/unattached buffers could
   show another file's cached completions. Strict now: no uri, no rows.

3. Pending prefixes own the keyboard: Action::Pending (C-x ...)
   dismisses the popup and the popup shadow is guarded on an empty
   dispatcher prefix, so the sequence's continuation and its C-g abort
   reach the dispatcher instead of the popup.

4. Window-scoped sessions: CompletionPopupState.window_id (stamped by
   completion_popup_open; Lua never sees it). Only the owning window's
   overlay paints -- same-buffer splits each carry a persistent
   overlay -- and a focus change invalidates the session.

5. Flaky worker test: the /proc thread-count probe and the idempotence
   check both build EditorStates and could run concurrently, polluting
   the baseline; merged into one test (non-Linux keeps a portable
   idempotence variant).

Regression tests for 1-4; framing doc gains the as-built notes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 16:54:14 -04:00
Levi Neuwirth 21d12b486b docs: in-buffer completion popup framing (Arc 1a)
Framing for wiring the dormant M4.7/M4.11 completion system to real UI
in both frontends. Q#C1-C9 decisions: Lua driver, core-owned session
(search-store pattern), partial dispatcher shadow with post-dispatch
validation, MenuView-style CompletionView rework, protocol v15
CompletionPopup message (phase 2), explicit GPU control-key predicate,
validated accept, LSP scoping + flush-before-request, and the
single-char trigger heuristic. Incorporates the 2026-07-07 review
findings (LSP store scoping, score filtering, didChange flush, motion
closure, GPU ESC routing, hook payload policy, context_for nit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:24:46 -04:00
Levi Neuwirth 642505099a docs: July 2026 roadmap — ranked arcs; Arc 1 (LSP utility surface) active
State assessment from a five-way sweep (core editing/persistence, LSP,
GPU parity, extensibility/terminal, deferred-work inventory). Records
the decision to push Arc 1 (completion popup, LSP panels, semantic-token
auto-pull, signature trigger) with Arc 2 editing table-stakes
interleaved, plus the ranked remaining arcs and housekeeping list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:00:09 -04:00
Levi Neuwirth 076bdfc5f4 docs(ux): as-built for sub-arc 3 + arc close (relative/hybrid, protocol v14)
Record sub-arc 3: the shared LineNumberMode enum + number_for in
pmacs-protocol, the v14 mode bump, the cursor-line repaint-on-move
dependency, stable gutter width, and the binary-toggle + completion-picker
selection UX. Plus the arc-close note: Q#UX1 scored false (two protocol
bumps), and the deferred gutter-riding features.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-07 10:03:32 -04:00
Levi Neuwirth 0e36f13f11 docs(ux): as-built for sub-arc 2 (diagnostic gutter signs)
Record the sub-arc 2 as-built: signs-ride-the-gutter coupling, the
Viewport.gutter_w + paint-reorder mechanism (TUI), the layout_runs bar
mechanism (GPU), the TUI-glyph/GPU-bar rendering difference, the
en-route multi-frontend window.close crash fix (PR #87), and the known
completion-navigation follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-06 19:25:00 -04:00
Levi Neuwirth 1ea2d5f17f feat(gutter): daemon-owned line-number mode over protocol v13 (unified toggle)
Fix the control plane for the line-number gutter: M-x
window.toggle-line-numbers now works from EITHER frontend, each affecting
its own window.

Root cause (scores framing bet Q#UX1 false): rendering a gutter is
frontend-local, but the TOGGLE is a daemon command, so the mode has to
reach the GUI over the wire. My earlier GPU control (a --line-numbers flag)
left M-x-in-the-GUI a no-op and the two frontends' settings disconnected.

- Protocol: new additive `InstanceMessage::LineNumbers { buffer_id,
  enabled }`; PROTOCOL_VERSION 12 → 13, SUPPORTED grows to [6..13].
  Daemon-gated < 13 (a v12 peer keeps its gutter off), like every prior
  additive bump — no encoding break.
- Producer: SemanticRenderState::line_numbers_msg reads the frontend's
  active window mode (via active_window_for(frontend_id)) and emits on
  change; cached-compare suppression seeded to the frontend's `off`
  default, so a plain window adds zero traffic and existing frames are
  unchanged.
- Daemon: gate LineNumbers >= 13 in the write loop.
- TUI: drops LineNumbers silently (reads its window directly).
- GPU: consumes LineNumbers → drives local `line_numbers`; the
  --line-numbers flag retired.

Now the daemon Window.line_numbers is the single source of truth; both
frontends render locally from it.

Tests: line_numbers_msg emit-on-toggle/suppress-when-unchanged; protocol
version pins updated to 13. Validated: fmt + clippy --all-targets clean
both flavors; 1440 lib + 12 protocol + 53 pmacs-gpu tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-06 14:27:44 -04:00
Levi Neuwirth 296cf34ae9 refactor(lua): re-export install_* wiring fns to preserve the public API
Review follow-up on the F-016 split. install_diag / install_project_index
/ install_mcp were `pub fn` reachable at crate::lua_bindings::install_* be-
fore the split, but moving them into private child modules dropped those
paths without a re-export — shrinking the public API, which the split is
supposed to preserve. (They take crate-internal handle types so no external
caller can invoke them, and none does, so nothing actually broke — but the
paths should still resolve.)

Re-export all three alongside the factories/handles already re-exported,
restoring the paths for the two already-merged tranches (diag, index) too.
Deliberately narrowing these to pub(crate) is left as a separate change.

Validated: fmt clean; clippy --lib clean under both Lua flavors; full lib
suite 1437 passed / 0 failed under luajit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-06 11:57:56 -04:00
Levi Neuwirth e8e56bdbf1 refactor(lua): extract pmacs.mcp into its own module (F-016, tranche 2)
Third tranche of the F-016 split. Extract the pmacs.mcp surface (MCP client
bindings) from src/lua_bindings/mod.rs into src/lua_bindings/mcp.rs,
verbatim.

Corrected model (see framing): a helper-hoist is NOT a prerequisite for
most domains. The contamination that stopped parse/theme bites only when a
shared helper is *defined inside* the range being extracted. A domain that
merely *uses* a cross-section helper reaches it via `super::`
(parent-private access). So mcp extracts cleanly: all its items are
self-contained, and it reaches the JSON converters (still in the lsp
section) via super::json_to_lua / lua_to_json, and SharedProcessSupervisor
via super::. The JSON-helper hoist is deferred to the tranche that
extracts lsp itself (where they're defined).

mod.rs declares `mod mcp;` and re-exports make_mcp_manager (external caller
editor.rs) and McpServerIdLua — the latter to preserve its public-API path
crate::lua_bindings::McpServerIdLua (moving it into a private module had
dropped it from the crate surface; the split must not shrink the public
API).

Pure code motion, no behavior change. mod.rs: 14603 → 14020 lines.

Validated: fmt clean; clippy --lib clean under both Lua flavors; full lib
suite 1437 passed / 0 failed under both luajit and lua54.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-06 11:49:07 -04:00
Levi Neuwirth df35e03ecf refactor(lua): extract pmacs.index into its own module (F-016, tranche 1)
Second tranche of the F-016 split. Extract the pmacs.index surface (the
project symbol-index bindings) from src/lua_bindings/mod.rs into
src/lua_bindings/index.rs, moved verbatim.

index is the one genuinely clean remaining leaf: its private helpers
(symbol_kind_from_lua, lua_symbol_from_table, search_hit_to_lua) are used
only within its own range, and it has zero shared-core coupling — it
depends only on crate::project_index, mlua, and std, reaching one stranded
helper (lua_to_json, still in the lsp section) via `super::`.

mod.rs declares `mod index;` and re-exports `SharedProjectIndexer` +
`make_project_indexer` via `pub use`, so the crate::lua_bindings::… paths
in editor.rs and completion_framework.rs (and an in-file completion-
framework use) stay valid — no external file changes.

Pure code motion, no behavior change. mod.rs: 14986 → 14603 lines.

While vetting the next leaves I found the recon under-counted the
misplaced shared helpers: parse/theme, window, and minibuffer trail off
into shared style/color, caller_source, and command/menu helpers, so a
dedicated helper-hoist tranche must precede them (framing tranche plan
updated). This tranche stops at index rather than force a contaminated
extraction.

Validated: fmt clean; clippy --lib clean under both Lua flavors; full lib
suite 1437 passed / 0 failed under both luajit and lua54.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-06 11:32:55 -04:00
Levi Neuwirth d002b7da71 refactor(lua): split lua_bindings.rs into a module dir; extract pmacs.diag (F-016, tranche 0)
First tranche of the F-016 split of the 15k-line src/lua_bindings.rs.
Deliberately minimal — it validates the mechanics before bulk moves.

- Convert src/lua_bindings.rs → src/lua_bindings/mod.rs (the
  `pub mod lua_bindings;` in lib.rs resolves to mod.rs unchanged).
- Extract the pmacs.diag surface (diagnostic_to_lua + install_diag) into
  src/lua_bindings/diag.rs, moved verbatim. mod.rs declares `mod diag;`
  and its one internal call site is now `diag::install_diag(...)`.

Pure code motion: no logic, signature, or behavior change. diag.rs reaches
shared-core items (BufferIdLua, SharedCore) via `super::` — a child module
can see its ancestors' private items, so no visibility widening was
needed; install_diag's only caller is mod.rs itself, so no re-export
either. The Lua-visible pmacs.diag.* API is byte-for-byte unchanged.

mod.rs: 15202 → 14986 lines. Framing + tranche plan:
docs/lua-bindings-split-framing.md.

Validated: fmt clean; clippy --lib clean under both Lua flavors; full lib
suite 1437 passed / 0 failed under luajit AND lua54 (the tests drive
pmacs.diag.* through the Lua VM — same outcomes, code relocated).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-06 10:46:47 -04:00
Levi Neuwirth 3b630bfee4 docs(features): document the Lua feature matrix; drop unreachable compile_error idea (F-002)
`--all-features` can't build pmacs — luajit and lua54 select mlua's
mutually-exclusive Lua backends. Document the model so generic tooling
(CI, cargo hack, distro packaging) doesn't trip over it:

- README §Build: a feature-matrix table (luajit default / lua54 fallback /
  orthogonal crdt), the supported build lines, and an explicit "don't use
  --all-features".
- src/lib.rs crate docs: a "Lua flavor features" section stating the
  exactly-one-flavor rule.
- Cargo.toml [features]: expanded comment on the mutual exclusivity.

CI already iterates the flavors explicitly (never --all-features), so no
CI change was needed.

The audit's suggested crate-local compile_error! for the wrong-flavor case
was investigated and rejected as unreachable: the flavor check lives in
the mlua-sys *build script*, which cargo compiles before the pmacs crate,
so a mis-set flavor (both or neither) fails there first and pmacs's own
compile_error! never evaluates — confirmed empirically for both cases. A
dependent crate can't preempt a dependency's build failure, so the docs
are the honest mitigation and they name mlua-sys as the actual error
surface.

Validated: fmt clean; clippy clean under both Lua flavors; both flavors
build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-03 19:54:35 -04:00
Levi Neuwirth 8472c4d87b fix(packages): F-005 must also guard the frozen/lockfile plan path
Review follow-up on F-005. The basename-collision check only ran in
ResolverState::into_plan, which covers fresh resolves and UpdateOne — but
UpdatePolicy::Frozen returns Lockfile::to_resolve_plan(...) directly,
building a ResolvePlan without the check. A pre-existing or hand-edited
lockfile containing two distinct packages that share an install basename
(e.g. owner/magit and other/magit) would produce one plan and install both
to <root>/<basename>, silently colliding.

Make find_basename_collision (and its message helper) pub(crate) and apply
it in Lockfile::to_resolve_plan too — up front, before any fetch, so a
colliding lockfile fails fast via a new LockfileError::BasenameCollision
(surfaced through the Frozen path as ResolveError::Lockfile). Both
plan-construction sites now reject; to_resolve_plan is pub and has direct
callers, so guarding the method (not just the resolve_with_policy branch)
covers them all.

New unit test builds a two-entry colliding lockfile and asserts
to_resolve_plan rejects it before touching the fetcher.

Validated: fmt clean; clippy --all-targets clean under both Lua flavors;
1437 lib tests pass (incl. the new frozen-path test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-03 19:05:08 -04:00
Levi Neuwirth 1694908e9c fix(packages): basename-collision reject, SHA-256 cache key, timeout thread join, commit→revision, dead-code (F-005/F-009–F-012)
Package-manager hardening sweep from the repo audit — one Medium + four
Lows, all in src/packages/ (F-011 also renames across lua_bindings + tests).

F-005 (Medium) — install dirs are named by package basename and require
routes by basename, so two distinct packages `owner/magit` and
`other/magit` collapse to one dir with most-recent-install silently
winning. Reject a resolve plan that contains distinct names sharing a
basename: new ResolveError::BasenameCollision + find_basename_collision()
in into_plan (the one place holding every name at once). The loader's
*intended* cross-scope override (project- vs user-scope, most-recent-first)
is untouched — its test still passes. Namespace-preserving layout and
cross-resolve install-time detection are named-deferred.

F-009 (Low) — the fetch bare-mirror cache dir was keyed by 64-bit FNV-1a
of the (attacker-adjacent) repo URL — trivially collidable. Swap to
SHA-256 (sha2, already a dep for lockfile hashing). normalize_url still
folds equivalent URLs to one entry; only the digest changes (re-clones
once, it's a cache).

F-010 (Low) — on a git subprocess timeout, run_with_timeout returned
before joining the stdout/stderr drain threads (joined only on the normal
path), leaving detached readers. Restructure to break the wait loop with a
Result, reap the child on every path, and join both threads at one point
before propagating.

F-011 (Low) — ResolvedPackage.commit was documented "Full 40-character
commit hash" but commit_for_tag() puts a tag string there (the resolver
works against commit-ishes by design, deferring SHA resolution to the
installer/lockfile). Rename the field to `revision` + honest doc.
Compiler-driven rename hit exactly the ResolvedPackage sites; the
Lua-visible "commit" record key is unchanged.

F-012 (Low) — the topo sort built an indegree map, argued in comments it
was backwards, and rebuilt it. Delete the dead first block + the
meandering narration.

Framing/as-built: docs/package-manager-hardening-framing.md.

Validated: fmt clean; clippy --all-targets clean under both Lua flavors;
1436 lib unit tests pass (incl. new F-005/F-009 tests, the F-010 timeout
test, and the loader override test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-03 18:47:23 -04:00
Levi Neuwirth 45b02d1597 fix(gpu): F-008 fail-fast must actually tear down the session, not just flag it
Review follow-up on the F-008 bounded outbox. Closing the outbox on a
lossless overflow set a `closed` flag but did not disconnect: the reader
stayed blocked on its still-open socket clone, so no `Disconnected` fired,
the daemon was never signaled, and the optimistic CRDT edit whose
`send_crdt_op` failed was applied locally, logged, and forgotten. That is
silent divergence — the GPU keeps showing text the daemon never received,
the exact stalled-daemon case F-008 exists to handle.

Keep a `shutdown_handle` socket clone and `shutdown(Both)` whenever the
outbox closes — the overflow path in `send_event`, and the writer's own
write-failure path. Clones share the socket's file description, so the
shutdown wakes the reader (blocked in `read_message`) with EOF: it fires
the existing `Disconnected` flow, which renders `(daemon disconnected)`,
and the daemon sees the half-close. The fail-fast is now a real teardown
→ the user gets a visible disconnect (and a fresh snapshot on re-attach)
instead of a silently diverged buffer.

New socketpair test asserts a send against a closed outbox drives the peer
to EOF. Auto-reconnect/resync remains deferred (named in the framing).

Validated: fmt clean; clippy -p pmacs-gpu --all-targets clean; 52
pmacs-gpu tests pass (incl. the new shutdown test + both headless renders
on the local adapter).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-03 17:14:14 -04:00
Levi Neuwirth be6943c49d fix(gpu): attach robustness — clear non-CRDT error, bounded queue, clamped dropdown (F-003/F-008/F-007)
GPU attach-path robustness batch from the repo audit. All three live in
pmacs-gpu; no protocol or daemon change.

F-003 — a daemon built without `--features crdt` advertises
`crdt_replica`/`semantic_render` as false in its `Hello`; negotiation then
"succeeds" but no BufferSnapshot ever arrives and the window hangs on
`(connecting...)`. The daemon already tells us its capabilities in Hello,
so check them client-side right after the handshake and fail with an
actionable in-window line ("daemon lacks CRDT support — restart it built
with --features crdt") instead of hanging. New CapabilityMismatch error +
missing_capabilities() + window_status(). No AttachResponse/daemon change.

F-008 — the outbound FrontendEvent queue was an unbounded mpsc, so a
stalled daemon grew memory without bound and replayed stale
viewport/pointer traffic on recovery. Replace it with a bounded,
coalescing Outbox (Mutex + Condvar): a Viewport or Pointer{Drag} whose
kind matches the queue tail replaces it (collapsing scroll/drag floods to
O(1) without reordering across a click or key), everything else is
appended lossless, and a lossless append past OUTBOX_MAX fails fast
(closes the outbox → clean disconnect/resync) rather than silently drop a
CrdtOp and desync the optimistic replica.

F-007 — the completion dropdown grew upward by n*row_height with no clamp,
so a short window rendered rows above y=0 with the selection off-screen.
Add mb_dropdown_window(n, selected, band_top) → (first, count): clamp the
count to rows that fit (hide when not even one fits, so top_y is never
negative) and scroll to keep the selection visible. glyphon's existing
TextBounds clip the scrolled-out rows; the buffer is still shaped once, so
no per-resize re-shape. The whole-fits path is (0, n) — byte-identical to
before.

Framing/as-built: docs/gpu-attach-robustness-framing.md.

Validated: fmt clean; clippy -p pmacs-gpu --all-targets clean; 51
pmacs-gpu unit tests pass (9 new across the three findings), incl. the two
headless render tests on the local Vulkan adapter. Still needs a human
eyeball (non-CRDT banner; tiny-window dropdown; normal attach renders).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-03 17:01:25 -04:00
Levi Neuwirth 623bd884af fix(gpu): F-004 AltGr strip → Ctrl+Alt only; run pmacs-protocol tests in CI
Two follow-ups on this session's audit work, both on freshly-merged code.

F-004 hardening (regression fix). The AltGr text-input strip
(`is_layout_text`) gated on *any* command modifier, so it fired on
Alt-alone. On macOS the Option key is reported as Alt and emits printable
text for most letters (Option+x → "≈"), so every GUI Meta binding (M-x,
M-f, M-b, …) was stripped to a self-insert. Tighten the gate to the true
AltGr signature — both Ctrl and Alt (the LCtrl+RAlt the OS synthesizes on
Windows) — so Alt-alone forwards as a Meta chord again while Windows AltGr
still inserts. Strict narrowing of when we strip: no-op on Linux/Windows,
unblocks macOS Option-as-Meta. Test flips the Alt-alone € assertion and
adds the macOS Option+x case.

CI coverage (F-001 residue). `workspace_default_members` is only the root
`pmacs` package, so `cargo test` skipped pmacs-protocol — the shared wire
format the daemon, TUI, and GPU all depend on. Add
`cargo test -p pmacs-protocol --all-targets` to the test job (its ~12
encode/decode + transport-framing tests). All three first-party crates now
run in CI (root pmacs, pmacs-gpu via the render job, now pmacs-protocol).

Validated: fmt clean; clippy -p pmacs-gpu --all-targets clean; pmacs-gpu
tests 42 pass (render tests on the local adapter, PMACS_REQUIRE_GPU=1);
pmacs-protocol 12 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-03 14:38:19 -04:00
Levi Neuwirth 68542adaa5 docs: GPU headless render harness framing + as-built (Q#GH)
Records the gap (no test exercised the wgpu path), the enabling refactor
(window/surface Optional + render_to_view split + offscreen readback),
the narrow smoke scope, and the lavapipe CI decision.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-03 12:15:19 -04:00
Levi Neuwirth 8c962998f6 audit remediation: workspace clippy gate + stale metadata + cruft (F-001/F-013/F-015)
Acts on the 2026-07-03 repository audit (tracked as
docs/repository-audit-2026-07-03.md).

F-001 — the pmacs-gpu clippy CI blind spot, closed:
- Fix the 3 workspace-clippy lints in pmacs-gpu/src/main.rs (two
  cast_possible_wrap in minimap_jump_to via i64::try_from; a
  format_push_string in compose_status_spans via write!).
- README documents `cargo clippy --workspace --all-targets` and
  `cargo test --workspace`.
- CI's Lint job now also runs `cargo clippy -p pmacs-gpu --all-targets
  -- -D warnings` — the root-package clippy never lints the flavor-
  independent pmacs-gpu crate, which is how these (and recent) warnings
  slipped through.

F-013 — refresh stale metadata: pmacs-gpu/Cargo.toml description and the
docs/pmacs-gpu-design.md status header, from "session 2: hello-world /
pre-implementation" to the real feature set + the per-feature docs.

F-015 — .gitignore editor backups (escaped `#*#`, `.local-bak`) and the
root `/pmacs` dev symlink, so they stop showing as untracked. Untracked
docs are left for triage per the audit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-03 10:31:17 -04:00
Levi Neuwirth 0de4b3bee0 docs: GPU general chord forwarding framing + as-built (Q#GC)
Records the rule (forward Char/Enter/Tab + Ctrl/Alt), what stays local
(Ctrl-V paste, Escape, Meta/Super → OS), what didn't change
(should_forward_key, motion, the optimistic path), and the bets that held.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-03 10:05:55 -04:00
Levi Neuwirth e0f7ff6eca docs: GPU minibuffer framing + as-built (Q#MB)
Framing + as-built record: the render-only architecture, the v12 wire, the
band prompt line + caret, the candidate dropdown, opening-chord forwarding,
the delivered phasing, and an "As-built divergences" section (monospace
caret, a third TextRenderer, best-at-top dropdown ordering) plus the
categorical bets that held.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-06-30 21:03:32 -04:00
Levi Neuwirth 844821bd27 docs: right-click context menu framing + as-built (Q#CM)
Consolidates the framing into an as-built record: the architecture,
context model, clipboard, input/routing, wire (v11), the delivered
phasing, and an "As-built divergences" section noting where the build
departed from the framing (no `SetClipboard` message, resolve runs in
Lua, `MenuPrompt` carries rows not "MenuItemWire", Emacs clipboard keys,
etc.) plus the categorical bets that held.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-06-27 22:19:00 -04:00
Levi Neuwirth 6b7d3fb95d docs: consolidate in-buffer search framing (substring + regex)
Folds incremental-search-framing.md (#70) and regex-search-framing.md
into a single in-buffer-search-framing.md that records the design as
shipped, and removes the two superseded docs. The incremental doc in
particular described the C-f + minibuffer-hosted plan that never
shipped (C-s/C-r + a dedicated core SearchSession did), which was
actively misleading.

The consolidated doc adds an "As-built divergences" section capturing
the four places implementation departed from the framing passes:
C-f → C-s/C-r (veto resolved), minibuffer-hosted → frontend-agnostic
core mode (the GUI has no minibuffer), regex deferred → shipped, and
single-line substring → multi-line regex wash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 15:03:31 -04:00
Levi Neuwirth e03f5620c9 framing: regex in-buffer search (multi-line, C-M-s + M-r toggle)
Extends the merged isearch (#70) with regex matching: both dedicated
entry keys (C-M-s / C-M-r) and a mid-search toggle (M-r), with
multi-line matches. Q#RX1–RX6 stances + bets.

Key survey finding recorded: the GPU's push_glyph_extent_rects already
fans a byte range across visual lines (multi-line selections use it),
so multi-line needs no GPU rendering change — only the TUI SearchView
(which assumed single-line) mirrors paint_local_selection's per-row
wash. SearchPrompt gains regex/invalid → protocol v10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 14:20:47 -04:00
Levi Neuwirth d45b064ab2 framing: incremental in-buffer search (smart-case isearch)
Decided: incremental isearch (live highlight, same key steps next,
Enter accepts, Esc restores origin) + smart-case substring. Q#SR1
per-buffer SearchStore mirroring DiagnosticStore; Q#SR2 smart-case
find_all over a rope snapshot (regex deferred); Q#SR3 producer +
TUI SearchView mirror the diagnostics path; Q#SR4 yellow/amber
palette; Q#SR5 minibuffer-hosted query + new on_changed hook +
next/prev interception; Q#SR6 navigation mirrors diag.next/prev.
Four bets (stale-after-edit linger the headline). Three-commit arc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:33:40 -04:00
Levi Neuwirth db073cc668 CUA type-over is a single undo step (Q#U1)
Typing over a selection composed two edits in Lua —
delete_region() + insert_char() — so it recorded two undo steps:
one undo left the half-replaced text, two restored the original.
Undo granularity is per apply_edit in both modes (v0.1 pushes one
UndoEntry per edit; CRDT commits per edit via export and groups by
commit, with record_checkpoint unused), so the fix is to make
type-over one edit.

New core EditorCore::insert_char_over_region emits a single
EditOp::Replace when a region is active (cursor past the inserted
bytes, selection cleared) and delegates to insert_char otherwise.
The three type-over commands (buffer.newline / tab / self-insert)
call it via a new Lua binding instead of the delete+insert pair.
delete_region and insert_char are unchanged for their other
callers; region-aware backspace/delete already emit one op.

Verified one undo unit in BOTH modes (dual_mode
replace_is_a_single_undo_step covers v01 + crdt — a CRDT Replace is
delete-then-insert internally but one commit) plus an end-to-end
acceptance test through the key-dispatch path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 20:00:53 -04:00
Levi Neuwirth 4d71bf7455 pmacs-gpu: wavy diagnostic squiggles (Q#W1)
Replaces the straight 2px diagnostic underline bar (PR #65) with an
actual sine squiggle. A dedicated SquiggleRenderer pipeline whose
fragment shader computes A·sin(x·2π/λ) and alphas pixels by distance
to the curve (fwidth/smoothstep — core WGSL, no MSAA or feature
flag). Geometry reuses push_glyph_extent_rects unchanged; only the
vertex format differs, adding a uv: absolute screen-x (so phase is
continuous across separately emitted glyph-run rects) and signed px
from the band centerline.

The diagnostic underline is split out of the solid wash batch into
its own buffer + draw, slotted under the glyphs where the bar was;
washes (selection / current-line) stay solid quads. The severity
palette (decoration_kind_to_underline_color) and minimap marks are
untouched. DIAG_UNDERLINE_PX (2px) → DIAG_SQUIGGLE_PX (6px) so the
wave fits inside the band.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:41:56 -04:00
Levi Neuwirth 70a26d4ac1 framing: GPU status band — local freshness + v8 StatusFacts
Q#S1 splits facts by authority: L:C and scroll derive locally (the
optimistic caret must not lag a round trip); name, modified, and
exact diag counts arrive via a new additive StatusFacts variant
(protocol v8). The reserved ModeLine(Vec<Cell>) stays grid-shaped
and unused. Q#S2 quad + second TextArea; Q#S3 one text_area_bottom
helper for the five bottom-assuming geometry sites. Three bets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:45:12 -04:00
Levi Neuwirth aea08a58ec framing: mouse deferred set — triple-click, Shift-click, minimap jump, edge auto-scroll
Q#M4 TripleDown variant (protocol v7, the cheap additive kind —
SUPPORTED becomes [6,7], ladder restarts on the v6 floor) + daemon
select_line_at_cursor incl. trailing newline. Q#M5 Shift-click
extension daemon-side, zero wire change (mods already carried).
Q#M6 minimap jump GPU-local, consumed before text hit-testing,
scrubbable. Q#M7 edge auto-scroll via ControlFlow::WaitUntil tick
re-firing the drag hit-test. Three bets recorded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 12:12:18 -04:00
Levi Neuwirth a11d9ab374 framing: pmacs-gpu diagnostic parity — quad squiggles, minimap marks
Q#D1 quad-pipeline 2px bars (glyphon draws no underlines), Q#D2
retire the diagnostic fg recolor, Q#D3 minimap marks via v6
underline_color in FileStyleSummary, Q#D4 defer counts (no GPU
status band). Three categorical bets recorded, incl. the summary
recompute-gate (generation-only today; diagnostic publishes must
refresh marks without naive traffic growth).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:00:06 -04:00
Levi Neuwirth 7bde2e7079 session R — per-line incremental reshape (typing latency floor)
Per docs/pmacs-gpu-perline-reshape-framing.md. Every keystroke ran a
full visible-slice reshape: rebuild all rich chunks, set_rich_text
(resets every BufferLine's shape cache), shape_until_scroll re-shapes
every visible line with Shaping::Advanced. Now a single-line edit —
the keystroke case — rebuilds exactly ONE BufferLine; the other
lines' shape caches survive and shape_until_scroll touches only the
fresh line.

- Q#R1: clipped_chunks_for_range is the single chunk source both the
  full reshape and the surgery derive from (full = slice range,
  surgery = the line's content range), so the two paths cannot
  disagree about a line's content. Parity with cosmic-text's own
  line splitting verified against the vendored 0.18.2 source:
  BidiParagraphs strips the separator per line in both its ASCII and
  BidiInfo paths, creates no trailing empty line, and set_rich_text
  assigns LineEnding::Lf uniformly + adds attr spans only when they
  differ from the defaults — the surgery mirrors all three.
- Fallbacks to full reshape: slice origin moved, line count changed
  (Enter / multi-line deletes), '\n' in the inserted text, edited
  line outside the shaped slice (an edit entirely PAST the slice updates
  view_range + redraws without any shaping), exotic paragraph
  separators, multi-edit batches.
- Q#R2: the pointer hit map goes lazy — surgery marks it dirty and
  hit_test_source_byte rebuilds on demand from the same chunk fn
  (clicks are rare next to keystrokes; the rebuild is an O(slice)
  byte walk, no shaping).
- Pure parity test pins full-walk == concatenated per-line walks
  (text + colors), including the newline-anchored inlay-hint
  boundary case (predicted finding #1's most likely site).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 15:01:51 -04:00
Levi Neuwirth 9528595c0e session M-1 — Pointer wire + daemon byte-space mouse semantics
Per docs/pmacs-gpu-mouse-framing.md (resolves the deferred Q#B5):
a pixel frontend cannot express the daemon's cell coordinates —
inline adornments shift visual columns invisibly to cell space and
the design contract forbids hit-test round trips — so the frontend
hit-tests locally and ships source-byte gestures.

- protocol v5: FrontendEvent::Pointer { buffer_id, byte, kind, mods }
  with PointerKind { Down, Drag, Up, DoubleDown }. Double-click
  detection is frontend-side (only it knows pixel proximity).
  SUPPORTED_PROTOCOL_VERSIONS gains 5; the send gate runs in the
  frontend (an older instance cannot decode the variant).
- daemon: dispatch_pointer replays the existing mouse gesture
  semantics in byte space against the semantic session's window —
  Down places + anchors, Drag grows, Up collapses an empty click,
  DoubleDown selects the word. Routed by the authenticated source
  (CrdtOp/Viewport trust rule); hit bytes clamp + snap to UTF-8
  boundaries (a hit can race an in-flight edit).
- word_range_at fix (pre-existing CUA bug the new test surfaced):
  double-clicking a word's FIRST character selected the previous
  word too — backward_word from pos sees the non-word char behind
  the hit and crosses over; walk from pos + ch_len instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 13:10:17 -04:00
Levi Neuwirth 5762171831 docs: viewport-scoped rendering + scroll framing (perf)
Frames the fix for unusable large-file editing in pmacs-gpu: render
only the visible byte slice (O(visible) not O(file)) + line-based
scroll. Stance: feed cosmic-text only current_text[vstart..vend] (the
native Scroll path only makes shaping lazy, not set_rich_text /
projected_rich_chunks, which dominate). Q-decisions: line-based scroll,
caret-follow auto-scroll, small overscan, rebase-by-vstart, scoped
Viewport declaration; daemon whole-file highlight query deferred
(Q#S6). Bet S2 (coordinate-space rebasing) flagged as the QB3-class
risk. Fact-checked: all load-bearing claims hold (GPU declares
whole-file viewport; reshape is O(file); producer already clips spans
to vp.visible; cosmic-text splits BufferLines on \n so slices must be
line-aligned; caret/wash builders use whole-file offsets).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:28:59 -04:00
Levi Neuwirth e9bbd1f024 docs: close quad-backgrounds arc — audit + framing status
Records the 9.1–9.3 scoring and the QB1–QB3 follow-on findings that
manual validation surfaced (read-only-mirror sourcing, per-tick
whole-file recompute, line-relative glyph offsets). Marks the framing
doc CLOSED and retires Phase A finding A8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:49:21 -04:00
Levi Neuwirth 57feae1e2d session 9.3 — peer-presence cursor/selection (fixes QB1)
Manual validation of 9.2 surfaced finding QB1: the CurrentLine wash
never appeared, and 9.1's Selection never actually rendered either.

Root cause: `Selection` and `CurrentLine` are the only two
per-WINDOW-state decorations; every other rendered family (StyleSpans,
diagnostics, inlay hints, minimap) is keyed to the shared BUFFER. The
producer emits both from the viewing frontend's own window
(`active_window_for(self.frontend_id)`). pmacs-gpu is a read-only
mirror with no input path — it never sends Key/cursor events, so its
own window's cursor stays pinned at 0 and its selection stays None.
Both decorations are therefore inert in pmacs-gpu: CurrentLine paints
a static line-0 wash (invisible at alpha 0.08) and Selection never
appears. What the user actually watches is the *editing* frontend's
(their TUI's) cursor — which is peer presence.

Fix is consumer-only — no producer or protocol change. The wire
already carries it: `InstanceMessage::PresenceUpdate { frontend_id,
buffer_id, cursor, selection }` is broadcast by the daemon to every
`multi_frontend` recipient, pmacs-gpu already negotiates
`multi_frontend: true`, and it was simply dropping the message at its
`_ => None` catch-all.

Q#5 (recorded in the framing doc): peer presence is the authoritative
cursor/selection source for a read-only mirror.

- New `peer_presences: HashMap<FrontendId, PeerPresence>` state,
  cleared on BufferSnapshot (peer offsets are prior-buffer-relative).
- New `PresenceUpdate` arm stores per-peer (buffer_id, cursor,
  selection) and requests a redraw.
- `peer_background_rects` replaces the old
  `decoration_background_rects`: renders `CurrentLine` over the source
  line holding each peer's cursor (`source_line_range`) and
  `Selection` over each peer's selected range, both via the shared
  `push_glyph_extent_rects` (the former inline glyph-overlap loop,
  extracted). Own-window Selection/CurrentLine in `current_decorations`
  are no longer drawn as backgrounds — they're inert for a read-only
  mirror. Diagnostic (foreground) decorations are untouched.
- The producer keeps emitting own-window Selection/CurrentLine (9.1/
  9.2) unchanged — correct and forward-looking for when pmacs-gpu
  gains its own input in Phase B; simply unconsumed-for-backgrounds by
  the mirror today.

Deferred within the stance (documented): per-peer stable colors (single
peer reuses the Selection/CurrentLine colors), peer caret glyph +
"user N" label, and own-vs-peer cursor merge once input lands.

New tests: `source_line_range_locates_enclosing_line` +
`source_line_range_handles_empty_and_leading_newline`. The peer
rect generation itself needs a laid-out buffer (font system) and is
covered by the manual probe.

Two pre-existing functions tipped past clippy's 100-line limit by the
additions (`State::new` 101, `apply_attach_message` 116, a per-variant
match dispatcher); both get `#[allow(clippy::too_many_lines)]`,
matching the precedent on `semantic_render::render_frame`.

Gates green:
- cargo fmt --all -- --check
- cargo clippy --all-targets --workspace -- -D warnings
- cargo clippy --all-targets --workspace --features crdt -- -D warnings
- pmacs lib 1329 + pmacs-protocol 11
- pmacs-gpu unit 15 (+2 source_line_range tests)
- m4_acceptance 88, m11_5_semantic_acceptance (--features crdt) 2

Manual probe: daemon + TUI attach + pmacs-gpu attach. Move the cursor
in the TUI — pmacs-gpu's CurrentLine wash should track the TUI's line.
Select text in the TUI — the Selection wash should mirror it. Both
should now actually appear and follow the editing frontend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 14:42:52 -04:00
Levi Neuwirth 54991c89e4 session 9.1 — Selection quad backgrounds + framing doc
Retires the first half of Phase A's deferred finding A8 (background-
bearing decoration kinds couldn't render through cosmic-text's
foreground-only `Attrs`). `DecorationKind::Selection` now paints a
translucent blue rectangle under the selected glyphs in pmacs-gpu,
reusing the wgpu `QuadRenderer` that shipped for the minimap in
session 7.

The framing doc (`docs/pmacs-gpu-quad-backgrounds-framing.md`)
commits the load-bearing decisions before code lands: stance (α)
for Q#2 — single render pass, three draws in the order backgrounds
→ text → minimap — is what this commit implements. Q#1 (CurrentLine
source location, stance α: producer-side from
`core.active_window_for(self.frontend_id).cursor`) and Q#3 (per-line
cadence, stance β) are sketched for session 9.2; Q#4 defers search
backgrounds awaiting an upstream pmacs search feature.

Three components:

1. `decoration_kind_to_bg_color` helper, sibling of the existing
   `decoration_kind_to_color`. Returns `Some([f32; 4])` RGBA for
   Selection; `None` for CurrentLine (9.2), SearchMatch /
   SearchMatchActive (deferred), and the four diagnostic kinds
   (foreground-only). New unit tests assert disjoint total cover
   between the two helpers across the eight kinds.

2. `State::decoration_background_rects` walks
   `Buffer::layout_runs()`, finds glyphs whose `[start, end)`
   overlaps each background-bearing decoration's `ByteRange`, and
   produces one `MinimapRect` per laid-out visual line that
   contributes glyphs. Multi-line selections fan out as N rects.

3. Render-order change in `State::render`: a `bg_buffer` is built
   ahead of the minimap buffer and drawn first in the render pass
   (before `text_renderer.render`), so selection fills sit under
   the glyphs with the 0.30-alpha letting source color show through.
   Minimap continues to draw last.

Gates green:
- cargo fmt --all -- --check
- cargo clippy --all-targets --workspace -- -D warnings
- cargo clippy --all-targets --workspace --features crdt -- -D warnings
- pmacs-gpu unit: 13 (+2 new bg-color helper tests)
- pmacs lib: 1325, pmacs-protocol: 11
- m4_acceptance: 88, m11_5_semantic_acceptance (--features crdt): 2

Bet exercise so far: bet #1 (multi-line vertex decomposition) is
implicitly tested by the layout-run loop but waits on visual
validation for honest scoring. Bet #2 (overlap composition) is not
exercised in 9.1 — Selection is the only background kind, so no
overlaps with CurrentLine or future kinds. Bet #3 (cadence) is a
9.2 concern.

Manual probe: launch daemon + TUI attach + pmacs-gpu attach against
any file, select text in the TUI, verify the pmacs-gpu window paints
a translucent blue rectangle over the selected glyphs that tracks
selection extension. Multi-line selection should produce per-visual-
line rectangles.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:49:52 -04:00
Levi Neuwirth a67cb8a6f1 Close pmacs-gpu phase A audit 2026-05-28 12:49:23 -04:00
Levi Neuwirth a706864e04 session 2: pmacs-gpu workspace + wgpu/winit/glyphon hello-world
Adds the pmacs-gpu binary crate to the workspace. wgpu 29.0 + winit
0.30 + glyphon 0.11 (cosmic-text 0.18 via re-export) + pollster +
env_logger; pmacs-protocol in the dep graph but not consumed yet
(session 3 wires the attach loop).

The binary opens an 800x200 window titled 'pmacs-gpu hello-world',
sets up wgpu against its surface, configures glyphon with the bundled
JetBrains Mono Regular, and renders 'hello, pmacs' once per redraw.
Close button or Escape exits. Resize re-configures the surface and
glyphon viewport. Surface acquisition matches wgpu 29's
CurrentSurfaceTexture enum (success/suboptimal render through; lost/
outdated re-configure; timeout/occluded skip the frame).

Bundled assets: pmacs-gpu/fonts/JetBrainsMono-Regular.ttf (268 KB)
and pmacs-gpu/fonts/OFL.txt. Font shipped as required by the SIL
Open Font License 1.1.

One finding surfaced during the move and absorbed under rule (iii)
of the framing pass (small / no structural change): the design doc
recorded JetBrains Mono as Apache 2.0; the actual license has been
OFL since the family's open-source release. Doc corrected in
docs/pmacs-gpu-design.md.

Gates: cargo fmt + cargo clippy --all-targets -D warnings clean for
the whole workspace; cargo test --lib still 1314 (pmacs main crate
untouched); m4_acceptance 83; m11_5_semantic_acceptance --features
crdt 2.

Visual confirmation pending — agent environment is headless, so
'window opens, text renders' is user-side validation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:37:26 -04:00
Levi Neuwirth 1dc36f7cd4 docs: pmacs-gpu design note (framing pass closed)
Post-v1.0 design artifact for the GPU/GUI frontend. Inherits the
contract boundary from semantic-frontend-protocol.md and applies the
M10-matured framing discipline to a multi-month effort:

- Toolkit: wgpu + custom + cosmic-text + glyphon. Unambiguous.
  Records the against-gpui case so the decision isn't relitigated.
- Scope: A (read-only viewer, ~2-3 weeks, adversarial verification of
  the producer arc) → B (TUI parity, 2-3 months after A) → C
  (beyond-TUI, 3-4 months after B). Sequential, not alternative.
  ~6 months to parity, ~9-10 to beyond-TUI; recorded honestly at
  decision time.
- Phase A's framing is adversarial verification, not "build a viewer
  that works." Six static probes + one temporal probe drive the
  corpus; the viewer is the artifact of having done so.
- Predicted findings: five categorical bets, scored as a category
  matrix not a count, methodology recorded before data lands.
- Finding feedback loop: classification rule (iii) pre-authorized —
  small absorbs into Phase A, structural deferred per the
  verification-milestone premise-check.
- Distribution: workspace + separate pmacs-gpu binary. pmacs-protocol
  crate extraction as a discrete 4-hour prerequisite PR before any
  GPU work.
- Q#1 (visual motion) committed to stance β: frontend implements
  visual motion; instance stays pixel-pure. Phase A starts with
  (β-impl) — recompute wrap on motion events — with documented
  upgrade path to (α-impl) if smooth scroll lands.
- Cursor scope at v0.1: blink, multi-cursor rendering, peer cursors.
  Multi-cursor commands out of scope.
- Font: bundle JetBrains Mono + Lua override; tofu fallback for
  missing glyphs; real fallback chain is v0.2+.
- Rhythm: cadence relaxes from hour-level to daily-PR for larger
  features; discipline anchor moves from per-PR to per-session.
- Audit artifacts: this doc is the design artifact; per-phase audit
  material lands in separate per-phase audit docs (M10.x pattern).

Session plan: 1 = pmacs-protocol extraction; 2 = pmacs-gpu workspace
+ hello-world; 3 = attach loop; 4+ = Phase A proper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:47:45 -04:00
Levi Neuwirth c692710321 M1+M2: FileStyleSummary (minimap producer, Q#2 resolved) + doc
New InstanceMessage::FileStyleSummary { buffer_id, generation, lines:
Vec<Style> }: a coarse whole-file styling summary for a Zed/VSCode-
style minimap, resolving the design note's Open Q#2. One dominant
Style per source line (by byte count across the producer's current
spans); the frontend maps minimap rows to one or more lines.

Producer scoped_file_summary reuses scoped_style_spans with a whole-
buffer synthetic viewport, so policy A's authority pick (tree-sitter
for grammar-backed languages, LSP semantic tokens otherwise) is
inherited automatically — no separate styling path. file_style_summary_msg
is keyed on the buffer's CRDT generation: an idle buffer at the same
generation pays nothing (the whole-file summary is the expensive bit
on large files, so re-emit only after edits). First frame for a
buffer always emits; the existing first-frame test updated to expect
3 messages (StyleSpans + Decorations + FileStyleSummary).

Per-line dominant style is the v1 representation. Future refinements
(fixed-N bands; whole-file RLE style runs) are recorded in the design
note as straightforward extensions if a real frontend prefers them.

Structural gating same as the other semantic families: the daemon
only constructs a SemanticRenderState for sessions that negotiated
semantic_render, so non-semantic sessions never receive it. Grid TUI
adds the variant to its ignore list. Round-trip fixture covers it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 19:23:20 -04:00
Levi Neuwirth 8ec3abcad5 Step 3+5: InlineAdornments producer from LSP inlay hints + doc update
scoped_inline_adornments (free fn, mirrors scoped_style_spans) reads
the inlay-hint store via for_uri and maps each InlayHint to an
InlineAdornment { at, AtOffset, Text{padded label, default style} },
clipped to the declared viewport (anchor in [vis_start, vis_end)).
Step 0 established inlay columns are already byte offsets by the time
they reach the store (inbound_converted rewrites the Position-shaped
InlayHint.position), so line_col_to_byte is exact with no per-server
encoding — unlike semantic-token styling.

inline_adornments_msg does the suppression: the InlineAdornments wire
variant has no generation/full/segments, so this is M11.2-level only
(whole-set re-send on any change, nothing when byte-identical, and
never an empty frame when there is nothing to say — no spam).

Tests: clip-to-viewport + padding + AtOffset, suppress-then-resync,
no-emit-without-hints; the old never-emitted invariant is split into
block_adornments_and_fold_state_still_never_emitted (Block/Fold are
still unwired) plus inline_adornments_not_emitted_without_hints.
assert_semantic_only now admits InlineAdornments.

Step 5 (folded): docs/semantic-frontend-protocol.md moves StyleSpans
(policy A) + InlineAdornments out of "declared, not wired", and adds
two deferred Open questions — per-byte tree-sitter/LSP blend, and
multiple-servers-one-URI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:00:44 -04:00
Levi Neuwirth baddd22878 docs: land the semantic-frontend-protocol design note
The spec the M11 arc (M11.1–M11.5) implements; all five CHANGELOG
entries reference it. Status header updated from "post-v1.0 design
draft, no code" to an implementation map against the arc, so the
landed doc is not self-contradictory. Design body unchanged; the
"Open questions" remain open by design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:26:21 -04:00
Levi Neuwirth c50db222d3 V0.2-prerequisite pull-forward + M10.11 clean audit round
Pulls a set of planned V0.2 prerequisites forward to ship in v1.0,
plus the clean audit-review round over that work.

V0.2-prereq implementations (documented promotions, not M11
surprises; operator raised the v1.0 public-API ceiling to absorb
them — see V0.2-PREREQUISITES.md "v1.0 pull-forward"):

- CC-1: `bypass_intercept` opts on buffer insert/delete/replace —
  skips the Lua intercept chain only; preserves the same-buffer
  re-entry guard, undo/dirty bookkeeping, view notifications, and
  CRDT broadcast queueing.
- CC-2: `pmacs.buffer.on_removed(buf, cb)` + idempotent `:remove()`
  handle; buffer-local keymaps pruned on removal. Fires for both
  `pmacs.buffer.remove` and `.kill` (incl. interactive C-x k);
  callback errors logged to *errors* without failing the removal.
- SP-4: `pmacs.buffer.from_file`.
- SP-5: `pmacs.fs.watch` (polling; `:cancel()`/`:is_cancelled()`).
- SP-7: `pmacs.async.yield_to_next_tick` (worker-free next-tick
  yield); outline-aggregate repaint now uses it instead of
  workers.sleep(0):await(), pinning propagation to one async tick.
- SP-1: `pmacs.editor.move_to_line` (0-based, clamps out-of-range).
- SP-6: `pmacs.outline.query` published by pmacs-outline.
- SP-3: audit rule 15 `reach-around-require-field` (Info).
- CC-3: runtime API-availability documented (docs-only).

Clean audit-review round (M10.11 framing stop-condition pass):

- Finding 1 (fixed): clippy needless_raw_string_hashes blocked
  `clippy -D warnings` on both lanes; raw-string delimiter fixed.
- Finding 2 (fixed): fs_watch acceptance test was racy — the
  `pending == 1` gate could not distinguish the in-flight baseline
  stat from the steady-state poll sleep, so under load the mutation
  raced the baseline (~1/3 fail in the default lane). Rewritten to
  re-emit a distinct change each pump iteration; 6/6 on the
  previously-failing invocation.
- Finding 3 (fixed): documented fs.watch's async-baseline startup
  window and size+mtime-granularity detection limit.
- Finding 4 / SP-8 (logged, non-blocking, out of diff): a
  pre-existing PTY-lifecycle test timing flake under severe CPU
  oversubscription; src/process.rs untouched here.

CC-1's opts-extension-counts question resolved explicitly
(consistent treatment: counted; ceiling raised to fit).

Gate at normal load, both lanes: fmt clean; clippy --all-targets
-D warnings clean; non-crdt lib 1223/0; crdt lib 1377/0;
m8_1/m8_9/m8_10 green.

Not in scope here: v1.0 CHANGELOG body, version bump, the M10.11
Finding-4 (reattach undo) user-facing artifact, and the recorded
two-laptop manual acceptance — tracked as the remaining v1.0 steps.

.gitignore: M*-FRAMING.md added to the internal-only block for
consistency with the M*-AUDIT.md / M*-SHIP-GATE.md siblings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 10:31:31 -04:00
Levi Neuwirth 587a2a15de M9 ship gate
Land the Model Context Protocol (MCP) integration as a transport binding,
not a built-in feature. Six Lua functions plus userdata methods expose
the substance of three MCP feature areas (resources, tools, prompts), a
notification dispatcher, and a non-trivial AI-assistance example package
that meets the architectural ship gate (spec/pmacs-spec.tex:1572): zero
direct calls into the Rust core, zero special-cased MCP handling outside
the public API, source under 2000 lines of Lua.

The M9.5 -> M9.6 -> M9.7 -> M9.8 layered composition validates the claim
"AI is a transport binding, not a feature" -- pmacs-mcp-ai composes with
pmacs-mcp-prompts.render and inherits notification handling transitively
through M9.7's package, demonstrating that the AI domain is a layer
above MCP, not a thread woven through the core.

Subtask shape:
  M9.1 stdio transport + initialize handshake + restart policy
  M9.2 resources with in-flight + settled cache and per-uri invalidation
  M9.3 tools with isError-vs-JSON-RPC-error semantics + cancellation
  M9.4 prompts with required-argument validation
  M9.5 notification dispatcher (on_notification, off_notification)
  M9.6 tools-as-commands fixture package + 12 audit findings disposed
  M9.7 prompts-as-result-buffers fixture package + tree-sitter-md grammar
  M9.8 AI-assistance fixture package (363+ LoC; 17/17 acceptance tests)
  M9.9 formal package audit -- PASS on all three criteria
  M9.10 release: TRANSITION-M9.md + MCP-for-package-authors guide

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:04:23 -04:00
Levi Neuwirth 0b715de505 M7 tail: package system, audit lint, lockfile, resolver 2026-05-07 16:50:37 -04:00
Levi Neuwirth 291eb0fd8d Fix CI and Documentation issues 2026-05-04 10:19:19 -04:00
Levi Neuwirth c8d0d67615 Fix PTY final-output drain race 2026-05-04 09:44:30 -04:00