Stages 1 and 2 landed (#160, #161). Re-scouting Stage 3 against `main`
@ `46a1b8f` — six merged PRs past the rev-4 snapshot — produced three
findings that change the plan and four that confirm it. Two were
established by running Lua in a fresh `EditorState` rather than by grep,
and are marked *probed* in §0.1.
**Stage 3 violated this document's own splitting rule.** §4 says "no PR
in this arc mixes a cross-cutting substrate change with Lean feature
content" and "a reviewer looking at Stage 3 sees only Lean" — while §4's
own risk column for Stage 3 read "two `lsp.lua` generalizations". Those
cannot both be true. One generalization shipped as Stage 2; the other is
Q#LN9's dispatch seams, which modify `handle_server_requests` —
confirmed the only production drain of LSP events, since
`LspManager::take_all_events` has no non-test caller. By the test that
justified splitting Stage 2 out, that is cross-cutting substrate. Stage
3 is now 3a (seams + canonicalizer, no Lean) and 3b (the Lean server),
strictly sequential.
**The Lean resolver could not satisfy the contract Stage 2 documented.**
#161 established that a configured root reaches `file_uri_for` verbatim
and that the resulting URI is the affinity key. Probed:
`pmacs.editor.file_path()` is not canonical — opening
`<tmp>/linkpkg/sub/./../sub/a.lean` through a symlink yields
`<tmp>/linkpkg/sub/a.lean`, lexical collapse only. No canonicalize
binding is exposed to Lua, and `pmacs.project.detect` canonicalizes but
returns nil without a marker. So one Lake package opened by two
spellings would spawn two `lake serve` processes — the bug Stage 2 was
built to prevent, re-entered through Stage 3's door. New Q#LN20 adds a
synchronous `pmacs.fs.canonicalize`; it rides 3a, and it serves every
future function-valued root rather than only Lean's. Two alternatives
are recorded with why they were rejected — the `detect`-anchored walk in
particular is incorrect, not merely inelegant.
**`pmacs.fs.stat` is unusable in the resolver.** It is async and the
resolver runs synchronously inside `ensure_server` ← `attach_buffer` ←
`buffer.after-load`, with no coroutine to await on. Probed: `io` and
`os` are exposed in the sandbox, so the marker walk uses `io.open` — the
opposite of what a reader would assume, hence Q#LN8 now says so. One
edge, also probed: `io.open` succeeds on a directory, so the walk reads
a byte rather than testing for a handle, and acceptance 24a bites the
version that does not.
Confirmed rather than changed: Q#LN7's stop-before-respawn is necessary
(default policy is OnCrash, the termination handler never consults the
exit code, and `maybe_restart` has no attempt ceiling — a broken `lake`
respawns forever; `stop()` setting `restart = Never` is what disarms
it); the response seam works as specified, since `Response` events are
pushed unconditionally and `send_request` returns the keying id.
One confirmation narrowed the design. `handle_server_requests` builds
its sid list from `attachments` and `push_event` is uncapped, so
subscribers fire only for servers with a live attachment. That turns
acceptance 34 into a reachable leak: killing the buffer with a request
outstanding strands the registration behind a drain that no longer runs.
The purge is now driven from both edges and 34 exercises the buffer-kill
path, which is the one a user can reach.
Also: §9 states the lane's coherence impact per COHERENCE §20 (journey
steps, interaction islands, config registry, background attribution),
including the honest note that 3b makes §2's step-3 grade marginally
worse by adding one more instance of the silent-spawn-failure class.
Three items are named in §6 rather than paid: the uncapped event queue,
the dropped `cfg.restart`, and surfacing the spawn failure itself.
Acceptance keeps every rev-4 number. The two split sections are
bulleted with literal labels because a markdown ordered list renumbers
from its first item, and 3b's criteria are non-contiguous; round 3's
finding 4 was stale references surviving a renumber, and not renumbering
is the cheaper way to not repeat it. Stale cross-references from the
split were reconciled in the same pass, and `project_root_for`'s
citation was corrected from 513 to 592 per COHERENCE §25.
Lean 4 Stage 2 (#161) landed while this branch's first-ever CI run was
in flight, which put the PR back to CONFLICTING at an unmoved head.
Merged rather than rebased, same as the 8c86d34 integration and for the
same reason: the PR is awaiting review rounds and a rebase would break
every review anchor.
The sole conflict was docs/active-work.md, as it was last time and for
the same structural reason -- every merge to main edits the lane ledger,
so a long-lived PR re-conflicts there and only there. Both sides' lanes
kept verbatim; main's updated Lean 4 heading taken over the stale one.
This integration is code-disjoint from the lane. Intersecting main's
changed files (COHERENCE.md, builtin/runtime/lsp.lua,
src/lua_bindings/mod.rs, tests/lsp_multi_root_acceptance.rs) against the
lane's own changed-file set leaves exactly docs/active-work.md, so none
of the first integration's pmacs-gpu/src/main.rs auto-merge risk recurs
here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZjWMjwPXhPbt9upku9mCk
Separates the pre-integration numbers, which described a tree 28 commits
behind, from the ones that describe what the PR now proposes.
The GPU test count is the integration proof rather than merely a pass: it
went 199 to 202, and e547a90 added exactly three tests to pmacs-gpu,
which is the entire delta on main since the merge base. Both sides' tests
are therefore present and running, and neither was dropped by the
auto-merge. Confirmed structurally as well -- main's fix survives as the
deferred closure form rather than the eager one that panicked, with its
regression test, alongside this lane's math work in the same file.
The lane was 28 commits behind. Merged rather than rebased, per the
#135/#137 precedent: the PR is awaiting review rounds and a rebase would
break every review anchor.
The only conflict was docs/active-work.md, where both sides add lanes.
Kept both: main's lanes verbatim, with this lane leading since it is the
one in flight. The conflict was pre-existing rather than introduced by
the dired or Lean 4 ledger commits -- it already conflicted against main
at e745068.
The integration surface, derived from git diff merge-base..main rather
than from another PR's file list, is pmacs-gpu/src/main.rs: main gained
72 lines there from e547a90, the minimap all-blank-slab divide-by-zero
fix, and this lane rewrites large parts of the same file. Git auto-merged
it textually. A clean auto-merge is not evidence the tree compiles, so
the full gate suite is what discharges it; the ledger records the
post-integration numbers separately from the pre-integration ones, which
described a tree 28 commits behind.
Lands the approved dired framing on main as its own docs PR, and brings
the two required docs current after find-file merged as #162.
The framing was approved after two review rounds (seven findings, then
six) and revised twice more since: revision 4 recorded what implementing
Stage 0 falsified in the approved text, and revision 5 adds the coherence
impact statement that #163 made mandatory for every framing.
The coherence statement is new work, not a restatement. COHERENCE.md
section 20 Priority 1 already names this arc -- a find-file surface and
directory-argument handling -- so the framing now states which journey
steps it touches (7, and partially 3), that it adds no interaction island
because its keys are a mode-scoped keymap through the ordinary registry
and wdired is a mode swap rather than a modal layer, that it adopts the
config registry for dired.kill-when-opening, and that it inherits the
worker-attribution gap for its read_dir jobs without worsening it. It
also draws the boundary against the adjacent Journey Stage 1 arc: CLI
directory handling belongs there, the two meet at resolve_target_buffer,
and dired supplies the buffer a directory should resolve to rather than
growing a second directory surface.
One convergence worth recording: section 2 grades the golden journey
broken at step 3 because pmacs on a directory exits 1, and the mechanism
it cites -- File::open succeeding on a directory, then read_to_end
returning EISDIR -- is the same one Stage 0 pinned in its
accepting-a-directory test, where the pcall turns it into a status
message instead.
The handoff snapshot was stale through eight merges. It now anchors on
main at 2af1ab3, records COHERENCE.md as required reading and a required
framing input, and carries the two minibuffer facts find-file
established: a custom completion source cannot descend directories, and
a selected candidate shadows typed text -- both of which apply to M-x and
switch-buffer, not just find-file.
The ledger gains the dired lane with Stage 1's scope, the reason its one
Rust change cannot be done in Lua, and the rebase note for the dired
branch, whose framing commits become redundant when this lands.
The blocker was process, not design. The test file was committed before
`cargo fmt` ran, so the reflow of five over-width assertions sat
uncommitted in the working tree while the branch as pushed failed the
first gate in CLAUDE.md. The "fmt clean" reported on the PR described
the worktree, not the branch. Gate results are only meaningful run
against the pushed tree, so this commit lands the formatting first and
the gates are re-run against it.
Two pins review asked for, each covering a branch the nine acceptance
tests left untested:
- A **string** `config.root` as an affinity key. acc17 covers only the
function form, so `return configured, "config"` had no test. The bite
puts both files in their own marked project: drop the config arm and
they key on their own detected roots and spawn two servers, so one
server on the configured root is only reachable if the override wins.
- `root = false` reads as unset. Defended by a truthiness check rather
than `~= nil`, previously by comment alone. Under `~= nil` the config
arm returns `false, "config"` and `file_uri_for(false)` returns nil, so
the file lands on a rootless server instead of its detected project.
Each was falsified against exactly the mutation it targets and neither
against the other.
Also documents an asymmetry review caught: `project_root_for`'s
"detected" arm is canonicalized for free because `pmacs.project.detect`
canonicalizes before walking, but a **configured** root — string or
resolver return — is fed to `file_uri_for` exactly as written, and the
affinity key is that URI. On macOS a resolver returning `/var/…` and a
detected `/private/var/…` are therefore different keys for one
directory, silently yielding two servers for one project. There is no
Lua-side canonicalizer to normalize it, and Stage 3's Lean resolver is
the first real consumer, so the obligation is stated in the
`config.root` doc comment where that resolver's author will read it.
Stage 1 merged as #160 (`main` @ `0827dd1`); the Lean lane header and
branch line now say so, and Stage 2 gets its own subsection.
Edits stay inside the Lean lane. PR #156 is still open against both this
file and `docs/agent-handoff.md`, and it rewrites the snapshot header,
the canonical-base line, and the whole bottom-panel lane — so those are
left alone rather than merged twice. `agent-handoff.md` is untouched for
the same reason plus its own: §1 describes what is on `main`, so it
updates at merge, not during review.
Records the one finding this stage turned up but did not fix:
`ensure_server` never forwards `cfg.restart` to `pmacs.lsp.spawn`, so a
`restart` in `pmacs.lsp.config[lang]` is silently dropped on the
auto-attach path. Pre-existing, and out of scope for a PR whose
acceptance 16 pins existing attach behavior as unchanged.
Review round 1 flagged that neither ledger knew about this branch, and
`docs/active-work.md`'s stated job is exactly the volatile open lanes.
Records the branch, base, framing revision, what Stage 1 ships, the
discharged Q#LN1 obligation, the Q#LN4 blast radius, and the four
implementation findings that are not in the framing (the `warning`
colour collision with `number`, `Some(1)` resolving to `@function`
rather than `@constructor`, the `module > declaration > def` nesting,
and `injection_aliases` being a write-only proxy). Also carries forward
the two Stage 2 corrections the framing already holds, since that lane
starts next.
Deliberately ADDITIVE ONLY -- one new section, zero deleted lines. PR
#156 is open against both this file and `docs/agent-handoff.md` and owns
the snapshot header, the canonical-base line, and the bottom-panel
lane's status. Touching those here would collide with a PR already in
review, which is the "frozen reviewed PRs do not absorb moving
overlapping work" lesson from #135/#137.
`docs/agent-handoff.md` is deliberately untouched: its §1 snapshot
describes what is ON `main`, so it gets updated when this merges, not
while it is in review.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The approved framing for Arc 8, revision 4, after three review rounds.
Seven stages: grammar/mode, multi-root LSP affinity, the Lean language
server, the Unicode input method, the goal view, the #eval output
channel, and module hierarchy. 19 decisions, 64 acceptance criteria.
Committed as this branch first commit per the house workflow; the
implementation of Stage 1 follows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two halves that touch live rendering, landed together because the
acceptance criteria that make either honest need both.
Suppression (Q#MS3/MS4/MS5/MS11). Detection runs in the per-line chunk
builder — the chunk-build path, never the edit path — and substitutes
each suppressed span's source bytes with ONE spacer chunk BEFORE tab
expansion, so a literal tab inside a span vanishes with it while tabs
outside keep their SourceTab provenance. The gate reads the EFFECTIVE
caret (own_cursor, which optimistic edits predict forward — F4's
no-flap requirement holds by construction) plus both own-selection
endpoints. Three motion paths can flip a gate without a content
change, and each now re-runs the per-line chunk compare, gated on a
one-scan "does the visible slice hold a $" check: the CursorByte arm,
finish_optimistic_edit (the text re-chunks under the OLD caret there;
without the hook a typed char rendered one keystroke stale), and the
Decorations arm — whose "no decoration change needs a reshape" premise
acquires exactly one exception, the Selection endpoints Q#MS11 made
suppression inputs.
The line-reuse predicate (acceptance 11, the #120 edge). Per-line
math state is cached in lockstep with line_chunk_cache: every detected
span with the gate bit it was built under. The scroll-reuse path
refuses a retained line whose cached bits disagree with the CURRENT
caret/selection — content is unchanged on every reuse path, so the
cached span set is authoritative and the gate bits are the only
variable. The acceptance test drives the stale-gate case through
rebuild_lines_reusing_scroll directly and fails if the gate is removed
from the predicate.
The hit map (B1'). hit_test_source_byte rebuilds its runs from a
whole-slice chunk walk, so it now reads the substitutions BACK from
the per-line caches — never re-planned under a possibly-newer caret —
keeping the map and the shaped glyphs one source of truth.
The draw pass (Q#MS6/MS7). Every MathItem::Glyph draws from its own
mini-buffer with Attrs pinned to the bundled math family (F8b), placed
at layout's exact x and the shaped line's REAL baseline; the
mini-buffer itself is positioned by the line_y cosmic-text actually
produced for it, so no font-metric rederivation can drift. Fraction
rules ride the bg quad batch after the decoration washes and under the
glyphs. Wash geometry gains Q#MS11's intersection rule: a wash
touching a suppressed span widens to the box's whole reserved
rectangle (a match strictly inside the span produced a zero-width
interval before), while the round-3 exclusive-end fix keeps a
non-intersecting wash off the box.
Acceptance (framing §5). Criteria 5-11 and 14-16 run on real pixels
through render_to_view: drawn ink where a literal-spacer control
renders none, with the before-region pixel-identical; the fraction
rule as a full-width run with operand ink both sides; caret-inside
rendering EXACTLY as math-disabled (driven through the real
CursorByte arm, which owns the refresh — a direct helper call would
not have pinned the wiring); every failure mode (unbalanced, unknown
command, $$, uncoverable glyph) pixel-equal to disabled; box clicks
snapping to the span start with the trailing edge landing after the
span; the scroll-reuse stale-gate bite; reflow confined to the
affected line with the after-text shifted by exactly the quantized
projection difference; selection gating and the whole-rectangle wash;
and the licence provenance pair. Criterion 17 is discharged
differentially: cargo tree -e features output for ttf-parser is
byte-identical with and without this crate's dependency line.
Also folded in, per the round-3 close-out: the F6 documenting test
($a$$b$ is eaten by the $$-opaque rule; one separating character
restores both spans), the depth-search bound raised 6 -> 8 so a
metric shift cannot make the "floor is dead code" expect fire with a
misleading message, the MathBox { end, .. } pattern nit, and the
active-work.md lane entry.
Named v0 approximations, deliberate: the peer-caret half of
acceptance 14 is pinned at the mapping level (unit tests), not
pixels; a soft-wrapped spacer draws its box whole at the first run's
origin (the one-rectangle model); the fit budget reads the bundled
code face even under a custom set_font family — the draw anchors to
the real shaped baseline either way, so only the fit margin is
approximate.
Clippy is CLEAN across the workspace at -D warnings for the first
time on this branch: the draw pass consumed every formerly-dead item,
and the three lints it could not fix (a test-only accessor, one doc
string, one manual midpoint) are fixed here.
Gates: cargo fmt --check; cargo clippy --workspace --all-targets
-- -D warnings; 1,815 default + 1,992 CRDT library tests; M4 121
(basedpyright skipped); 199 pmacs-gpu tests under PMACS_REQUIRE_GPU=1;
workspace sweep 3,131 across 88 suites (isolated XDG_CONFIG_HOME);
git diff --check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.