Handoff §1 gains the arc entry: the window-parameter substrate, the two
production `Layout::compute` callers, the recursive minima, hiding as a
durable transition, per-window input gating, the per-frontend jump
origins, and the shared initial-target load seam. §5 gains four durable
lessons, three of them the same class:
- a guard with no production caller passes every direct-call test;
- a geometric readout (`at_bottom`) is not a state predicate;
- a PTY does not translate LF to CRLF, so text equality over clipped
output is vacuous;
- widening an ambient resolver into a scoped one can make a total
function partial — which is what took CI red on all four Test jobs.
The roadmap position, the arc's named deferrals, and DAP's unblocking
are recorded too.
active-work closes the lane, refreshes the canonical base to `e745068`,
and keeps Stage 2's named obligations plus the two gating facts found on
the way (the sweep needs an isolated XDG_CONFIG_HOME; compile_mode
acceptance is load-sensitive, verified pre-existing).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
F1 was a real bug pinned by my own committed test. `end` in
ChunkSource::MathBox is EXCLUSIVE, so source position `end` is the first byte
AFTER the span — but the arm claimed it for the box's left edge, and the test
asserted that wrong value while calling the byte "interior". Consequences it
would have caused once overlays land: a search match starting just after a
span washes the whole box it does not intersect, violating Q#MS11; a peer
caret after the span draws at the box's left edge; caret geometry jumps
backwards. The same class existed in projected_to_source for a line-FINAL box,
where `within` clamps to the run length and the arm returned `start`
unconditionally, so a click past end-of-line landed on the span start. Both
committed hit tests put a chunk after the box, so that edge was never
exercised; there is now a test with the box last.
F2: parse_scripts peeked for the next marker without skipping whitespace, so
`x^2 _i` built a NESTED script — drawing the subscript displaced right by the
superscript's width — and `x^2 ^3` parsed where TeX errors, contradicting the
module's own "whitespace is insignificant" rule.
F3: layout is now fallible. A character the math font cannot draw used to
yield zero metrics and still emit a Glyph item, rendering tofu at zero advance
over its neighbour. Q#MS8's rule is "failure is always show the source", and
the draw pass needs a refusal signal — changed now, before that pass consumes
the API.
F4: the fraction gap was a hardcoded `thickness * 2.0` while the MATH table's
FractionNumeratorGapMin / FractionDenominatorGapMin went unread. Reading them
moved the flagship \frac{a}{b} from 0.732 to 0.867 and the fallback boundary
from depth 3 to depth 5. The round-2 review's hand-arithmetic estimate of
~0.85 was right; my 0.732 was inflated by the guess. The depth-SEARCHING test
absorbed the change without edits, which is the property it was written for.
F5: TeX's \epsilon and \phi are the lunate/symbol forms (U+03F5, U+03D5), not
U+03B5/U+03C6. Their italic mappings had to land with the seed change, since
both sit outside math_italic's U+03B1..03C9 run and would otherwise render
upright beside italic neighbours.
F7: the line-box budget derivation moved out of the test into
`line_box_budget`, so the draw pass and the acceptance test cannot compute
different splits while both stay green.
F8: the live-code clippy items are cleared. The 25 that remain are all
dead-code awaiting the draw pass.
189 pmacs-gpu tests pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR #155 review round 2, self-review of the round-2 commit.
The round-2 change labelled "minor" — resolving both arms of
pmacs.window.buffer() through the acting frontend for uniformity — made
the NO-ARGUMENT arm fallible. `acting_frontend` follows the interactive
origin, which can name a frontend that has no registered view: a bare
`dispatch_key` from an unattached peer does exactly that. `selected_window`
then raises "acting frontend has no layout" instead of answering.
Nothing surfaced that error, because the runtime callers do not pcall it.
killring, syntax, autosave, pair, indent and comment all read
pmacs.window.buffer() on ordinary edits, so the raise silently dropped
the operation: kill_ring_acceptance went 30/30 to 25/5, with
frontend_detached_drops_per_frontend_state reporting only "B has kill
state". main is 30/30, and reverting this one file restored it.
The no-arg arm is back on ambient active_buffer_id() and now documents
why that is deliberate rather than an oversight: dispatch sets
active_frontend to the acting frontend before running a command, so the
two agree on every real path, while only the ambient resolver has the
fallback that makes it total. The explicit-window arm keeps its Q#BP11
layout validation, which is what the arc actually needed.
acc19c pins it through the real path — a buffer.after-edit subscriber
reading pmacs.window.buffer() during a viewless peer's dispatch_key —
rather than by calling the binding directly. Bite-verified:
scripts/bite bbe4152 src/lua_bindings/mod.rs --test
bottom_panel_stage1_acceptance -- acc19c goes red with the exact
"acting frontend has no layout" traceback.
The ledger also records two gating facts found on the way: the workspace
sweep must run with an isolated XDG_CONFIG_HOME, because the real user
init.lua installs a local package and the losing race leaks a status
message into painted-frame comparisons; and a latent pre-existing main
bug in the buffer CRDT undo path, which is not this branch's and whose
proptest seed is deliberately not committed here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012j4omtTMn9v1UfmHQb9ap6
MathBox/MathItem composition for the Q#MS2 subset: characters advance a pen,
scripts shift by the MATH table's superscript/subscript amounts at script
scale, and \frac stacks its operands around a rule at the math axis. Inline
\frac sets its operands one style down, which is TeX's rule and also what the
parent framing's Tier 3 specifies — and it is load-bearing for Q#MS10, since
full-size operands would not fit the line at all.
The height budget is now measured rather than assumed, and the round-2 review
was right to insist on that. Two things were wrong.
First, my own test derived the budget from the MATH font's metrics. Q#MS10
says the budget is the LINE BOX, whose baseline the CODE font places —
JetBrains Mono ascends 16.32 px and descends 4.80 px at 16 px inside the 22 px
line, against Latin Modern Math's 12.90/3.10. Using the wrong font made a
plain \frac{a}{b} score 0.485 and appear to fall below the floor, which would
have meant the flagship case never rendering.
Second, with the budget derived correctly, B6 holds — \frac{a}{b} scales to
0.732 — but rev 3's guessed fallback case does not. A doubly-nested fraction
scores 0.744 and still renders; the floor is not tripped until depth 3, at
0.580. Round 2 predicted precisely this surprise-pass. Worth keeping: depth 2
scores HIGHER than depth 1, because the binding constraint flips from descent
to ascent as nesting grows asymmetrically, so "deeper is always tighter" is
false.
The test therefore SEARCHES for the tripping depth instead of hardcoding it,
and fails if no depth trips the floor at all — which would mean the fallback
arm is unreachable and the floor is dead code. Acceptance 12 records the
measured table.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Font, licence, and the metrics half of Tier 3.
The bundled font is Latin Modern Math under the GUST Font License, added as
fonts/GUST-FONT-LICENSE.txt — deliberately a separate file from fonts/OFL.txt,
which covers JetBrains Mono only. GFL is LPPL-derived, not the SIL OFL; the
framing's F6 corrected that error and this is the discharge. At 733,736 bytes
the font is now the largest embedded asset in the repository.
ttf-parser is declared with default-features = false and only
"opentype-layout". Verified differentially: the ttf-parser feature set from
`cargo tree -e features` is byte-identical with and without this dependency
line, so the declaration widens nothing and forces no rebuild of the font
chain.
That check also corrected acceptance 17, which asserted `std` would be absent.
It is not — fontdb already enables it via `std = ["ttf-parser/std"]`, upstream
and independent of us. As written the criterion would have failed a correct
implementation, so it is now stated as the differential property that actually
matters.
MathConstants reads only what the Q#MS2 subset needs — axis height, script
scale percent, the two script shifts, and fraction rule thickness. Reading
more would be speculative: constants for deferred constructs have no consumer
to validate them, which is the Q#LX5 discipline applied to metrics. A font
with no MATH table is a typed error rather than plausible-looking zeros, so a
bundled-font regression cannot be silent (Q#MS7).
math_italic implements TeX's convention as the framing's table states it:
ASCII letters and lowercase Greek italic, uppercase Greek upright, digits and
operators unchanged, with U+210E for `h` because the 1D4xx run has a hole
there and arithmetic would land on a reserved codepoint.
Five tests, all against the real embedded bytes rather than fixtures, since B5
is the bet that would sink Tier 3 if false. One goes beyond the framing: every
italic mapping must resolve to a glyph the bundled font actually has, because
a mapping that produced tofu would be worse than the roman fallback it
replaces.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four findings, one of them a real gap.
R2-2 is the substantive one. Rev 2's math-italic fix stopped at ASCII, which
reintroduced the same defect one symbol over: the parser resolves \alpha to
U+03B1, so $\alpha x$ would have drawn an upright alpha beside an italic x —
mixed styles inside a single expression, with the Greek seed map being the
slice's second flagship case after x^2. The mapping now follows TeX's actual
convention and states it as a table: ASCII letters and lowercase Greek in math
italic (U+1D434 and U+1D6FC runs, with the U+210E hole for h), uppercase Greek
deliberately upright, digits and operators unchanged. Acceptance 13 gains
$\alpha$ and $\Gamma$ cases so the uniformity is pinned rather than assumed.
R2-1 was self-inflicted: rev 2's Q#MS3 rewrite added a corrected copy of the
tree-sitter paragraph without deleting the original, leaving the stale rev-1
text immediately below the new one. Removed.
R2-4 sharpens Q#MS11 from "covers" to "intersects". For selections the
distinction is vacuous, but a search match can genuinely half-overlap a span —
searching "2$ af" in "before $x^2$ after" matches from inside the span to
outside it — and the whole-rectangle rule must apply there too, since the box
has no interior byte map to place a partial wash against.
R2-3 moves the follow-up section after prior art; the sections now run 0
through 9 in order.
Acceptance 12 also absorbs a round-2 non-finding: the reviewer's arithmetic
puts \frac{a}{b} near 0.85 and suggests \frac{x^2}{y} also clears the 0.6
floor, so the fallback case has to be computed against the real font rather
than guessed — otherwise the fallback half of the test surprise-passes by
rendering. The criterion now says so and asks for the computed scale to be
pinned.
Framing only; no implementation, no runtime code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nine findings, two of them decisions the implementation could not have
proceeded without, one a compliance error. Two rev-1 claims were wrong rather
than imprecise and are withdrawn where they occur.
F1, the fraction height budget, was hidden inside "a box that would exceed the
line" without saying width or height. Lines cannot grow — metrics are uniform
at 16 px over a 22 px line — and a textstyle fraction does not fit. Q#MS10 now
picks fit-to-line scaling with a 0.6x floor and source fallback below it,
records why overdraw and line growth were rejected, and states the honest
consequence: v0 shrinks nested math uniformly where TeX would use style
levels. B6 makes the floor falsifiable against the flagship case.
F3 was a genuine contradiction: acceptance 10 demanded source-width boxes
while Q#MS4 implied layout-chosen width, and that gap IS the caret-toggle
reflow question. Resolved for layout-chosen, because pinning to source width
leaves every fraction sitting in a large permanent gap, whereas reflow is
transient and tied to a deliberate caret move. Acceptance 10 is rewritten to
bound it rather than forbid it.
F2 withdrew the zero-glyph strut: a RichChunk's only width is its text, so the
mechanism is the SourceTab precedent — projected spacer text, width quantized
to whole advances. B1 is restated against the mechanism that exists.
F5 would have bitten real prose immediately: the rev-1 rule renders "prices
are $5 and $6 today" as math. Pandoc currency guards adopted, spans confined
to one line, both in acceptance.
F6 is factual and a licence matter. Latin Modern Math is the GUST Font
License, not OFL, and is 733,736 bytes rather than ~200 KB — verified against
a local TeX Live copy. The bundled licence file must say GFL, and this becomes
the largest embedded asset in the repo. The parent framing carries the same
error; correcting it is recorded as a follow-up rather than done here, since
the parent is already merged.
F4 names the new shaping-invalidation edge as the #120 trap class, including
that suppression must read the effective caret or flap during optimistic
typing. F7 puts math italic in scope, without which the flagship case renders
a roman x. F8 clarifies that glyph IDs are still resolved internally and that
drawing must pin Attrs to the math family. F9 closes the smaller gaps: the
detection hook wording, an overlay rule for selections and washes, degradation
of deferred display syntax, and folding Char/Symbol.
Framing only; no implementation, no runtime code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`TerminalViewStatus.scroll_offset` is the retained rows between the
VIEWPORT and the live tail, so it necessarily tracks viewport height: an
assertion that it survives a panel height change unchanged is either
vacuous or wrong, and it went red once under a loaded sweep for exactly
that reason. Q#BP7's invariant is that the ANCHOR is frozen, so acc32
and acc33 now compare the first visible row's text across the change,
and additionally pin the follow behavior that distinguishes them: a
shrink never re-arms follow, growth reaching the tail does, and growth
with a frozen selection does not.
Both also wait for the child's last line before sampling, so neither
races further output.
Also records the round in docs/active-work.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The obvious next unit was Tier 2 (parser) plus Tier 3 (layout). This framing
rejects that for the arc's own reason: the substrate lane's Q#LX5 refused to
land the parser ahead of layout because MathNode's shape is only validated by
a consumer, and that argument does not stop at Tier 2. MathBox is only
validated once a renderer consumes it, so Tiers 2+3 with no Tier 4 reproduce
the same objection one layer up.
The unit is therefore thin and vertical: characters, sub/superscript and
fractions, detected, parsed, laid out against a real OpenType MATH table, and
drawn — with breadth deferred to follow-on work against an API that has been
exercised rather than only designed.
The scout also found a load-bearing error in the parent's integration table.
It places math_parse.rs and math_layout.rs in the core crate, which cannot
work: pmacs-gpu depends only on pmacs-protocol and never on pmacs, so a
core-crate parser is unreachable from the frontend that renders it, and
ttf-parser reaches only pmacs-gpu. Both also contradict the parent's own
"instance never learns a pixel" contract and its "pure frontend
responsibility in v0" protocol section. Q#MS1 moves both modules into
pmacs-gpu.
Two decisions buy the slice most of its simplicity. Rendering is suppressed
whenever the caret is inside a span, which makes editing-shows-source and
moving-away-renders mutually exclusive states and removes the
caret-inside-math problem rather than approximating it. And layout positions
characters rather than glyph IDs, which is sufficient for this subset and
explicitly does not generalise: glyph variants are what stretchy fences and
big operators need, and both are deferred.
Acceptance puts every claim that something reaches the screen on real pixels
through the existing headless render seam, because a layout engine wired to
nothing would pass any weaker test.
Framing only; no implementation, no runtime code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review finding, verified: rev 2's "no new build cost" was true only under
an unstated condition. ttf-parser re-exports `math` behind
`#[cfg(feature = "opentype-layout")]`, and it is compiled today only
because fontdb requests that feature — with `default-features = false` and
a set that is NOT ttf-parser's own default (fontdb's adds no-std-float and
omits std). A plain `ttf-parser = "0.25"` therefore unions std in and
forces a one-time rebuild of ttf-parser, fontdb, cosmic-text and glyphon.
Record the zero-rebuild spelling, `default-features = false, features =
["opentype-layout"]`, in Tier 3 §A and in the component table, so the
Tier 3 implementer declares it deliberately rather than tripping over it.
The C1 row points at the detail rather than repeating it.
Also note in the header that every anchor was re-checked at f07b75b. The
scout pin stays at ddaa80d because that is when the scouting happened;
#153 landed between the two and is test-only, moving no anchor cited here.
Framing only; no implementation, no runtime code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the lane to docs/active-work.md: branch, base, what Stage 1
implemented, the verification run, and the two known local-only test
caveats (the parallel-load GPU flake and compile_mode_acceptance's
single-thread requirement).
The durable handoff snapshot stays untouched until the PR merges, per
its own update protocol.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Revision 1 was written against protocol v18, before LaTeX Stage 1 (#144),
web grammars (#146), folding Stages 1-2 (#142/#149) and the GPU initial
target (#148) landed. Revision 2 changes no design decision; it corrects
the ground truth those merges invalidated and records the staging decision
the sibling substrate framing already took. A new section 0 lists every
correction so a reader who knows revision 1 can read it alone.
Two corrections change implementation choices rather than line numbers:
- The MATH-table dependency story was wrong in both directions. Revision 1
said a crate must be added and that "neither is in the tree today";
ttf-parser 0.25.1 already reaches pmacs-gpu non-optionally through
fontdb -> cosmic-text -> glyphon, the same fontdb the frontend already
calls. And the choice is not "one of ttf-parser or read-fonts": only
ttf-parser exposes the MATH table, supplying exactly the constants Tier
3 names. read-fonts 0.37.0 is present but has none, so selecting it
would be a dead end.
- Tier 2's staging was already decided elsewhere and this note did not say
so. The sibling framing's Q#LX5 puts the parser beside its Tier 3
consumer, never ahead of it, because MathNode's shape is only validated
by a layout consumer. That makes Tier 2 not independently shippable,
which is worth stating explicitly: it is pure and conflict-free, so
landing it alone while other lanes hold the render path is exactly the
tempting move Q#LX5 refused.
Tier 1 is materially de-risked: the LaTeX grammar already exposes
math_environment and math_delimiter, and the in-repo query overlay this
tier proposed already exists and captures both, so the mechanism is proven
rather than speculative. The guessed node name (math_expression) is
corrected to the grammar's own. Markdown still needs the overlay
treatment.
Tier 4 gains a contention note. Revision 1 described the GPU render path
as though math were its only claimant; folding Stage 3 and the
bottom-panel arc's Stage 2 now converge on it, so whichever lands second
re-scouts against the first.
Framing only; no implementation, no runtime code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the approved framing for the bottom-panel arc: a buffer displayed
in a fixed-height window pinned to the bottom of the frame, targeted by
policy rather than by stealing the selected window, plus the missing
display-buffer/window-parameter concept underneath it.
Revision 4 follows three review rounds, an integration review, and a
landed-state audit against GPU initial target (#148, protocol v20) and
folding Stage 2 (#149).
Amended before branching by the pre-implementation dependency
verification recorded in section 0.6:
- the folding dependency is cleared and re-verified against canonical
main at ddaa80d (nothing in flight, folding Stage 2 acceptance 48/48
green, every borrowed anchor reproducing, and folding's only window.rs
edit confined to one 22-line hunk that leaves the layout functions
pre-folding code);
- R5-B1: Layout::compute has TWO production callers, not one. The
second, the peer-presence overlay pass in src/overlay_paint.rs, builds
its own text-area rect from active_layout() and never routes through
window_placements, so the planned compute(area, fixed) signature change
would otherwise leave every peer cursor painted at its no-panel row.
Corrected in section 1.1 and Q#BP2, pinned by acceptance 1, and the
fixed map is now specified to come from one shared helper rather than
being assembled per call site.
Stage 1 (window placement + TUI side windows) changes no wire shape.
Move the lane from active-work.md into its Closed section, retire the
protocol v20 / main-hash references to LANDED form in agent-handoff.md, and
record both review-round lessons (failure-socket containment, upgrade-gated
replica publication) in the ops-lessons ledger.
Integrate folding Stage 2 and its landed-state documentation with the
protocol-v20 GPU initial-target branch. Preserve per-session fold projection
selection in the target bootstrap transaction and retain v19 compatibility
coverage after the later protocol bump.
Shut down bootstrap sockets on every dispatcher-side failure and reject
frontend events whose session state was never installed. This prevents a
lingering failed client from reaching absent render/size state.
Track target-side CRDT upgrades independently from load/create status so a
deduplicated hidden buffer is published to every existing grid replica. Add
real-daemon regressions for both failure containment and replica publication.
Post-merge housekeeping owed from #149, kept as its own docs-only PR per
the #138-#140 / #147 convention. No runtime code.
- active-work.md: base snapshot and the recovery check bump 47581f4 ->
6ed4fe9. The Stage 2 lane is retired and replaced by a folding lane that
records both stages as merged with nothing in flight, and states Stage 3
(GPU) has no branch and no framing yet — carrying its named obligations
(GPU collapse at TUI parity, caret/hit-test fold-awareness, the
BufferSnapshot fold-mirror clear, CRDT-origin unfold, and flipping
FrontendView.fold_projection true for semantic frontends) as that
framing's starting point. "Closed since the last snapshot" gains #149
and #147.
- agent-handoff.md §1: main @ 6ed4fe9, the "Last updated" line and section
date, the Stage 2 bullet flipped from IMPLEMENTED/PR-OPEN to LANDED with
the Stage 3 obligations attached, and the roadmap entry (remaining arcs
now read "6 folding Stage 3").
Both stages' design points are recorded as traps Stage 3 inherits rather
than as history: the merged-hidden-component unit, per-window/per-target
map instances, per-frontend projection, position-not-row normalization,
and the post-intercept edit site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
PR #149 review round 5 flagged this PR as stale: it still claimed `main`
@ `c49a8c7`, Stage 2 framing "rev 2, under review; no implementation, no
PR", while `main` is `47581f4` and Stage 2 is implemented and open.
- Base snapshot and the recovery check bump `c49a8c7` -> `47581f4`.
- The folding Stage 2 lane becomes IMPLEMENTED / PR #149 OPEN: framing
rev 4 approved, the `VisibleLineMap` spine, the base-moved merge (and
why it was merged rather than rebased), and the five review rounds'
design-changing findings — each of which is a trap Stage 3 inherits.
- `main`'s ledger had gone unrefreshed through four merges, not one, so
"Closed since the last snapshot" now also records web grammars HTML +
CSS (#146) and LaTeX Stage 1 (#144) with its inline-math framing
(#145), including their durable lessons.
- agent-handoff §1: `main` @ `47581f4`, the "Last updated" line, the
Stage 2 substrate bullet, and the roadmap entry.
Rebased onto `47581f4` so it stays one documentation-only commit
directly off canonical main.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
Post-merge housekeeping owed from #142, kept as its own docs PR (no
runtime code).
- agent-handoff.md §1: bump main to c49a8c7, add the folding Stage 1
substrate bullet (store/View, structural source, C-c @ surface,
command-path unfold, FoldState production; no protocol bump), refresh
the "Last updated" line and the roadmap Arc 6 entry, and note Stage 2
is in framing on folding-tui (the visible-line-map reframe).
- active-work.md: retire the Stage 1 folding lane (PR #142 was OPEN),
add a "Closed since the last snapshot" entry for #142, open the
Stage 2 (grid/daemon collapse) framing lane on folding-tui, and
refresh the canonical base snapshot to c49a8c7.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
Keep foreign BufferSnapshot publications out of existing semantic GPU
sessions while retaining grid-replica coherence. Treat dead peer writes as
peer-local failures, restore active-frontend cleanup, deterministic probe
readiness, GPU logging, shared tilde expansion, and accurate docs.
Add focused publication and cleanup coverage and record the two-window
Wayland/Vulkan smoke plus the complete post-review gate results.
`main` moved from c49a8c7 (folding Stage 1, #142) to 47581f4 (web
grammars, #146) while Stage 2 was in framing and implementation. The
text merge is clean, but it is NOT semantically clean: #146 added three
new `Viewport { .. }` literals to `src/highlight.rs`'s unit tests, and
Stage 2 gives `Viewport` a `folds` field. Merged alone, `cargo test
--lib` fails to compile — so the carry-over is resolved here rather
than left for CI to discover.
Merged (not rebased) so the four framing revisions the review rounds
cite by SHA (59410c0, e221f13, 8160d66, 4222ffa) stay reachable.
Full gate suite re-run on the merged tree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
Update the framing, durable handoff, and active-work ledger after integrating
current canonical main and completing the required gates and real GPU smoke.
Add protocol-v20 semantic bootstrap and readiness result framing so
`pmacs --gpu FILE` opens the requested path before the GPU window becomes
ready. Keep target identity scoped to the authenticated frontend, preserve
legacy/no-target attach behavior, and publish fresh buffers coherently to
existing replicas.
Carry Unix path bytes and launcher cwd through the root broker, resolve paths
lexically in the daemon, reuse or create buffers without ambient-view state,
and preserve the managed daemon lifecycle from #141. Add focused parser,
wire, lifecycle, hook, isolation, and real-connector acceptance coverage.
Route command-time visible-line maps through each operation's target
window, while retaining the acting frontend as the projection-policy
owner. Model nested and crossing folds as merged hidden components so
row and byte clamps always resolve to one actually visible head.
Also key projection on the negotiated render selection and correct the
unmerged status of the separate Stage 1 housekeeping PR.
Round 2's three findings + two nits, all verified against c49a8c7:
- F1 (major): fold-aware motion must be frontend-projection scoped. Shared
EditorCore::move_up/down/page_* would make a simultaneous unfolded GPU
session skip source lines it still displays (a grid + a semantic session
can attach to one buffer, daemon.rs:876). Add a per-FrontendView
`fold_projection_active` flag (editor_core.rs:240, set at attach / cleared
at detach); gate ALL command-time visible-line reckoning (motion, paging,
wheel, click, auto-scroll) on it. Render-time clamps are already
grid-path-only. New Q#FD21 + simultaneous TUI+semantic acceptance.
- F2 (major): render maps must be per WINDOW, not per frame — paint_frame
and the presence pass iterate windows with distinct buffer_ids
(editor.rs:2922, overlay_paint.rs:124). Specify one map per rendered
nonterminal window (keyed on window buffer_id + TextView); peer presence
uses the recipient window's map. New split-of-different-buffers acceptance.
- F3 (moderate): hidden positions need COLUMN projection, not only row
clamping. Add `visible_position_of(pos)` -> outermost fold's range.start
(end of visible head line, Stage 1's point-move target) for local/peer
carets and selection endpoints; hidden interiors still drop. New
hidden-cursor-column-differs acceptance.
- Nits: fix the Viewport<'a> typo; state build cost honestly as O(folds)
with a byte->line lookup per fold (B4).
PR #147 (the #142 housekeeping) confirmed clean by the reviewer, no findings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
Review found that adding ("attribute", fg(3)) for HTML/CSS also colours the
@attribute capture three already-bundled grammars emit — rust (attribute_item),
lua (<const>), yaml (directives) — which were previously unpainted. Verified on
a Rust buffer: #[derive(Debug)] now paints uniformly yellow (fg 3), an
improvement over unpainted and the distinct-attribute convention most editors
follow.
Name this retro-paint as intended in the framing (Q#WEB4, rev 4) and pin it with
rust_attribute_repaints_via_shared_attribute_capture so it is a chosen effect,
not incidental. @tag is unaffected (HTML/CSS only).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 1's five findings + two rulings, all verified against c49a8c7:
- F1 (major): nested folds. `head_of`(innermost) could clamp onto a
still-hidden inner head. Replace with `visible_head_of` (outermost
visible head); `view_top` clamps BACKWARD to the head, not forward
past the fold; relative numbers anchor on the clamped visible cursor.
- F2 (major): the consumer census was incomplete. Add the full §2.2
table — local selection (editor.rs:3241), peer presence
(overlay_paint.rs:159, after paint_frame), mode-line indicator
(editor.rs:3803), style/search/completion overlays — and make TUI
peer-presence fold behavior explicit scope.
- F3 (major): line numbers default Off => gutter_w==0 => no sign cell.
Make the fold glyph conditional: off => ellipsis only; on => sign
cell with diagnostic priority. Dedicated column (unconditional) named
as a deferred layout change.
- F4 (major): a frame-pinned map can't serve command-time motion, and
Viewport is Copy. Reframe as one derivation primitive with per-phase
short-lived instances (render via Option<&VisibleLineMap> on a
lifetime-bearing Viewport, preserving Copy; after-frame direct;
command-time fresh); home usable from EditorCore.
- F5 (moderate): key the Lua-path widening on InteractiveCommandOrigin
(editor.rs:53), hook the common run_buffer_edit (not only
run_managed_edit) so bypass_intercept edits don't escape, require the
target to be the invoking frontend's active-window buffer, and
explicitly DEFER undo/redo unfold.
Rulings: Q#FD17 include (normalize a hidden cursor to the visible head
before stepping); #142 housekeeping stays a separate docs PR.
Acceptance expanded to pin nested-fold/shared-cursor, local selection,
peer presence, an ordinary overlay across a fold, completion anchoring,
the scroll indicator, and both gutter-off/gutter-on fold-marker cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
Reframe Arc 6 folding Stage 2 in detail off canonical main @ c49a8c7
(Stage 1 / #142 merged), per the parent framing's §8/§14. Continues the
Q#FD scheme from Q#FD12.
Stage 2 makes the daemon grid renderer fold-aware: collapse hidden lines,
head-line ellipsis + gutter fold glyph, fold-aware line numbers
(visible-line relative distance), diagnostic-sign clamp-to-head, caret
clamp, visible-line viewport/scroll accounting, and the interactive-Lua
unfold widening (yank/query-replace/comment). No wire schema or protocol
change — FoldState production (Stage 1) is untouched; the GPU path is
Stage 3.
Scout findings that shaped the framing:
- The TUI has NO non-identity source-line->display-row map today; the
identity `view_top + row` is baked into ~7 sites. Folding is the first
such map, so Stage 2's spine is one shared per-frame visible-line map
(Q#FD12) that the render loop and every view_top-arithmetic site
consult; collapse lives in TextView::render, not the diff shell.
- Correcting the parent's premise: yank + query-replace are
apply_active_edit callers (local), not Lua-mutator callers; only
comment-toggle/yank-pop take the Lua path (shared with the
remote/optimistic-CRDT apply that stays deferred to Stage 3). The
widening hooks the local funnels only (Q#FD19).
One open scope fork flagged for the user: Q#FD17 (fold-aware vertical
line-motion vs render-time caret clamp only).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
Advance the active lane to Revision 2 and record closure of all four
non-structural framing findings. Keep implementation gated on explicit user
approval.
Pin launcher-owned tilde expansion, require same-buffer dedup hooks, fail
closed when hooks kill the target, and document stderr feedback during the
pre-window bootstrap wait. Record the observed protocol-version echo and
non-Unicode argv panic.
Add tree-sitter HTML and CSS grammars for .html/.htm/.xhtml and .css
highlighting, and light up HTML's <script>->javascript / <style>->css
injections (the north-star injection consumer). Both crates export their query
constants over the tree-sitter-language shim (no overlay, ABI-fine). The only
reconciliation is two new capture entries (tag, attribute) in highlight.rs.
Framing approved through three review rounds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Advance the volatile ledger to the current canonical base and record the
portable Revision 1 framing checkpoint, scope, recovery command, and approval
boundary.
Round 2 correctly found the Finding-2/3 fixes were unpinned (reverting
them left the suite green). Both are now bite-verified:
- **Kill-path purge (Finding 2).** Replaced the direct
`forget_buffer(id)` unit test with
`killing_a_buffer_through_the_real_path_purges_its_fold_store`, which
drives `pmacs.buffer.remove` — the production route through
`after_buffer_removed` — and asserts the store is gone via the dead id
(BufferIds never recycle). Mirrors config_registry's real-kill-path
test. Bite-verified: reverting the `after_buffer_removed` fold branch
turns it red.
- **close-all point move (Finding 3).** Added
`close_all_command_moves_point_to_enclosing_head`, which invokes the
`fold.close-all` command with the point inside the second of two
top-level fns and asserts the cursor landed on that fn's head-line
content end (and both folds exist). Bite-verified: reverting close_all's
`maybe_move_point` loop turns it red.
- Ledger: `docs/active-work.md` folding lane now records PR #142 OPEN +
the two landed review rounds (was "opens once the gate suite is green").
Correction to the round-1 gate report: the acceptance suite is **21**
tests (round 1 was 20, not 24 — a tally slip), green under default and
`--features crdt`. Full gate suite otherwise green (fmt, clippy
--workspace --all-targets, git diff --check).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
The parent-arc framing was authored on the desktop but never tracked, so
references to it — from the merged latex-grammar lane framing and its Cargo.toml
comment (#144) — dangled on a fresh clone. Commit it as its own docs change and
drop the now-stale "untracked, desktop-only" caveat from the lane framing.
Frames the full four-tier inline-math renderer (detection -> parse -> layout ->
GPU render). Stage 1 (LaTeX grammar) shipped as #144; the math parser (Tier 2),
layout (Tier 3), GPU render (Tier 4), and (math_environment) @math injection are
deferred, tracked in docs/latex-grammar-math-substrate-framing.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Advance the durable snapshot to merge 63fbc66, record the green CI rerun, and
remove the completed GPU invocation lane from the volatile active-work ledger.
The Cargo.toml comment and the lane framing both cited docs/inline-math-framing.md,
which is an untracked, desktop-only doc — the path dangles on a fresh clone.
Point the Cargo.toml comment at the committed lane framing instead, and note the
parent's untracked status in the framing header (committing it as its own docs
PR, or listing it in the handoff's machine-local inventory, remains a tracked
follow-up).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Advance the framing to Revision 6 and record the strengthened non-CRDT,
Ctrl-C, strict-operand, probe-throttling, and PID-cleanup contracts. Update the
durable and volatile checkpoints to implementation commit 154cb9f.
Advance the active and durable checkpoints to 69825d0 and clarify that every
spawned managed daemon enters the named reaper before connection or handshake
work can fail.
Advance the framing to Revision 5 and record the reviewed lifecycle, CLI, and
acceptance contracts. Update the durable and volatile handoffs with checkpoint
82355ca and the completed verification matrix.
Carve the frontend-agnostic, conflict-free substrate out of the parent
inline-math arc: Stage 1 bundles a LaTeX/TeX grammar for .tex/.latex/.sty/.cls
highlighting; the math parser (Tier 2) and GPU render (Tier 4) are deferred to
the inline-math arc. Framing approved through three review rounds; provenance
diff of the chosen grammar crate against upstream latex-lsp is discharged
(grammar.js/scanner.c byte-identical).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Update the durable handoff and volatile active-work ledger for open PR #141, including the implementation checkpoint, accepted architecture, verification record, visible Wayland/Vulkan smoke, and cross-machine recovery commands.
Add the root --gpu broker, strict GPU entry points, daemon connect-or-start orchestration, process-group isolation, bounded retry, named child reaping, and a deterministic headless lifecycle probe. Cover the complete launch matrix with real subprocess acceptance, make root Cargo runs unambiguous, and document the coherent build and one-command workflow.
Q#FD4 settled: the user chose Emacs hideshow parity, so Stage 1 ships the
`C-c @` prefix set (`C-c <letter>` is fully taken by the LSP surface; the
hs-minor-mode prefix collides with nothing). §6/§9 now list the five
bindings; §0 records the rev 4 -> rev 5 approval note; §14 records the
rebase onto canonical `main` @ 96d0bae at implementation start. Bet B1
accepted as framed. active-work.md folding lane flipped to APPROVED /
Stage 1 implementing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
One major, three minors, and a nit from the third review, all fixed:
R3-1 (major, derived head line): rev 3's head-selection ascend was not a
no-op for brace languages — rustfmt wraps long signatures
(fn foo( / a: u32, / ) -> bool {) and puts { on its own line under where
clauses, so block.start_line > parent.start_line, the ascend fired, and
the fold hid the wrapped signature: the R2-5 defect class one level up.
Replaced by a derived head line — the interior comes from the body node
alone (closer-aware tail unchanged) and the head is the line immediately
above the first hidden line (B.start_line - 1 for an introduced
delimiter-less body, B.start_line otherwise). Emacs hideshow / LSP
foldingRange parity: the fold hides the body, nothing else. The
introducer<->body association survives for matching and close-all only.
Acceptance 1 gains wrapped-signature cases in both grammar shapes.
R3-2: "innermost-first" on a shared head line made the outer fold
unreachable via fold.toggle (close inner, reopen inner, forever) and
allowed zero-visible-change presses. Replaced by state-aware ordering:
close acts on the innermost open fold, open on the outermost closed
fold, toggle cycles org-TAB-style (close inward-out, then open all).
Acceptance 9 updated.
R3-3: Stage 1's "command path" is dispatch_key self-insert/delete only;
interactive Lua commands (yank, query-replace, comment-toggle) mutate
through the Lua mutator path and classify programmatic, so their edits
land inside a fold without unfolding. Stated as the intended Stage 1
line; widening the classifier to interactive Lua command contexts is a
named Stage 2 obligation beside Stage 3's CRDT-origin unfold.
R3-4: the data API's normalization of an arbitrary range is now defined
(head = line containing start; hidden = full lines strictly after it
through the line containing end, exclusive of an end at a line start).
Nit: stored-range containment pinned start-exclusive/end-inclusive with
the matching View boundary bias, so typing at the end of a head line
neither unfolds nor lands hidden; acceptance 6 asserts it.
Also: Sec 14 records that canonical main has advanced past the cac4961
base (docs + tab-width #137, no Stage 1 overlap; rebase at
implementation start), and the active-work folding lane is brought
current (head was stale at rev 1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five majors + four minors from the second review, all fixed:
R2-1 (head line on indentation grammars): tree-sitter-python's block
starts on the first statement line, so the old heuristic made a body line
the fold head and left def foo(): headless. Added a head-selection ascend
rule (block-like node -> ascend to the introducer parent while
parent.start_line < block.start_line), a no-op for brace languages.
Acceptance 1 now tests both Rust and Python.
R2-2 (Q#FD5 cannot live in the View): View::on_edit(&Buffer,&Edit) and
Edit carry no source frontend and no point-inside signal (only crdt_op),
verified at overlay.rs:248 / rope.rs. The store View now does translate +
drop only; the interactive unfold is a pre-edit step at the dispatch layer
that holds the authenticated frontend and its point.
R2-3 (CRDT typing misclassified): the classifier is the authenticated
source frontend's point, not the transport — a GPU user's CRDT-op insert
inside a fold is interactive. Stage 1 implements the command path;
CRDT-origin unfold is a named Stage 3 obligation.
R2-4 (#120 stale-mirror trap): revert drops the store + emits
BufferSnapshot + resets the baseline, so the empty store is suppressed as
'initial empty' and the GPU keeps stale folds unless its snapshot arm
clears the fold mirror. Pinned as a Stage 3 obligation and in acceptance 7.
R2-5 (line-aligned tail hid non-member text): } else { / }, [deps]) —
now the closing-delimiter line stays visible (closer-aware tail);
delimiter-less nodes still hide through the last body line. Decided, not
bet.
Minors: unfold is plural (nested); shared head lines toggle
innermost-first; Q#FD9's reason corrected to the >=1-hidden-line rule (not
bounds); and the Stage 2/3 sketch now names fold-aware LineNumbers,
visible-line viewport/scroll accounting, and hidden-line sign/presence
clamp-or-drop.
Seven findings from the first review, all fixed:
F1 (architectural): the grid TUI is daemon-rendered and never receives
FoldState (advertises semantic_render:false), so its fold collapse is
instance-side work in the daemon grid renderer reading the store directly
— the vterm Stage 2 shape. FoldState on the wire serves only semantic
(GPU) sessions. Staging reworked: Stage 2 = grid/daemon rendering, Stage
3 = wire-fed GPU.
F2: stored range pinned to the line-aligned hidden interior (head line
visible, closing-delimiter line hidden); one normalized form everywhere,
resolving the R1 5/7 contradiction.
F3: the store's edit-translation is the instance-side buffer-attached View
(BufferStyleSpanTranslator pattern, overlay.rs:235), which sees every real
edit — not the frontend-side translate_byte_range. Split the two resets:
per-session producer baseline vs per-buffer store lifecycle on content
replacement.
F4: stale-tree fold creation refuses with a message when
ParseViewHandle::current() is None or pending_edit_count() > 0.
F5: multi-frontend point + edit-vs-fold pinned — invoking point moves to
the head; interactive-point-inside edits unfold, programmatic/remote edits
translate; the no-cursor-inside invariant is creation-time-only in Stage 1.
F6: Lua data API takes an explicit buffer (no ambient resolution, per
#127) with full range validation, which is also what makes terminals
never fold.
F7: FoldState follows the authoritative-empty discipline; open-all emits
exactly one empty frame; the flipped pin test keeps asserting
BlockAdornments is never emitted.
Minors: source lines not display rows; close-all folds top-level only
(hs-hide-all parity); an injected-layer (markdown fence) acceptance added;
and an explicit note that FoldState needs no protocol bump.
The ledger inherited from #135's merge still listed Vterm Stage 3 as an
open 'never merge without authorization' implementation lane; it is merged
(main @ cac4961), so per the update protocol it moves to Closed and Arc 5's
terminal stage is marked complete. Adds the folding framing lane (branch
folding, off cac4961, framing-only) and points the canonical base at
cac4961 / protocol v19.
Draft framing for the folding arc, committed to the `folding` branch for
review before any implementation. Branch is cut from canonical main @
cac4961 (post Vterm Stage 3 #135).
The load-bearing finding: the bundled tree-sitter grammars ship no fold
query and no folds.scm — they export HIGHLIGHTS/INJECTIONS/LOCALS/TAGS
only. The roadmap's "tree-sitter fold ranges" premise is therefore not
free, so the fold source is a real decision (Q#FD1). The draft recommends
structural node folding (fold the nearest enclosing block-like node
spanning >= 2 rows), which reuses the existing parse trees for every
grammar and injection layer with zero per-language authoring; indentation
folding (grammarless fallback) and curated per-language queries (quality
pass) are deferred.
FoldState already exists in the protocol, declared but unproduced, with a
test pinning that it is never emitted; no frontend consumes it; gutter
markers are frontend-derived like the diagnostic sign bars, so no new wire
type is needed. Staged like vterm: Stage 1 engine (instance-side fold
model + structural source + Lua commands + FoldState production, headless),
Stage 2 TUI collapse+gutter, Stage 3 GPU at parity.
Numbered decisions Q#FD1-9, three falsifiable bets, named deferrals, and a
Stage 1 acceptance list. Awaiting review rounds; bindings (Q#FD4) and the
block-kind heuristic (Bet B1) are the two calls flagged for the user.
Record the approved managed-launch contract, daemon lifecycle and process-group rules, strict GPU CLI behavior, headless acceptance seam, and the complete acceptance matrix before implementation.
Advance canonical state to protocol v19, close the Vterm lane and roadmap arc, preserve the cross-PR integration lesson, mark PR #91 landed, and point machine-local housekeeping at the durable policy.
vterm-stage3-framing (superseded; content carried on vterm-gpu) and
tab-width-parity (merged via #137) were deleted with authorization —
worktree + local ref + githubsucks ref, origin tracking pruned. The
-framing branches for each are kept. Retires the now-dangling
superseded-lane recovery entry.
Integrates canonical `main` @ 2625ec7 after PR #137 (tab-width parity)
merged. The agreed order was #137 first, this lane second: #137 was
approved and FROZEN at 5b23e11, and "frozen" is incompatible with
"rebase onto the resulting main" — landing it second would have broken
its freeze and voided its approval.
Integrated by MERGING main into the branch rather than rebasing, matching
repo precedent (Merge canonical main into vterm-tui, ... into modeline
detection). A rebase would have force-pushed away the review anchors on
the two completed review rounds of #135.
Main had also moved past this lane's base by #133/#134/#136, so the
integration surface was wider than the #135/#137 overlap: src/
semantic_render.rs was a fourth overlapping code file. It auto-merged, as
did pmacs-protocol/src/lib.rs. The single code conflict was the
pmacs_protocol import list in pmacs-gpu/src/main.rs — TAB_STOP_COLUMNS
against the terminal types — resolved as a union.
The feared semantic collision did not occur, and this is verified rather
than assumed: terminal cell geometry still uses the monospace advance and
never TAB_STOP_COLUMNS. pmacs-gpu/src/terminal.rs references neither the
constant nor display_width, and terminal_cell_viewport / terminal_run_rect
/ hit_test_cell derive from mono_advance() and code_line_height() alone.
That separation is correct by construction: a terminal's columns come
from the child, while tab expansion is a document projection concern.
Doc conflicts resolved toward landed state: the tab-width lane moves to
"Closed since the last snapshot", the #135/#137 coordination section is
kept as a resolved worked example, and the Arc 5 lines in the roadmap and
handoff now read "implemented and in review". While resolving, restored a
clause main had dropped from the handoff's injection-follow-ups list
("literals, doc-comment code);"), keeping main's strikethrough-and-SHIPPED
convention for the modeline entry.
Post-integration gates, from a clean tree: cargo fmt --check; strict
workspace clippy; pmacs-protocol 17; cargo test --lib 1,768; --features
crdt 1,944 (3 ignored each); vterm Stage 1 9/10, Stage 2 4/4, Stage 3
5/7, statusline 7/8, tab-width 2/2 (default/CRDT); M4 121 passed (3
ignored, 1 filtered); required GPU 139; workspace sweep 2,946 passed
across 84 suites (19 ignored), one invocation; git diff --check clean.
PR #137 (tab-width-parity) is approved and frozen at 5b23e11. Neither
lane copies from or merges the other; whichever lands second rebases
onto the canonical resulting main and reruns the complete gate suite.
The overlap is pmacs-gpu/src/main.rs, pmacs-protocol/src/lib.rs,
Cargo.lock, and the two ledger docs. The lock and docs are mechanical;
the two source files are not — both PRs edit the GPU renderer's
measurement path and widen the protocol crate's export surface in the
same region, so a conflict-free apply is not evidence of a correct
merge.
One real defect, three cleanups, and a named deferral.
A daemon disconnect in terminal mode hid the disconnect notice. The
Disconnected arm set the placeholder text but never left terminal mode,
where the document code layer is not prepared at all and the terminal glyph
layer keeps painting its last frame — so the user was left looking at a
frozen, live-looking terminal that silently ignored input. GPU auto-reconnect
is a named deferral, so that state persisted until relaunch. State::
on_daemon_disconnected now leaves terminal mode, forces a repaint even when
the notice text is byte-identical, and requests a redraw.
The fix and its test share a file, so scripts/bite's file granularity cannot
bite it; the equivalent was done by hand. Neutralizing only the
exit_terminal_mode() call makes the test fail on the "must leave terminal
mode" assertion; restoring it makes it pass.
sync_semantic_terminal_layout no longer clones the whole visible cell grid to
read one size. It ran every dispatcher tick for any semantic frontend with a
declared terminal; TerminalManager::screen_size reads the value from the
borrowed projection instead.
Inbound terminal events now require a negotiated v19 session. The outbound
TerminalFrame was gated twice while TerminalResize/TerminalPointer relied on
the frontend's send gate alone. A pre-v19 peer cannot construct those
variants, so this only refuses a hand-rolled client — and the a32 forgery
tests already prove such an event reaches nothing but the sender's own
authenticated active view — but the asymmetry was not deliberate.
A terminal-mode press that misses the grid no longer arms a drag, so a later
in-grid motion cannot send a Drag with no preceding Down. Daemon-side impact
was nil; the state is now honest. A release still always ends the drag.
The roadmap and handoff Arc 5 lines still said Stage 3 was framed and
awaiting approval, contradicting this PR's own ledger. Both corrected.
Named deferral: terminal wheel gestures discard scroll magnitude. One winit
wheel event becomes one gesture regardless of the lines it accumulated, while
the document path scrolls by lines. Closing it means either N gestures
(chattier) or a magnitude field on the pointer event — a protocol change.
Neither belongs in this stage.
Gates: fmt; strict workspace clippy; 1,758 default + 1,934 CRDT library
tests; Stage 1 9/10, Stage 2 4/4, Stage 3 5/7, statusline 7/8
(default/CRDT); M4 120; required GPU 129; workspace sweep 2,923 across 83
suites; diff check clean.
Share one fixed eight-column tab-stop contract across core and GPU renderers. Consolidate byte-to-display-column accounting, expand GPU code tabs with source provenance, align caret/hit/decoration geometry, and refresh minimap projection on edits.
Five findings, all addressed. One was a real defect; one prediction did not
reproduce and is documented as such rather than papered over.
Hover no longer claims durable terminal control (finding 2, the real one).
apply_terminal_gesture claimed the controller before dispatching, including
for Move, which does nothing. A semantic frontend reports motion at pixel
rate, so sweeping the mouse across a passive split's terminal took durable
control, and the next layout sync resized the shared PTY to that background
view's geometry — precisely the theft the controller rule exists to prevent.
Bare motion no longer claims; every deliberate gesture still does.
scripts/bite HEAD src/editor.rs on the new test is a clean behavioral bite.
The terminal-mode presence-sweep skip is removed (finding 1), but the
predicted failure did NOT reproduce. The review reasoned that skipping the
sweep freezes last_broadcast at the abandoned document position. It does
not: the buffer-follow clears the terminal declaration when it ships the
snapshot, so terminal_active is false on the tick a window first shows a
terminal, and the declaration cannot arrive until a later tick — the
frontend learns the buffer id from that very snapshot. One truthful sweep
always lands first. The real-daemon two-frontend test written to catch the
freeze passes against the pre-fix tree; the bite is vacuous and the test is
labelled a regression guard, not fix evidence. The skip goes anyway: it was
load-bearing on tick ordering and bought nothing, and removing it makes
"presence follows the frontend" structural.
Terminal motion is deduplicated by cell (finding 3). Sub-cell motion
resolved to the same coordinate and still crossed the wire, where every
event is a daemon-side gesture. Press and release re-arm the memo so the
first drag after a press still reports. Its unit test cannot bite — the
seam did not exist pre-fix — and says so.
Declarations record only once sent (finding 4).
terminal_declaration_if_changed is now a pure query;
note_terminal_declaration_sent records. A failed write is retried instead of
suppressed as already-declared. The existing a35 test caught the contract
change and now pins both halves.
Unchanged frames skip revalidation (finding 5). The complete-payload
comparison runs before validate; only validated frames are ever stored, so a
frame equal to the baseline has already passed. The chrome tail is factored
into terminal_chrome so both exits emit it identically.
Gates: fmt; strict workspace clippy; 1,757 default + 1,933 CRDT library
tests; Stage 1 9/10, Stage 2 4/4, Stage 3 5/7, statusline 7/8
(default/CRDT); M4 120; required GPU 128; workspace sweep 2,921 across 83
suites; diff check clean.
Mark PR #134 as shipped in the durable handoff, framing, and side-quest
backlog. Remove the completed volatile lane and advance the canonical recovery
anchor to the merge commit.
Vterm Stage 3 — the final vterm stage. A semantic frontend can now host a
terminal: the daemon ships complete validated cell grids, and pmacs-gpu
renders them with fixed-cell geometry, its own input path, and no document
projection at all.
Protocol v19 appends three variants after their enums' final v18 members:
InstanceMessage::TerminalFrame (daemon-gated), and FrontendEvent::
TerminalResize / TerminalPointer (frontend-gated). It is the first bump to
gate in both directions, so criterion 28 pins each filter independently and
byte pins on StatuslineSegments and MenuPointer guard the placements.
pmacs-protocol gains src/terminal.rs: the shared row/column/visible-cell/
grapheme/metadata bounds, TerminalProcessState, TerminalSelectionSpan, and
TerminalFrame::validate — the ONE structural policy the daemon runs before
emission and the frontend runs after decode. src/terminal/* re-exports them
so no duplicate type exists, and unicode-width becomes a workspace dependency
so the screen and the validator measure glyph columns with one table. A new
8 MiB aggregate glyph bound keeps the largest legal frame (measured:
13,437,863 bytes) under the unchanged 16 MiB transport cap rather than
widening every connection's allocation ceiling.
The semantic producer suppresses the whole document family for a terminal
buffer while keeping the status band, theme, font, statusline, menu, and
minibuffer, and compares the complete ordered payload rather than
screen_generation — scroll, selection, and process state all change without
advancing it.
Two things the framing did not spell out, both found by the real-daemon
acceptance:
The Viewport gate keys on the authenticated source's ACTIVE buffer, not the
buffer the message names. Viewport also aligns the window to what it
declares, so a stale document viewport in flight when a command opened a
terminal dragged the frontend straight back off it: the window oscillated,
every terminal declaration was refused, and no frame ever arrived, with
nothing logged anywhere.
The producer clears terminal mode on every exit path. The daemon uses that
flag to suppress CursorByte and the presence sweep, so an early return that
left it set kept both suppressed after the frontend returned to a document.
pmacs-gpu/src/terminal.rs is a pure cell-space paint planner, unit-testable
without a GPU. The renderer builds one shaped buffer per text run, so a wide
or cluster glyph's advance can never choose the next column's origin.
Criterion 37 needed a seam rather than a fixture: pmacs-gpu depends only on
pmacs-protocol, so attach::connect's reader sink was generalized and a
--headless-probe mode added. The acceptance drives a real daemon, a real
/bin/sh child, the real attach client, and real composited pixels in one
path — which is how both defects above were found.
Gates: fmt; strict workspace clippy; 1,757 default + 1,933 CRDT library
tests; vterm Stage 1 9/10, Stage 2 4/4, Stage 3 4/5 acceptance
(default/CRDT); statusline 7/8; M4 120; required GPU 127; workspace sweep
2,919 across 83 suites; diff check clean.
Assert that every bundled local-sensitive highlight query has a non-empty,
compilable locals query. Clarify the upstream-parity worst-case complexity of
lexical reference resolution in the framing document.
Replace the obsolete fail-closed locals note with the settled lexical-facts
contract and record the feature branch, verification, and recovery commands in
the active-work ledger.
Revision 8 of docs/vterm-framing.md was reviewed and approved on the
documentation branch vterm-stage3-framing. Stage 3 is implemented on this
branch, cut from canonical main, rather than stacked on that branch.
The framing locks additive protocol v19 (TerminalFrame, TerminalResize,
TerminalPointer), an 8 MiB aggregate glyph-byte bound under the unchanged
16 MiB transport cap, dual viewport declaration after every semantic
snapshot, authenticated per-view routing, and a fixed-cell native GPU
renderer. Criteria 28-37 are the scope of this branch.
Advance the durable baseline to PR #132, remove the completed volatile lane,
and record the shared language pin, bounded modeline contract, Vterm Stage 2
landing, remaining deferrals, and current roadmap state.
Integrate landed Vterm Stage 2 before the approved modeline merge. Preserve the
active modeline lane in the volatile ledger and record the full integrated gate
results.
Parse bounded Emacs and Vim modelines, normalize common aliases, and give
explicit file metadata precedence over inferred language. Pin one fresh-load
language decision for syntax, LSP, pairing, comments, and initial major mode,
while preserving the LSP path guard and explicit mode overrides.
Cover supported forms, rejection boundaries, precedence, unknown modes,
shebang and modeline pinning, reopen behavior, and pathless buffers.
Match Vim's real direct and set-form tokenization, reject nonexistent colon
assignments, clarify suffix scanning and trust, extend aliases, and pin the
shebang-edit regression in acceptance.
Update the durable handoff and active-work ledger with the second-review
fix checkpoint and exact final gate evidence.
Co-authored-by: OpenAI Codex <codex@openai.com>
Advance the canonical base after the mode-system handoff merge and preserve
the draft modeline framing branch, checkpoint, scope, and recovery command.
Define bounded Emacs and Vim modeline parsing, alias normalization,
explicit precedence, one pinned language decision shared by syntax and LSP,
and the acceptance contract for load-time mode detection.
Move mode-system wiring from the active ledger into the durable handoff,
refresh the side-quest priorities, and preserve the macOS acceptance lessons
from the final CI review round.
Align the durable handoff with the approved escape-prefix contract, record the performance and lifecycle hardening, update the Stage 2 verification map, and publish exact final gate evidence in the active-work ledger.
Give the daemon its normal five-second handshake window before switching
the mode-system acceptance client to short frame polling. Document reload
and session-persistence boundaries and correct stale describe-key guidance.
Resolve post-main integration drift in authenticated routing, terminal view projection, Lua installation, and inherited acceptance callers. Preserve the terminal statusline provider alongside the landed Themes provider and record the final Stage 2 gate evidence.
Co-authored-by: OpenAI Codex <codex@openai.com>
Add cross-surface Lua, shared-view, clipboard, authenticated routing,
BEL, resize, and real-host PTY acceptance. Ensure terminal-local keymaps
run before raw child transport and document the criterion-to-test map.
Co-Authored-By: Claude <noreply@anthropic.com>
Merges canonical main up to 2e37c04 and records the second of the two
arcs that landed while this lane was open. Vterm Stage 1 (#126) was
already recorded; this adds the config registry and reconciles every
claim the two merges falsified.
Handoff §1: main pointer moved to 2e37c04, and a config-registry entry
covering the parts a future agent cannot re-derive from the code --- the
always-store rule and why the "equal-value set is a no-op" reading
silently voids a buffer-local pin; the two-scope model and the
no-ambient-buffer contract on get(name); explicit-dispose-only listener
lifetime and the absence of any MetaMethod::Gc; the InitCompleteFlag
freeze that kept editor.rs untouched; and the strict-registry /
lenient-wrapper split that preserves trim_on_save("yes") and
interval_ms(1500.7).
Handoff §5 gains two lessons. Tab width is a rendering-parity bug, not
a config gap: five sites across two crates with two different values,
and no tab expansion at all on the GPU main text path, so
editor.tab-width is the obvious-looking first adopter and is not one.
And "a test that never runs passes" --- pmacs.editor.save() is the raw
save while buffer.before-save fires inside the buffer.save COMMAND, and
save() no-ops on an unmodified buffer, which made two review-round tests
vacuous until the buffer was dirtied and the command invoked.
Handoff §6: the three config-registry-blocked deferrals are resolved
(the per-buffer auto-pair toggle shipped as editing.auto-pair), replaced
by the registry's own named deferrals --- persistence, list-settings, a
settings completion source, table-valued settings, the unmigrated scalar
setters, and a scope = "global" flag, since set_local is currently
accepted for autosave.interval-ms where a per-buffer value is
meaningless.
active-work.md: base pointer and recovery assertion moved to 2e37c04;
the Vterm Stage 2 lane is told to cut from current main rather than
643d1e1; a closed-since-last-snapshot section records the merged lane
and the parallel-lane result --- two arcs in sibling worktrees with the
shared files assigned one lane each in advance rebased with zero
conflicts, which is worth repeating and states its precondition.
side-quest-backlog.md: both original north-star items have now shipped,
so the board is re-ranked to locals-query processing, mode-system wiring
(promoted --- every editor resolve still passes &[], making it the
largest remaining scoping gap), and tab-width parity. The config-registry
entry is struck and tab width is split out of it, since listing tab
width as a config consequence is what made it look like a cheap adopter.
Documentation only: the diff against main touches no runtime code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Carry the reviewed Revision 7 framing onto the Stage 2 implementation branch
as its first commit. The contract defines durable per-view state, authenticated
input authority, TUI composition, strict Lua operations, and acceptance 15-27.
Co-Authored-By: Claude <noreply@anthropic.com>
Add the portable Revision 7 branch, exact approved checkpoint, current state,
next implementation lane, and cross-machine recovery command. Preserve the
separate documentation-lane ownership boundary.
Co-Authored-By: Claude <noreply@anthropic.com>
Review round 1, findings 2-4 plus doc notes. Finding 1 landed in fd80bcb.
Finding 3 --- spec fields meaningless for the declared type are now
rejected. DEFINE_SPEC_FIELDS whitelists all nine keys for every type and
the kind parser only reads its own arm's fields, so
`{ type = "string", choices = {...} }` silently defined a string that
accepts anything (the author meant enum) and `min` on a boolean was
dropped. These are typo-shaped bugs the R50 whitelist structurally
cannot see: the key is spelled correctly, it is on the wrong type.
`check_fields_relevant_to_kind` closes it with a pointed error naming
the misplaced field, and a companion test pins that each field is still
accepted where it belongs, including `min`/`max` on number as well as
integer.
Finding 4 --- the after_buffer_removed purge had no end-to-end test.
Every existing test called ConfigRegistry::remove_buffer directly, so
deleting the three lines wired into mod.rs would have left the whole
suite green. The new acceptance test kills a buffer through
pmacs.buffer.remove (the real remove_buffer_and_fire route) and asserts
the locals are gone; bite-verified by removing the hunk and watching it
fail.
Finding 2 (the half with a natural buffer) --- editing.trim-on-save is
now resolved against the buffer being saved rather than the global
chain. Reading globally meant set_local was accepted, stored, and
reported by describe, then never consulted: a pin the user believes in
that does nothing, which is the shape F1 exists to prevent. Two tests,
one for the override and one for the global fallback the change could
have broken; the override test fails against the old global read.
Both new save tests initially passed VACUOUSLY and were rewritten:
pmacs.editor.save() is the raw save, while buffer.before-save fires
inside the buffer.save COMMAND (default.lua:224), and save() no-ops on
an unmodified buffer --- so the original form asserted on a file that
was never rewritten. They now insert content to dirty the buffer and go
through pmacs.command.invoke("buffer.save").
The other half of finding 2 --- a per-buffer autosave.interval-ms is
semantically meaningless yet still accepted --- is recorded as a named
deferral proposing a define-time `scope = "global"` flag, alongside
deferrals for bound-parse field naming and StartupOnly reset symmetry.
Also recorded: interval_ms(1e30) now raises instead of storing a
nonsense float, an improvement but a real divergence from "the wrapper's
shape stays exactly as it was".
Doc: the module header cited framing revision 2; the shipped doc is
revision 3, whose corrections are what the code implements.
Gates: fmt, clippy -D warnings, --lib (1691), --lib --features crdt
(1865), lua54 backend, config_registry_acceptance (16), editops (72),
autosave (29), PMACS_REQUIRE_GPU=1 pmacs-gpu (109), and the full
workspace sweep (2806 tests, exit 0). git diff --check clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A third registry beside CommandRegistry and HookRegistry, per
docs/config-registry-framing.md. Unblocks the per-buffer auto-pair
toggle, the first of the five backlog items the missing config surface
was gating.
Substrate (src/config_registry.rs):
* ConfigRegistry keyed by name with definition order preserved, R42
mandatory descriptions, R50 typo detection, duplicate rejection,
and SourceLocation provenance -- the command/hook vocabulary.
* Closed scalar kinds: boolean, integer, number, string, enum. Owned
Rust values; Lua tables, functions and userdata are never stored.
Integer exactness is checked by value, never math.type, so the
luajit and lua54 builds agree.
* Two scopes. get(name, buf) resolves buffer-local -> global ->
default; get(name) with no buffer resolves the global chain only
and never consults an ambient buffer. Buffer-locals live in a
registry-owned side table purged at after_buffer_removed, beside
the keymap purge already there.
* An override is ALWAYS stored, even when equal to the value it
shadows; only value_epoch and listener dispatch key on effective
change. Without this a buffer pinned to the current value stores
nothing and a later global set flips it -- the pin silently never
existed. equal_valued_local_override_is_still_stored_and_shields_buffer
fails against the naive reading.
Bindings (src/lua_bindings/config.rs):
* define/get/set/set_local/reset/is_set/describe/list/on_change.
Spec tables are read raw, so neither an unknown key nor a
metatable-provided value can smuggle a field in.
* Listeners commit inside the borrow, snapshot, drop the borrow, and
only then re-enter Lua -- verified by holding the borrow and
watching the test panic with "RefCell already borrowed". A raising
listener is logged without blocking later ones or rolling back, and
a depth bound turns an accidental cycle into a pointed error.
Listeners persist until explicitly disposed; there is no Gc path,
matching the rest of the codebase.
* StartupOnly freezes off the existing InitCompleteFlag at write
time, so this arc adds no editor.rs call at all.
Adopters, each defining its own key so SourceLocation names the owning
module: editing.auto-pair (pair.lua, read per-buffer against the typed
edit's SOURCE buffer), editing.trim-on-save (editops.lua),
autosave.interval-ms (autosave.lua). No public function is removed or
deprecated, and both migration wrappers keep their legacy coercion --
trim_on_save("yes") still enables, interval_ms(1500.7) still floors to
1500 -- coercing before handing the strict registry a conforming value.
M-x describe-setting renders into *help*, modeled on describe-command.
Framing revision 3 records four defects implementation found in the
document itself: acceptance 30 and 31 contradicted each other; the
planned builtin/runtime/config.lua had nothing to hold and would have
broken the source-location contract had it held the one helper it might
have; F5 asked define to police a call it cannot see, moved to
set_local; and list() ordering was underspecified.
No protocol change; SUPPORTED stays [6..18]. No wire surface. Zero
changes to src/editor.rs.
Gates: fmt, clippy -D warnings, --lib (1683), --lib --features crdt
(1857), the new config_registry_acceptance (13) plus auto_pair (45),
editops (72), autosave (29) and m9_6 (25), m4 --skip basedpyright
(114), PMACS_REQUIRE_GPU=1 pmacs-gpu (109), the lua54 backend build,
and the full workspace sweep (2795 tests, exit 0). git diff --check
clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The cross-cutting substrate ranked first on the side-quest north star.
A third registry beside CommandRegistry and HookRegistry, with two
scopes (global and buffer-local), a closed scalar value vocabulary,
post-commit listeners, and no wire surface.
Ground truth established by scout at 7bc0c61, including three findings
that shaped the design:
* Tab width is five constants across two crates with two different
values -- four TAB_WIDTH = 8 in the daemon, 4 in the GPU minimap,
and no tab expansion at all on the GPU main text path. It is a
rendering-parity bug, not a config gap, so it is deferred to
stage 2 rather than made the proving adopter.
* The mode system is unwired: every editor KeymapStack::resolve
passes an empty active-modes slice. A mode scope would ship a knob
that silently never fires.
* load_user_config and set_init_complete both run inside
EditorState::new under one cfg(not(test)) block, so the
StartupOnly freeze point covers the daemon and local entry points
uniformly -- and never runs in --lib test builds.
Revision 2 records review round 1 (F1-F11). The load-bearing fix is
F1: an override is now always stored, with only value_epoch and
listener dispatch keyed on effective-value change. The prior
"equal-value set is a true no-op" reading meant an equal-valued
buffer-local override stored nothing, so a later global set would flip
the very buffer the user had pinned -- silently voiding the per-buffer
toggle this arc exists to deliver.
Also carried by revision 2: listener dispatch semantics pinned across
scopes, GC-collected listeners dropped (no MetaMethod::Gc precedent
exists), migration wrappers keep their legacy coercion, StartupOnly x
set_local rejected at define time, string-list dropped, describe's
`local` field renamed buffer_local (Lua keyword), and the direct-remove
leak corrected to permanent-but-non-aliasing since BufferIds are never
reused.
No implementation. Framing only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Synchronize the documentation continuity lane with #126, preserve the parked
work inventory, and record the landed terminal-core state and next sequential
TUI/GPU stages.
Advance canonical main to the #126 merge, retire the completed active lane, and
record the landed headless terminal core plus the remaining TUI/GPU stages.
Record the control-free cell and SGR mouse hardening, accepted cleanup changes,
remaining DECSTBM/allocation/limit deferrals, behavioral bite, and final gate
counts in framing Revision 5 and project continuity docs.
Record the addressed Stage 1 review, final branch head, full gate counts, and
clean behavioral bite while preserving the unmerged three-stage boundary.
Record the IND, NEL, RI, TERM, portable-liveness, and tab-stop fixes; make the
line-oriented EOF contract explicit; and list the accepted fidelity,
performance, and lifecycle deferrals.
Update final from-start gates and the clean behavioral bite proof.
Record the Darwin PTY signal-name mismatch found by PR CI, the normalization
commit, and the final from-start green gate sequence with updated test counts.
Record the published Stage 1 implementation and documentation heads, open PR
#126, final verification, and the sequencing boundary for the later TUI and
protocol/GPU stages.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Record canonical protocol v18 main after #125, mark Arc 4 complete, and pin
the Vterm Stage 1 feature commit and open PR #126 in the delivery records.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add compatibility-preserving full-screen ANSI operations, the bounded terminal
screen and input encoders, and a transactional TerminalManager owning one
read-only identity buffer, PTY process, and screen per session.
Drain terminal-owned process events before process.after-tick, retain exact
final output and PID/outcome annotations, reap killed buffers and shutdown
children safely, and enforce buffer-owned read-only checks across ordinary,
host, undo/redo, and CRDT mutation paths.
Cover split parser and grapheme boundaries, screen/reflow/history invariants,
device responses, lifecycle cleanup, and a real adversarial alternate-screen
PTY. Record the fully gated Stage 1 delivery and downstream TUI/GPU contracts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Record the chosen C-c escape, resize and exit behavior, additive v19
compatibility, complete-frame wire, unchanged Style, and shared-session
ownership. Specify the Emacs-style PID/outcome line retained after exit.
Define the terminal screen ownership boundary, three-PR delivery,
protocol v19 surface, fixed four-agent roster, and 33 acceptance cases.
No implementation is included; the framing remains pending user review.
Advance canonical main and protocol state after PR #125, remove the
completed statusline lane from the active-work ledger, and mark the
first four roadmap arcs complete. Record Arc 5 stage 2 as the next
formal roadmap stage.
Co-Authored-By: Claude <noreply@anthropic.com>
Record the manually folded hardening changes, current feature head,
second review resolution, and final sequential verification counts.
Co-Authored-By: Claude <noreply@anthropic.com>
Document and pin the GPU built-in-only narrow-band clipping policy,
including the intentional ability of a wide right group to hide the
left identity. Guard the fixed UI face ordering used by binary search,
preserve flattened provider tracebacks in *errors*, and rename the
phase-one unavailable reason to cover missing layout contexts.
Refresh the implementation verification record after the full gate
suite.
Co-Authored-By: Claude <noreply@anthropic.com>
Update the volatile ledger with the rebased feature head, real TUI PTY
smoke evidence, post-rebase gates, review comment, and recovery state.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Update the feature handoff and framing to current main bb17ec9 and the
post-rebase full-gate counts.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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>
Checkpoint revision 1 of the Arc 4 stage-3 statusline-segments framing
on its own portable branch. The framing remains awaiting user review;
there is no implementation or PR.
Record the exact feature head, PR #125, complete sequential gate results,
and clean recovery state for the statusline-segments review lane.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Mark CI run 29778967156 successful across all 12 jobs for exact PR head
5c202c5. Leave user review as the only remaining action and retain the
standing prohibition on unprompted merge.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
Record the green logged workspace sweep, final diff audit, and matching
public/checkpoint head 5c202c5. Pin PR #123's full head OID and fresh CI
run so any machine can resume at CI or review without reconstructing
local state.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
Record the green formatting, standalone Clippy, default and CRDT library,
M4 acceptance, live JSON/YAML provider, and required-GPU results for
checkpoint 5c202c5. Leave only the logged workspace sweep, final diff
check, and public PR-branch update outstanding.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
Record checkpoint 5c202c5 after the one-line doc-markdown correction was
amended into the live YAML test commit. Preserve the first Clippy result
and its rerun state for cross-machine continuation.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
Drive Red Hat yaml-language-server 1.24.0 through the default YAML
auto-attach path. Disable SchemaStore and the Kubernetes CRD catalog for
network-free determinism, require language-specific initialization and a
real syntax diagnostic, and prove the server remains alive afterward.
Update the framing and runtime commentary with the completed live-provider
evidence. The test passes against the pinned provider and fails against the
pre-JSON/YAML runtime under scripts/bite.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
Advance the portable JSON/YAML head to 3ef5e2e and record its clean rebase
onto canonical main f8096ff. Leave the full gates and public PR-branch
update as the only remaining work.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
Preserve PR #123's unpushed review fixes on a transfer branch: initial
didChangeConfiguration delivery, explicit JSON validation, the pinned
JSON server provider, corrected YAML configuration sections, and
deterministic plus real-provider acceptance coverage. Record the
observed yaml-language-server 1.24.0 standalone smoke and leave the
real YAML-through-pmacs test, rebase, and full gates explicitly pending
for the destination machine.
Add tree-sitter-json (0.24) and tree-sitter-yaml (0.7) to
BUILTIN_LANGUAGES (both ABI-current via tree-sitter-language, verified
compiling under tree-sitter 0.26), each self-contained highlights, no
injections of their own. Extensions json=.json, yaml=.yaml/.yml; root
kinds json `document`, yaml `stream`.
The payoff from the #122 injection engine is free: the markdown block
injection query already sets injection.language "yaml" for `---`
frontmatter (minus_metadata) and "toml" for `+++` (plus_metadata), so
registering yaml lights up YAML frontmatter highlighting with no extra
wiring, and ```json / ```yaml / ```yml fences resolve through the engine
(yml->yaml alias already present). Two acceptance tests pin this synergy.
LSP (builtin/runtime/lsp.lua): pmacs.lsp.config.json uses the maintained
extracted-bundle binary `vscode-json-language-server --stdio` (NOT the
stale standalone vscode-json-languageserver); MIT, no telemetry, remote
$schema fetch left enabled (no handledSchemaProtocols). pmacs.lsp.config
.yaml uses `yaml-language-server --stdio` with Red Hat telemetry
disabled by default. Both ship the exact workspace/configuration sections
each server pulls (json+http; yaml+http+redhat.telemetry) present-not-null
so the servers get defaults rather than erroring — the CMake #117 lesson.
Sections derived from server source/docs (neither binary installed on
this build machine to observe live; verify where present). Filetype
fallback entries added. JSON is the standing prerequisite for the Jupyter
.ipynb arc; handoff §6 updated.
Nine acceptance tests (grammar ABI, highlights compile, detection,
grammar<->LSP-key alignment, the two frontmatter/fence synergy proofs,
and the pinned LSP-config sections).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
Frame the JSON/YAML grammar-gap addition (tree-sitter-json 0.24 +
tree-sitter-yaml 0.7, both ABI-current) plus their language servers, and
the free frontmatter/fence highlighting that falls out of the #122
injection engine (the markdown block query already injects yaml for `---`
frontmatter and toml for `+++`). Four decisions (Q#JY1-JY4), nine
acceptance tests. JSON is also the standing prerequisite for the Jupyter
.ipynb arc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
Define the canonical repository by URL and bootstrap a stable local
`githubsucks` alias instead of assigning authority to machine-specific
remote names. Record JSON/YAML checkpoint f99870e and the completed live
YAML provider plus bite evidence, leaving only rebase and full gates.
Keep AGENTS.md and CLAUDE.md synchronized so a fresh agent receives the
same recovery rule on any machine.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
Record the pushed JSON/YAML checkpoint and framing heads so a clean
machine can verify it recovered the intended state, and document the
existing-local-branch worktree variant.
Add synchronized agent bootstraps and a volatile active-work ledger so
another machine can distinguish durable project state from open
branches, local checkpoints, machine-only providers, and incomplete
verification. Record githubsucks/main as the canonical development
line, refresh the #124/protocol-v17 handoff, correct the current
keybinding reference for compile mode, and mark the July roadmap as a
historical snapshot.
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