feat(math): caret-driven suppression, the draw pass, and the slice acceptance

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>
This commit is contained in:
Levi Neuwirth 2026-07-24 22:04:35 -04:00
parent cbf7782726
commit 14c1c01043
4 changed files with 1096 additions and 20 deletions

View File

@ -54,6 +54,41 @@ git status --short --branch
The `git log` command must expose `0dd16a5` or a newer intentional main.
If it does not, stop and repair the remote/fetch configuration.
## Inline-math slice lane — IN FLIGHT
- Portable branch: `githubsucks/inline-math-slice`; worktree
`../pmacs-math-slice`.
- Framing: `docs/inline-math-slice-framing.md` rev 3, approved after two
review rounds; parent arc framing merged as #154.
- State: parser, font bundle (GUST licence), MATH-table layout with the
measured height budget, currency-guarded detection, and the
`ChunkSource::MathBox` spacer substrate are implemented and
round-3-reviewed (review fixes at `cbf7782`: the exclusive-`end`
mapping bug its own test had pinned, script-marker whitespace, fallible
layout via `UncoverableGlyph`, real fraction gap-min constants —
flagship scale 0.867, fallback depth 5).
- Caret-driven suppression (the Q#MS5 gate over the effective caret and
Q#MS11 selection endpoints, chunk substitution before tab expansion,
the line-reuse predicate's third input, and the CursorByte /
optimistic-edit / Decorations refresh triggers), the draw pass
(per-glyph mini-buffers positioned by each shaped line's real
baseline, fraction-rule quads over the washes, the F8b family pin),
the Q#MS11 whole-rectangle wash widening, and the pixel acceptance
battery (criteria 511, 1416; 17 discharged by the differential
`cargo tree -e features` check — byte-identical with and without the
dependency line) are implemented on the branch tip.
- Clippy is CLEAN on the whole workspace at `-D warnings` — the draw
pass consumed every formerly-dead item.
- Verification at the tip: 199 `pmacs-gpu` tests under
`PMACS_REQUIRE_GPU=1`; 1,815 default + 1,992 CRDT library tests; M4
121; full workspace sweep green (isolated `XDG_CONFIG_HOME`).
- Remaining before PR: nothing known; awaiting the user's review pass.
Named v0 approximations: 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 fit budget
reads the bundled code face even under a custom `set_font` family
(the draw anchors to the real shaped baseline either way).
## Folding lane (Arc 6) — Stages 1 and 2 MERGED; Stage 3 (GPU) is next
Both shipped stages are on `main`; nothing in this arc is in flight. Stage 3

File diff suppressed because it is too large Load Diff

View File

@ -328,6 +328,9 @@ impl<'a> MathLayout<'a> {
Ok(Self { face, constants })
}
/// Test-only introspection: the production draw path consumes the
/// constants through `layout`, never raw.
#[cfg(test)]
#[must_use]
pub fn constants(&self) -> MathConstants {
self.constants
@ -516,6 +519,19 @@ mod tests {
use crate::math_parse::parse;
/// Framing acceptance 16 (provenance half): the GUST licence ships
/// beside the font, names itself, and is not the OFL that covers
/// `JetBrains` Mono.
#[test]
fn bundled_licences_are_distinct_and_name_their_terms() {
let gust = include_str!("../fonts/GUST-FONT-LICENSE.txt");
let ofl = include_str!("../fonts/OFL.txt");
assert!(gust.contains("GUST Font License"));
assert!(gust.contains("LaTeX Project Public License"));
assert!(!ofl.contains("GUST"));
assert_ne!(gust, ofl);
}
#[test]
fn tex_symbol_greek_forms_are_italicised_too() {
// F5's trap: correcting the seed map alone leaves these upright,
@ -744,7 +760,12 @@ mod tests {
if scale < MIN_FIT_SCALE {
break Some((depth, src.clone()));
}
if depth >= 6 {
// Headroom above the real boundary (5 with the round-3 MATH
// gaps): if a metric shift pushed the boundary past this bound,
// the expect below would fire with a message reading "the floor
// is dead code" when the truth is "the boundary moved past the
// search". Keep the bound comfortably above the boundary.
if depth >= 8 {
break None;
}
src = format!(r"\frac{{{src}}}{{c}}");

View File

@ -454,6 +454,22 @@ mod tests {
assert_eq!(mixed.len(), 1, "{mixed:?}");
}
/// Round-3 F6 — a DOCUMENTED casualty of the `$$`-opaque rule, not a
/// guard failure: in `$a$$b$` the first span's legitimate closer is
/// immediately followed by the second span's opener, the lookahead
/// reads that pair as display-math `$$`, and the pending opener is
/// abandoned. Adjacent inline spans therefore need a separating
/// character. Pandoc finds two spans here; this scanner deliberately
/// finds none, because distinguishing `$a$$b$` from `$$x$$` requires
/// closer-context the framing's opaque-`$$` rule gave away.
#[test]
fn adjacent_inline_spans_are_eaten_by_the_display_guard() {
assert!(detect_math_spans("$a$$b$").is_empty());
assert!(detect_math_spans("$x^2$$y^2$").is_empty());
// One separating character restores both spans.
assert_eq!(detect_math_spans("$a$ $b$").len(), 2);
}
#[test]
fn whitespace_before_a_script_marker_still_merges_the_scripts() {
// F2: without skipping whitespace in `parse_scripts`, `x^2 _i` built