Normalize source bytes to representable shaped-cluster cursors and
reflow the code buffer whenever gutter or minimap geometry changes.
Measure alternate monospace advances across complete shaped runs and
load hermetic fixture faces before FontSystem construction. Complete
the rendered geometry, popup, caret, snapshot, and fixture acceptance
coverage, and record the review fixes in framing revision 5.
tests/gpu_font_acceptance.rs (items 2-7; the header manifest routes
item 1 to src/protocol.rs pins, item 8 to the src/frontend.rs TUI
drop unit, items 9-14/16-19 to pmacs-gpu's headless suite, and item
15 to the docs):
- 2: a fresh attachment's first frame carries the REAL (None, None)
default; unchanged ticks are silent; a late joiner receives the
current preference without post-attach mutation
- 3: a mid-session set_font emits exactly one FontFacts on the next
frame; an identical re-set advances the epoch without emitting
- 4 (producer half): on_buffer_snapshot_sent re-ships buffer facts
but never the bufferless FontFacts
- 5: real-daemon probe -- a v17 semantic session receives FontFacts,
a v16 peer never does (crdt feature)
- 6: the strict Lua contract -- 5.999/72.01/0/-16/NaN/inf/non-number
sizes error naming `size`; empty/non-string family errors
naming `family`; unknown keys rejected by name; hostile
__index/__pairs metatables never invoked and never inject
values; quantization pins 15.994->1599 / 15.996->1600 and both
boundaries; set_font {} resets both axes; the getter returns a
fresh quantized plain table; every rejected shape leaves the
preference and the wire untouched
- 7: a load_user_config_at fixture's init.lua set_font lands in the
handle installed before user config, and a pre-attach
preference ships on the first frame
Bites (mutate, observe the acceptance test fail, restore): the
Option-seeded first-frame send, the payload compare, the snapshot
survival of the font baselines, the v17 gate (BOTH halves widened --
producer for_peer alone leaks nothing because the daemon skip arm
still filters; the wire test only fails when belt AND braces are
cut), the unknown-key rejection, the range-check-original ordering,
and the raw_get metatable isolation.
Docs (item 15): docs/semantic-frontend-protocol.md gains the
FontFacts variant entry (v17 gate, authoritative default, no-pixels
preference relay, fail-closed receiver) and folds FontFacts into the
BufferSnapshot reset-contract paragraph (bufferless facts survive;
the frontend's caret-follow scroll residual is the buffer-scoped
part). docs/pmacs-gpu-design.md's "future customization needs no
wire-protocol changes" claim is corrected in place and points at the
framing as design of record; the Lua-override bullet now names the
landed set_font shape. The framing status bumps to implemented.
Also re-homes the ProcessSupervisor doc comment that the make_font_pref
insertion had orphaned onto the wrong function.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
pmacs.gpu.set_font { family?, size? }: one global daemon-side font
preference relayed to GPU peers as the bufferless FontFacts fact at
protocol v17 (size in integer hundredths of a logical pixel -- the
InstanceMessage Eq derive forbids f32), applied mid-session as one
fail-closed GPU transaction: sanitized current-order fontdb
assembly with four-style monospace queries, derived metric fields
scaled from one knob (band height included), atomic
set_metrics_and_size on all seven buffers with real drawable
dimensions, Wrap::None on row-oriented popups, a measured
advance-ratio probe, and visual-run-aware caret preservation (a
normalized code-buffer Scroll residual + shape_until_cursor,
shared with the CursorByte and optimistic-edit paths, gutter
continuation blanks keeping line numbers aligned). Revisions 2-3
fold framing rounds 1-2 (Eq-safe wire size, wire validation
failing closed, caret repair ordered before the final reshape,
vertical-containment acceptance, sanitized default query,
quantization pins); revision 4 is the user's round 3 (visual runs,
horizontal-scroll deferral, atomicity, popup row invariance,
strict Lua raw-data contract). Acceptance items 1-19.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Post-merge handoff sync for multi-language injections (#122), folded
into the side-quest-backlog PR (rebased onto current main, which now
carries #120 themes stage 1 and #122 injections).
agent-handoff.md:
- §1: main head → 5e73966; add the #122 injection-engine entry (layered
ParseTreeBundle, worker-side layer building off the static grammar
table, settle-time query resolution, the new substrate — injections_query,
the alias snapshot, injection_capped, compute_highlight_spans_for, the
flatten event-sweep, the GPU fold — and the two hard-won findings).
- §6: the "multi-language injections" deferral is retired; replaced with
the real follow-ups (injection.combined, child-tree incrementality,
injectable Lua-registered languages, HTML/CSS/GraphQL/SQL consumer
grammars). Jupyter is now gated on JSON only.
side-quest-backlog.md: injections marked SHIPPED with its remaining
follow-ups; Jupyter gate and north star revised (JSON grammar and
locals-query processing rise to the top now that injections landed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
Consolidate the non-themes backlog into docs/side-quest-backlog.md: an
exhaustive, deduplicated index compiled from every framing doc's
"Deferred (named)" section, the handoff §6 list, the roadmap arcs, and a
code-level marker sweep. Organized as side quests (one-shot-ish, led by
the highlighting/detection/LSP/editing families that continue the
#114–#118 stack), raid bosses (vterm, folding, DAP, GPU splits, Jupyter),
cross-cutting substrate blockers, and a prioritization north star.
Themes/faces/color items are excluded (they're the Arc 4 main quest) and
listed once so they're visibly accounted for. Handoff §6 gains a pointer
to it.
Docs only. git diff --check clean.
Two follow-ups + doc cleanup.
[P2] Same-depth sibling precedence was reversed. The wire priority was
(depth, capture_order), omitting the layer ordinal, so two overlapping
spans from different sibling layers tied — and the active-set insert
then applied the later one first, making the earlier sibling win, the
opposite of the grid's layer-by-layer paint. Priority is now
(layer_index, capture_order): layer_index is the depth-ascending
position in bundle.layers, so a deeper layer AND a later same-depth
sibling both override, matching the grid exactly. New
flatten_same_depth_sibling_later_layer_wins pins it.
[P2] Cap surfacing had no end-to-end test. Added
injection_cap_surfaced_once_and_rearms_via_lua, which drives the real
Lua settle path (syntax.lua tick -> _injection_capped -> pmacs.error)
and asserts surfaced-once, suppressed-on-unchanged-reparse, and
re-armed-after-dropping-below-then-exceeding-the-cap.
Docs:
- Q#IJ6 now states the accurate bound O(n log n + Sum active) for the
event sweep, not O(boundaries).
- The full-buffer perf test is renamed/narrowed to guard the FLATTENER
regression; the summary's per-line dominant-style tally is a separate
pre-existing O(lines x spans) loop, not claimed linear.
- Framing #9 now matches the test: it drives spans_from_segments (the
extracted replace_style_spans transform) + source_color_at, not a live
State render.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
Four review findings + a cleanup bundle.
[P1] Wire flattener was O(spans²) and ran over the WHOLE buffer (the
file-style summary uses a whole-buffer viewport, not the visible one).
Replaced the per-interval full scan with an ordered active-set event
sweep (activate on start, expire on end, fold the active set) — linear
in practice. Added full_buffer_summary_scales_on_large_grammar_file
(1500-line rust) as the perf gate.
[P2] _parse_now used the empty alias map from make_request while
_dispatch snapshotted the registry map, so a `py` fence injected async
but not sync. Snapshot aliases on both paths; pinned by
sync_parse_now_resolves_alias.
[P2] The multi-range inline test used a one-line paragraph, whose block
inline node has no named children (link/emphasis are child-grammar
structures) — one range, so it couldn't falsify multi-range. Replaced
with a multi-line blockquote whose inline node carries a named
block_continuation: content_node_ranges now asserts >1 collected range
and emphasis parses on both lines.
[P2] The layer backstop dropped regions silently; the framing requires
a surfaced warning. run_parse now sets ParseTreeBundle::injection_capped;
syntax.lua's settle tick raises it once per buffer via pmacs.error
(_injection_capped). Added injection_layer_cap_surfaces_and_preserves_root
(drives >4096 fences, asserts the flag + bounded count + intact root).
Cleanup:
- The GPU acceptance test now drives the real StyleSpans full-frame
transform (spans_from_segments, extracted from replace_style_spans)
instead of a hand-rolled sort.
- content_node_ranges excludes NAMED children (documented as a round-1
refinement); framing mechanic #3 / Q#IJ5 updated to match.
- parse_duration doc now says root parse; the markdown entry no longer
describes inline as unhighlighted/future.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
Frame the tree-sitter injection-layer engine: after the root parse,
run the grammar's injections.scm, parse each embedded region with the
injected language, and merge every layer's highlight spans. First
consumer is markdown fenced code + inline (zero new grammars).
Eleven numbered decisions (Q#IJ1-IJ11) and thirteen acceptance tests,
approved over three framing rounds. Load-bearing calls: the layer set
lives inside ParseTreeBundle installed atomically (Q#IJ1); a two-stage
worker/settle handoff keeps parsing off the main thread and query
caching in the Rc registry (Q#IJ2); recursion is depth/count/visited
bounded with child-only failure (Q#IJ3); a case-folded alias map
snapshotted into ParseRequest bridges Lua-set aliases to the worker
(Q#IJ4); included ranges are Vec<Range> excluding children and
intersected with the parent (Q#IJ5); the wire producer flattens layers
into disjoint effective spans because the GPU re-sorts by start, and
the GPU first-span consumer is fixed to fold (Q#IJ6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
Snapshot: main @ 8ce2e9c, protocol v16. Stage-1 summary + the two
substrate contracts the review rounds produced (snapshot/baseline
reset, store-sourced diag-count freeze); NEXT pointer at stage 2
(pmacs.gpu.set_font, protocol v17) and stage 3 (statusline
segments); themes deferrals consolidated into section 6; the
dual-purpose-session-state lesson into section 5.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
The round-4 freeze read counted the retained for_uri vector inside
status_facts_msg -- correct, but StatusFacts runs at frame cadence
for every semantic session under the shared store mutex, so a long
stale interval cost O(frames x diagnostics x sessions).
DiagnosticStore now maintains per-URI severity totals alongside
by_uri: set replaces them (an empty publication removes them with
the vector), clear removes them, and mark_stale deliberately
preserves both -- entries exist exactly when by_uri entries do, and
set/clear are the store's only by_uri mutators. status_facts_msg
reads the tuple in O(1), and the all-URI severity_totals sum reuses
the cached tuples.
Store unit (acceptance item 34) pins the invariant: all four totals
replace correctly, survive staleness, and clear with the diagnostic
vector; empty_set_clears_uri asserts the totals drop too. The
rounds 3-4 freeze acceptance passes unchanged -- behavior parity,
so the unit pin is the evidence (no runtime bite exists for a
behavior-preserving refactor). Round-5 implementation
user-authored; this commit folds it with framing revision 9, the
protocol doc's O(1) note, and the acceptance manifest pointer to
the item-34 unit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
A session first rendering during diagnostic staleness emitted zero
counts: round 3's frozen_diag_counts cache was per-session and only
seeded from fresh computations, so a late joiner attaching mid-edit
(or a buffer first visited between didChange and fresh diagnostics)
had no entry and fell back to (0, 0), contradicting the documented
"frozen counts, never zeros" contract.
mark_stale (T M11.8) keeps the last published diagnostic vector --
only the positions are invalid -- so status_facts_msg now counts
the retained for_uri entries even while stale: the retained entries
ARE the frozen value. The per-session cache is deleted; sourcing
the freeze from the store means there is no session state to lose
to a snapshot reset and no history needed at attach, so the round-3
reset-survival property holds by construction and its round-trip
test passes unchanged against the new mechanism.
Acceptance item 33 marks a populated store stale BEFORE the
SemanticRenderState exists and asserts the first frame reports the
preserved counts; runtime bite vs pre-fix semantic_render.rs.
Framing revision 8; the protocol doc's freeze sentence now says
store knowledge, including the late-joiner case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Finding 1: the round-2 reset contract was asymmetric. The producer
resets search/menu/status baselines on every snapshot send, but the
GPU's BufferSnapshot arm only cleared spans, decorations,
adornments, summary, and the completion popup -- a menu or search
open at switch time survived the snapshot with no close message
ever coming (the new buffer's first CLOSED state is suppressed
daemon-side), leaving a stale popup that also held
daemon_intercepts_keys true and swallowed pointer events
indefinitely. The arm now clears search_prompt, menu, and
status_facts; the minibuffer is deliberately exempt on both sides
(one global core instance, matching the producer's surviving
last_minibuffer baseline). GPU test opens search + menu + status
via the real wire arms, applies a snapshot, and asserts all three
clear, the intercept gate releases, and the popup pixels vanish --
hand-bitten by disabling the three clears (fix and test share
main.rs).
Finding 2: the round-2 reset broke the diagnostic-count freeze.
last_status was both the peer emission baseline and the
stale-store freeze source, so a snapshot between didChange and
fresh diagnostics re-shipped StatusFacts with zeroed counts. The
freeze source now lives apart: frozen_diag_counts advances on every
fresh count, is read when the store is stale, and survives
on_buffer_snapshot_sent -- which keeps killing the emission
baseline to force the re-send. Acceptance renders (1,1), marks the
store stale, applies the reset, and asserts the re-sent StatusFacts
still carries (1,1); runtime bite vs pre-fix semantic_render.rs
fails exactly as predicted (Some((0,0)) vs Some((1,1))).
Framing revision 7; acceptance items 31-32; the protocol doc's
snapshot-reset paragraph now lists the full frontend drop set and
names the count freeze as daemon knowledge, not peer state.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Finding 1: a BufferSnapshot wipes the frontend's buffer-scoped
render state (spans, decorations, adornments, minimap summary,
completion popup), but the producer's per-buffer emission baselines
survived the switch -- on an unchanged A -> B -> A round trip,
last_summary[A]'s key still matched and the daemon emitted nothing,
so the frontend never regained A's themed minimap (or A's
StatusFacts: the band kept B's name) until an edit, republish, or
theme mutation happened to move the key.
The fix is the general contract, not a minimap special case:
SemanticRenderState::on_buffer_snapshot_sent(buffer_id) kills every
buffer-scoped baseline for that buffer (spans + style gate,
decorations, adornments, summary, status, search/menu prompts,
completion popup), called wherever the daemon writes a snapshot --
the active-buffer-follow path and the F29 upgrade broadcast; the
attach bootstrap constructs its session state fresh. Deliberately
surviving: the bufferless ThemeFacts pair, the global minibuffer
baseline, the per-frontend gutter mode, the revision-keyed diag
line cache, and other buffers' baselines.
Evidence: a producer round-trip acceptance test (themed summary and
StatusFacts return at the SAME generation; identical payload), a
real-daemon wire test driving A -> B -> A via dispatched keys
(runtime bite: times out against pre-fix daemon.rs), a Rust unit
pinning the reset's scope, and a GPU test where the re-shipped
summary restores the first visit's pixels exactly (frontend half --
no GPU code change, coverage only). The semantic_render.rs bite is
compile-fail (the hook is absent pre-fix), disclosed as weaker.
The protocol doc's composition section now states the snapshot
reset contract on both sides of the wire.
Finding 2: the acceptance-suite manifest header now lists the true
item split (1-19, 24-26, 28-29 here; 20-23, 27, 30 in the GPU
suite). Framing revision 6 folds the round; acceptance items 28-30.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Finding 1: accepting a FileStyleSummary drops the GPU minimap vertex
cache -- theme recolors and diagnostic republishes arrive at an
unchanged generation, and the cache keys only on (generation, dims,
scroll), so stale strokes survived until an edit/resize/scroll. The
daemon payload-suppresses identical summaries, so the invalidation
is precise. GPU test drives two same-generation summaries;
hand-bitten by reverting the single invalidation line (script-bite
is vacuous here: fix and test share main.rs).
Finding 2: lua_to_style propagates every Table::get error -- the
lookups run __index, so a raising metatable previously parsed as an
all-default style and the merge SUCCEEDED, committing valid siblings
against the Q#TH6 all-or-nothing contract. Boolean fields keep Lua
truthiness by design (mlua bool), so only raising lookups fail the
transaction. Acceptance reproduces the reviewer's trap shape;
runtime bite vs pre-fix mod.rs.
Finding 3: SemanticRenderState::for_peer records the negotiated
version; below v16 no ThemeFacts is produced and no ui.diag.* face
folds into the FileStyleSummary marks -- the summary is an ungated
pre-v16 channel, and a v15 peer must not get face-derived minimap
colors while its other severity surfaces stay unthemed. The summary
cache key zeroes its face-epoch component for such peers. Acceptance
drives v15/v16 producers side by side; compile-fail bite disclosed
(the test needs for_peer, absent pre-fix).
Finding 4: framing revision 5 weakens the canonical-severity claim
to what is true -- the daemon-RESOLVED color is canonical, while the
GPU's built-in squiggle/sign/counter defaults are historical bright
RGBs that differ from the minimap's converted Indexed marks, a
pre-existing divergence kept because unset faces must render
byte-identically to before.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
- §1: `main` @ 7d1b6d6; new arc bullet covering the grammars (every
LSP-configured language now has one), the extension → filetype →
filename → shebang detection chain and its user-extensible Lua
surfaces, the dockerfile/cmake LSP configs, and the substrate
(`highlights_query: &[&str]` base-first composition; fail-closed
`#is?`/`#is-not? local` handling in `compute_highlight_spans`).
Condensed the merged compile-mode/editops/auto-pairing blocks to
substrate essentials. Fixed the stale "#114 is the USER'S lane" note:
themes (Arc 4) is the user's active `theme-faces` lane.
- §3: folded the confirmed flakes (m6_1_pty, composition_overhead) into
the rerun-isolated note, plus the single-invocation sweep caveat.
- §6: new deferrals — locals-query processing, multi-language
injections (blocks notebooks / markdown code / HTML-in-JS), modeline
detection, JSON/YAML, byte-accurate cursor; a full Jupyter setup is a
real arc, not a one-shot.
Docs only. #118 is approved, merge pending. git diff --check clean.
Syntax.rs theme-handle line refs re-verified; the new shell grammar
introduces no ui-named capture, so the Q#TH2 reservation holds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Arc 4 stage 1 (docs/theme-faces-framing.md, revision 4). Faces are
theme entries under the reserved ui/ui.* namespace -- zero new Lua
API. Theme::face() resolves with the dotted-prefix walk but never
falls back to default_style; each face applies owns-surface within
its stage-1 component mask, identical on both frontends.
Substrate: two monotonic theme mutation counters (syntax/face) with
transactional set/merge/clear/default (parse before locking, commit
all-or-nothing, bump from the prior value); the StyleGate and the
minimap summary key on the counters -- fixing the pre-existing bug
where a mid-session pmacs.theme.set never re-shipped StyleSpans --
with the summary gaining payload-equality suppression that still
advances its key on computation.
Wire: InstanceMessage::ThemeFacts appended after CompletionPopup
(postcard discriminants are ordinal; a byte pin guards placement),
PROTOCOL_VERSION 15 -> 16, daemon-gated >= 16, one authoritative
table per attachment (None-seeded baselines), TUI silent-drop arm.
Grid: paint_frame resolves ui.modeline / ui.statusline /
ui.minibuffer(.candidate) / ui.gutter / ui.selection faces;
SearchView and DiagnosticView take the theme handle through the real
attachment paths (EditorCore injection, install_diag threading); the
canonical severity color resolves ui.diag.* with the Default ->
built-in policy that keeps the minimap presence encoding sound.
GPU: exact-name face table applied per draw with the Q#TH5 Default
mapping (plain text / window bg, reverse swap), local/peer wash
split, candidate-dropdown glyph site, and the status-band
shaping-cache invalidation without which a diag-face recolor with
constant counts kept stale counter colors.
Tests: 18-test acceptance suite (grid, wire, daemon gate, atomicity,
monotonicity, late join), 7 GPU headless tests incl. decoded vertex
colors, units for the face walk / transactional commits / producer
caches; protocol pins for v16 + the CompletionPopup byte pin.
Bites vs 3cbb9de (scripts/bite): semantic_render.rs (8 runtime test
failures), editor.rs (5 runtime), daemon.rs (v15 gate, runtime);
lua_bindings/mod.rs, pmacs-gpu/main.rs, search.rs, diag.rs, and
highlight.rs bite as compile failures (weaker evidence, disclosed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
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
Finding-by-finding (framing revision 13; bites via scripts/bite
against fe04aa4):
1. attach_style_overlay validates the handle. A handle's translator
follows edits to ITS buffer only, so attaching it to another
buffer created a render view showing spans nobody maintains —
rejected now, with the message naming the recorded owner and
pointing at add_style_overlay for the target buffer. A disposed
handle's translator is gone, so re-attachment resurrected
rendering with frozen coordinates — the disposed state is shared
across handle clones (FromLua clones) via Arc<AtomicBool> and
attachment after dispose() fails, pointing at add_style_overlay
for a fresh handle. Bite: r7f1 pins cross-buffer rejection,
same-buffer acceptance, dispose-then-attach rejection, and both
message shapes.
2. dispose() detaches the translator through the always-registered
SharedRegistry; only the window cleanup rides the optional
SharedCore. Pre-fix all cleanup lived inside the SharedCore
branch, so an install-only/headless host got success with the
translator left attached — paying on every edit for the buffer's
lifetime. Registry-only unit asserts the buffer's view count
returns to baseline (and stays there on double dispose); the
acceptance-crate twin r7f2 builds the same install-only host and
bites via the mod.rs swap (the in-crate unit vanishes with it).
Gates: fmt; clippy workspace all-targets; lib 1535; crdt lib 1709;
compile acceptance 65; crdt acceptance 3; m4 101; m6.4 15; m6.5 11;
m6.8 8; GPU 59; workspace sweep 2526/0; git diff --check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Finding-by-finding (framing revision 12; bites via scripts/bite
against a49adc2):
1. Render-view attachment is idempotent and split-complete. Overlays
expose overlay_identity (the span store's allocation address);
Window::ensure_overlay attaches a store-backed render view AT
MOST once per window — pre-fix every switch into the buffer
blindly pushed another copy onto EVERY matching window, so
passive panes accumulated duplicates, each cloning all spans and
rescanning the buffer per frame. A same-buffer split copies
clonable overlays to the new pane via clone_for_split (splits
fire no switch hook and started with an empty overlay list — the
new compilation pane rendered unstyled). Bites: the acceptance
test asserts both panes styled with exactly one attachment
IMMEDIATELY post-split (before any switch could heal the pane
through the attach-to-all path — the first draft asserted only
after bouncing and was vacuous against the split fix), then
re-asserts after three bounce cycles; fails against pre-fix
editor_core.rs (split half) and pre-fix mod.rs (accumulation
half) independently. Units pin ensure-once and split-copy/no-copy.
2. The translator ignores pure no-op edits (buffers deliberately
broadcast empty inserts/deletes for callers that count calls):
pre-fix each interior no-op split the containing span into two
adjacent fragments — unbounded list growth for repeated no-ops at
distinct positions, and a no-op at a UTF-8 continuation byte
minted a mid-codepoint span boundary. Units now cover genuine
EditOp::Insert (the round-5 "insertion" unit only replaced) and
no-ops at five interior positions including the continuation
byte; the Lua twin (r6f2) bites via the overlay.rs swap — as a
compile failure, since that file also carries the round-6
identity machinery (weaker evidence, per the bite script's
caveat; the in-crate unit pins the behavior directly).
3. StyleOverlayHandleLua retains the buffer and translator ViewId
and exposes idempotent dispose(): detaches the buffer-attached
translator (later edits stop paying for it) and removes every
window render view over the store. Documented lifetime contract:
one handle per buffer incarnation (the compile/REPL discipline)
needs no disposal — the buffer's death frees it; repeated
creation on a long-lived buffer must dispose retired handles.
Bite: r6f3 (translate → dispose → edit must NOT move the span,
render views gone, double-dispose safe) fails against pre-fix
mod.rs.
Gates: fmt; clippy workspace all-targets; lib 1534; crdt lib 1708;
compile acceptance 63; crdt acceptance 3; m4 101; m6.4 15; m6.5 11;
m6.8 8; GPU 59; workspace sweep 2523/0 (one m8-class flake, clean on
rerun); git diff --check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Finding-by-finding (framing revision 11; bites via scripts/bite
against 6793edc):
1. Style-span coordinate translation belongs to the BUFFER. A new
BufferStyleSpanTranslator is attached by
pmacs.buffer.add_style_overlay and sees every edit exactly once —
bypass writes, undo/redo, remote CRDT ops — independent of window
count or visibility; the window-attached BufferStyleOverlay
copies are render-only (on_edit removed). Pre-fix each attached
view translated the shared store: start_run's explicit attach
duplicated the after-switch hook's (switch_buffer fires it
synchronously), so the normal path shifted later spans TWICE per
byte-delta rewrite, splits multiplied further, and a hidden
buffer shifted ZERO times. The redundant attach is removed;
correctness no longer depends on attachment discipline. Bites:
per-cell rendered assertions active (red a, blue bc, CR, red é →
é red, b/c blue) and hidden (run finishes with the buffer in no
window; switch back renders true colors); three direct units pin
exactly-once with extra render views attached.
2. Translation preserves the untouched fragments of a partially
overlapped span: left of the replaced range keeps its styling,
right of it shifts by the length delta, only the rewritten bytes
lose theirs (the writer styles what it writes; inserted bytes
inherit nothing). Pre-fix any overlap dropped the WHOLE span —
red abc, SGR reset, CR, X left bc unstyled; zero translation
painted the default X red instead. Bite: exact (glyph, fg) cells
X=default, b/c=red — any_styled_cell cannot see either failure.
3. The per-CR/BS/erase-line whole-prefix scan is gone:
slot.line_start is tracked — advanced at every \n (append helper
+ the mid-line newline branch), read O(1) by the rewind paths,
reset on run start/resync/raw marker appends. Measured on 2 MB of
output + 3000 CR updates (release): 2.52s pre-fix → 0.67s
post-fix (remainder is fixture-bound; pre-fix cost grows with
buffer size). No correctness bite is possible for a pure perf fix
— the committed test pins the tracked value's behavior across
multi-line appends, batch-boundary CR, repeated CR, erase-line,
and recovery paths, and passes on both implementations by design.
Gates: fmt; clippy workspace all-targets; lib 1531; crdt lib 1705;
compile acceptance 60; crdt acceptance 3; m4 101; m6.4 15; m6.5 11;
m6.8 8; GPU 59; workspace sweep 2517/0; git diff --check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Finding-by-finding (framing revision 10; bites via scripts/bite
against b5bbce8):
1. CR rewrites are COLUMN-counted and newline-segmented, not
byte-counted. Each newline-free segment of a text event consumes
one existing codepoint per incoming codepoint (codepoints
approximate columns; double-width and combining characters count
as one — the documented stance), and LF is not an overwrite
column: a newline arriving mid-line drops the cursor to a fresh
line and the stale remainder survives in place (terminal
semantics). Pre-fix, abcdef\rX\n wrote "X\n" over "ab" — splitting
the line and leaving "cdef" as a ghost line the parser saw again
at EOF — and abc\ré ate two ASCII columns because é is two bytes.
Round-3's UTF-8 invariant holds per-segment: every edit's range
ends sit on codepoint boundaries, so the rope is valid after each
step and byte-native CRDT edits never reject. Bites: single-batch
(shorter rewrite, multibyte-over-ASCII, CRLF), split-feed with the
é split across batches, and a CRDT twin covering the segmented
multi-edit replication.
2. Alternate-screen exits resynchronize the effective style. The
parser now tracks the style the consumer LAST RECEIVED
(emitted_style; outside alt-screen it always equals
current_style). An ordinary ?1049l exit emits the resync SetStyle
whenever suppressed SGR changes drifted the two apart, and
finish() balances against emitted_style rather than
current_style — a suppressed SGR reset inside the alt screen left
the internal style default, so the old comparison saw nothing to
balance while the consumer stayed red. Consumer-mirror units for
both drift directions plus the no-drift no-event case; Lua twin
(r4f2) bites via the ansi.rs swap.
Gates: fmt; clippy workspace all-targets; lib 1528; crdt lib 1702;
compile acceptance 56; crdt acceptance 3; m4 101; m6.4 15; m6.8 8;
GPU 59; workspace sweep 2510/0; git diff --check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
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
Finding-by-finding (framing revision 8; bites via scripts/bite):
1. Rule validation is a stable, total snapshot: validated scalar
fields are copied into per-run plain tables via raw reads
(rawget; metatable-provided fields deliberately not honored), so
post-run mutation of the user's rule objects cannot alter an
in-flight run and a hostile __index is a counted skip, not an
error thrown through the pump mid-batch. The container traversal
is itself pcall-protected; traversal-raise semantics are
Lua-flavor-dependent (5.2+ ipairs consults __index, LuaJIT reads
raw) and the test pins both flavors.
2. Capture indexes must be FINITE (floor(math.huge) == math.huge, so
integrality alone passed it); math.huge is now a counted
malformed entry.
3. Shell-command never touches the rule table: no spurious
compile-rule warnings on M-!, and no rule-container state can
block a run that performs no parsing.
4. AnsiParser::finish() (and parser:finish()) now fully resets the
parser — in-flight CSI/OSC/escape state and alt-screen
suppression included — so a post-finish feed parses a fresh
stream. Three direct unit tests in ansi.rs plus a Lua-driven twin
in the acceptance suite (the twin exists because a scripts/bite
file swap replaces the in-file units along with the fix).
5. Comment corrections: fractional capture indexes read a distinct
absent key (not a neighboring capture); the group-coercion
comment describes truthiness, not false; the AnsiParserLua
rustdoc lists finish().
Bites: r2f1 (both shapes), r2f2, r2f3 fail against pre-fix
compile.lua; r2f4 fails against pre-fix ansi.rs. Gates: fmt, clippy
workspace all-targets, lib 1525, crdt lib 1699, compile acceptance
50, crdt acceptance 1, m4 101, GPU 59, workspace sweep 2501/0 (one
flaky-suite rerun per the standing m8 rule), git diff --check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Finding-by-finding (framing revision 7; every fix bite-verified via
scripts/bite against the pre-fix tree):
1. Stored coordinates must be finite integers, and both cursor walks
are movement-bounded — they clamp at EOF, and the column walk
clamps at the target row's EOL instead of marching onto later
rows. An astronomical %d+ capture can no longer hang the editor.
2. The grep panel gains the same immediate buffer.after-edit
recovery trigger as the compile slots: M-x buffer.undo after a
COMPLETED search is marked synchronously.
3. The rustc arrow rule uses the framing's ([^:]+) spelling — paths
with spaces capture whole.
4. All pattern captures are collected (index 4+ reads the real
capture, not nil-as-column-0); capture indexes must be positive
integers; a rule naming a column its match didn't produce rejects
the match.
5. emit_text_raw is module-local — a user global could shadow the
helper the terminal-event path depends on, and its error consumed
the terminal event before pump cleanup/forget ran.
6. stdin/group spec fields reject wrong Lua types as hard errors;
group is matched as a raw Value because mlua's bool conversion
applies Lua truthiness ("true" would silently coerce).
7. resync also nils the public line_start_byte — total pre-marker
anchor invalidation includes the byte anchor.
8. The inherited cwd resolves through
pmacs.instance.identity().working_directory; the header always
names a real path and relative error files get an explicit base.
9. New AnsiParser::finish() + parser:finish() (additions #5): a
truncated multibyte sequence at process EOF surfaces as U+FFFD
before the exit marker instead of vanishing.
10. The built-in default rules are a private deep copy — in-place
mutations of the public table no longer survive the "using
built-in defaults" degradation.
Eleven new tests (r1f1a/b–r1f10); bites: 9 fail against pre-fix
compile.lua, r1f2 against pre-fix default.lua, r1f6 against pre-fix
lua_bindings/mod.rs — all clean assertion failures. Gates: fmt,
clippy workspace all-targets, lib 1522, crdt lib 1696, compile
acceptance 45, crdt acceptance 1, m4 101, GPU 59, workspace sweep
2493/0, git diff --check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
cargo fmt over the new files; doc-markdown backticks; is_ok_and in
the recompile counter wait; m4_6's M-g n/p pin updated to the Q#CM5
takeover contract (error.next/error.previous with the diag commands
as the dispatchers' fallback — the test's no-attachment status
behavior is unchanged). Handoff §1: main @ 0efb5cd, compile-mode
branch in flight at framing revision 6, themes named as the
standing runner-up.
Gate results on this machine (laptop, basedpyright live): fmt,
clippy --workspace --all-targets, lib 1522, crdt lib 1696,
compile_mode_acceptance 34, compile_mode_crdt_acceptance 1,
m4_acceptance 101 (no skip), PMACS_REQUIRE_GPU gpu 59, workspace
sweep 2482/0, git diff --check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Revision 6 after five pre-branch review rounds. Scope: M-x-style
compile.run with a streaming *compilation* buffer and error-regex
navigation; grep-mode as an upgrade of project.search; M-! shell
command. Shared machinery: streaming intercept-read-only output
buffer with a revision-guarded external-edit recovery protocol, and
a claim-based error-source dispatcher with diagnostics fallback.
Supervisor additions: stdin="null" and group=true pipe-mode spec
options — the latter a full lifecycle policy (process_group(0)
spawn, group-directed signals, liveness-probed TERM-to-KILL reap
ledger integrated into shutdown, bounded in-drain enforcement,
poll-based cancellable readers). Binding additions: buf:revision(),
jump_back buffer.after-switch parity.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
A trap-guarded one-file swap over read-only git show: run the named
tests against an older version of one file and exit 0 iff they FAIL
there (the fix bites); exit 1 flags a vacuous fix. The file is
restored on every exit path including interrupts; the ./ pathspec
prefix makes it work from any directory inside the repo. Never
touches git stash — the stash namespace is repo-global, shared
across worktrees and with humans (a #111-round mishap popped a
PR-#17-era user stash; hence the tool).
Dogfooded three ways against the merged #111 history: the round-1
fix commit bites (exit 0), the same tests against HEAD read as
vacuous (exit 1), and a docs/-relative invocation restores cleanly.
Handoff: section 1 snapshot (editops #111 merged, worktree folded),
section 5 lessons (repo-global stash; worktree-per-lane for
parallel work).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vF4gQVozBWi38y1SJiGfQ
Finding 1: codepoint recognition is now full UTF-8 scalar validation
(shared second-byte constraint table: overlongs, surrogates, and
beyond-U+10FFFF all fail), and transpose validates the scalar AT the
cursor trailing-bytes-included — a valid lead with non-continuation
trailing bytes fails closed, as does a length-consistent overlong or
out-of-range span behind the cursor. Zap's single-codepoint check
uses the same validator as defense-in-depth (minibuffer contents
arrive as Rust-side UTF-8; the buffer-facing checks are the
load-bearing ones).
Finding 2: capitalize is per-word across the span — Emacs
capitalize-region parity, verified against Emacs 30.2 ("hello WORLD"
-> "Hello World", "9abc a9bc" -> "9abc A9bc"); the one remaining
deviation is named and pinned: `_` is a word constituent in this
pack's ASCII class, so "foo_bar" -> "Foo_bar" versus Emacs's
"Foo_Bar".
Finding 3: an unexpected error caught by the trim-on-save outer
pcall is no longer discarded — it reports on the status line AND the
*errors* buffer via pmacs.error (the autosave sweep convention),
both pcall'd, still never vetoing the save.
All three fixes bite-verified: the five new/updated acceptance cases
fail against the pre-fix editops.lua (72 total now). Framing at
revision 6.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vF4gQVozBWi38y1SJiGfQ
Snapshot: main @ 4174f3e, Arc 2 closed with #110 (framing revision 6,
two pre-branch + three PR rounds). Next action is the compile-mode vs
themes decision discussion — frame the tradeoff, don't pick. Pairing
deferrals consolidated into §6; new substrate noted (buf:path,
pmacs.lsp.buffer_language, PMACS_FAKE_LSP_CHANGE_SINK,
TestDaemon::spawn_with_config).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
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
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
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
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
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
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
RET now runs edit.newline-and-indent (builtin/runtime/indent.lua):
one insert/replace of "\n" plus the current line's leading whitespace,
copied verbatim and clipped at the split point (Q#AI3). Region RET
stays a single Replace (CUA type-over, one undo step, one CRDT op);
the selection clears after every successful edit (Q#AI4). Fix-up is
snapshot-guarded against context-switching intercepts and repairs the
cursor by right-gravity translation through the effective edit
(Q#AI5). buffer.newline remains the plain-newline escape hatch.
GPU (Q#AI1/Q#AI6): plain Enter is no longer optimistic-eligible --
its classifier arm's premise (byte-identical to a self-insert) died
with the new binding. Enter round-trips like the TUI, which also
makes global and buffer-local RET rebindings (buffer-list visit)
reachable from the GPU frontend.
Substrate fixes that RET would otherwise ship on top of:
- Q#AI8 search staleness: notify_buffer_edit now marks matches stale
and right-gravity-translates the live session origin, matching
apply_active_edit; SearchStore::step and search_match_summary fail
closed while stale (a live search un-sticks on the next pattern
keystroke, since set() clears staleness).
- Q#AI9 empty selections: insert_char reports success and the
no-region arm of insert_char_over_region clears a lingering anchor
only on Ok -- ordinary typing no longer type-overs its own previous
keystroke after S-Left at BOF, and a rejected insert mutates no
state.
Acceptance: tests/auto_indent_acceptance.rs (20 dispatch-driven
cases), tests/auto_indent_crdt_acceptance.rs (pending optimistic
input then round-tripped Enter converges on the source replica),
flipped GPU classifier test, and lib tests for the store, core, and
dispatch seams.
Framing: docs/auto-indent-framing.md (five review rounds).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ATiKMwJ4864d82D39EvsU6
docs/agent-handoff.md is the continuity bridge between development
machines: current state snapshot, the framing->review->gates->PR
working method, substrate invariants (command boundaries,
effective-edit returns, outbound_position, state-dir isolation),
machine-specific caveats to re-verify per box, ops lessons, and the
consolidated named-deferral backlog. Carries its own update protocol
so each machine hands it forward.
CLAUDE.md (auto-loaded by Claude Code on any clone) points there and
pins the always-true constraints: workflow, gate suite, shared-checkout
discipline, commit/PR conventions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtRqijWecEzTjPt1B4Nrt5
Q#CT1-6: pure Lua on the Arc 2 substrate (effective-edit mutators,
command boundaries, after-edit delivery). M-; = comment-line-style
toggle (named deviation from comment-dwim); pmacs.comment.strings
public table; min-indent insertion, skip-blank, mixed-span comments;
single buf:replace per toggle = one undo step / one CRDT op / one
effective-edit verification.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Q#KR1-11: Lua ring on a per-frontend command-boundary substrate.
Eight-row boundary table (keybound/self-insert/unbound/GPU-optimistic/
pointer/paste/menu/M-x-interactive); stable ring-entry ids so shared-
ring interleaving cannot corrupt per-frontend chains or yank sessions;
sessions carry {buffer,start,end,entry_id,text} snapshots with slice
verification. Three shipped bugs in scope (Q#KR10): semantic-path Paste
drop (GPU Ctrl-V is a no-op today), missing after-edit for M-x/menu/
paste edits, and paste trusting the client-supplied payload id instead
of the dispatcher's authenticated source. Q#KR11: frontend.detached
hook + command_history detach cleanup. Incorporates three review
rounds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses the PR #100 review round 4.
- MEDIUM stale skip-cache entry after a slot transfer. adopt() set
owner[hash] = new buffer but left the previous owner's `written` entry
pointing at the same hash, breaking the invariant
`written[id] => owner[hash] == id`. Repro: A and B are duplicate buffers
on one path; A owns the slot; B adopts (recover-file); B is killed
without saving, which frees the slot and deletes the file. A is still
dirty, but its stale written[A] = (hash, revA) makes the next sweep call
it "unchanged since its last copy" --- silently unprotected until its
next edit. adopt() now drops any other buffer's written entry for that
hash. Verified the new test fails without the fix (sweep writes 0).
- MEDIUM autosave write failures were swallowed. write_private can fail
(ENOSPC, a permission change, a clobbered state dir), but the tick and
before-quit paths did `pcall(sweep)` and dropped the error. For a
data-protection feature that is the worst failure mode: the user keeps
working, believing edits are captured, while nothing is written. Both
paths now go through a reporting wrapper --- status line "autosave
FAILED: ... --- your work is NOT being protected" on every failing sweep,
each distinct fault logged once via pmacs.error. The quit path reports
too (a failure there means the quit is about to discard work that was
never written anywhere) and still never vetoes.
Tests (autosave_acceptance now 29):
adopting_clears_the_previous_owners_stale_skip_cache,
a_failing_sweep_is_reported_not_swallowed (plants a regular file where
autosave/ must be a directory, standing in for ENOSPC).
Gates: fmt + workspace clippy clean; lib 1499; crdt 1670; autosave 29 + 8
units; desktop 11; persistence 5; m7_8 5; GPU 58; git diff --check clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>