Add the strict pmacs.statusline provider registry, deterministic
borrow-released per-window evaluation, context-scoped failure latches,
and a pure built-in LSP provider.
Preserve the legacy TUI modeline while composing faced custom runs,
and append authoritative complete StatuslineSegments replacements for
semantic frontends. Expand dynamic ThemeFacts, reset producer/frontend
baselines symmetrically, and gate all provider work off protocol v18.
Teach the GPU to atomically validate, resolve, shape, clip, and cache
custom modeline runs without displacing the protected status suffix.
Document the public Lua lifecycle, wire ownership, snapshot semantics,
and the fully gated Arc 4 stage-3 delivery state.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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
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
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
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>
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>
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>