Commit Graph

1182 Commits

Author SHA1 Message Date
Levi Neuwirth 34f7913f9d
feat(protocol): one scroll classifier, so the frontends cannot disagree
First implementation commit for QoL Stage 3 (framing section 5d.6,
COHERENCE section 16).

ScrollPosition and a pure classify() land in pmacs-protocol. Not a wire
type, and deliberately not presentation either. The split: each
frontend computes its own local layout facts --- whether the buffer's
first or last row is on screen is a question only the frontend that
laid the text out can answer --- while the shared crate owns the
semantic decision those facts feed. Rendering to a string stays in each
frontend.

No wire message and no protocol-version bump. classify is a pure
function over values each side already holds.

Why it cannot take the existing four counts. format_scroll_indicator
derives EVERY branch from total_lines, and line wrapping leaves no row
total to give it: the GPU shapes only its viewport slice, so it cannot
count rows it never laid out, and computing a total arithmetically
would disagree with the break points cosmic-text actually chose.
Handing that signature byte counts instead would make

  view_top + visible >= total_lines

compare rows against bytes --- plausible strings, meaningless
arithmetic. So the mixing is not avoided here, it is unrepresentable:
two decided predicates and a byte pair, and no count of rows enters the
module at all.

The bug this forecloses is not hypothetical. pmacs-gpu depends on
pmacs-protocol and never on the pmacs lib, so the readout was
duplicated STRUCTURALLY --- once in src/editor.rs, once in
pmacs-gpu/src/main.rs, each with its own tests. During this lane's
review a fix landed in one copy while the other kept reporting "All"
for a wrapped one-line buffer. The GPU's own test pins the premise
today: format_scroll_indicator(0, 10, 1, 0) == "All", and a wrapped
single line still has total_lines == 1.

a_wrapped_single_line_is_top_not_all is that case, and it passes here
for a structural reason rather than a careful one: the classifier is
never told how many lines there are.

Five tests including degenerate totality --- empty buffer, cursor past
the end, u64::MAX offsets --- and a range sweep asserting Percent never
leaves 0..=100.

Gates: fmt, workspace clippy -D warnings, diff --check,
pmacs-protocol --lib 24/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-06 23:04:38 +02:00
Levi Neuwirth a03786c9c9
docs: the classifier belongs where the frontends cannot disagree
Section 5d.6 answered as (b): ScrollPosition and the pure classify go
in pmacs-protocol, string rendering stays in each frontend.

The reading that settles it is sharper than "shared vocabulary". Each
frontend computes its own local layout facts --- which rows are on
screen is a question only it can answer --- while the shared crate owns
the common semantic decision those facts feed. That is not presentation
leaking into the protocol crate; it is the DECISION placed where both
frontends are structurally unable to disagree, with rendering left
where it belongs.

Two properties bound the change: no wire message and no protocol
version bump, since classify is a pure function over values each side
already holds; and the one-copy-fixed defect from 5d.3 becomes
unrepresentable rather than reviewer-guarded, because there is only one
classifier to fix.

Q#LL8 approved. All eight questions answered. Implementation begins.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-06 23:02:40 +02:00
Levi Neuwirth 7b2f90c77e
docs: one signature cannot serve two contracts
Revision 16 said the scroll formatter could keep its signature and
change only what callers pass. It cannot, and the byte fallback in
revision 17 made that unavoidable rather than merely untidy.

Every branch of format_scroll_indicator derives from total_lines:

  total_lines <= 1                      -> All
  visible >= total_lines                -> All
  view_top == 0                         -> Top
  view_top + visible >= total_lines     -> Bot
  pct = (cursor_row + 1) * 100 / total_lines

Revision 17 removed the total. Passing byte counts makes the Bot branch
compare rows against bytes --- it would return plausible strings that
mean nothing. Passing any stand-in small enough to satisfy the guards
restores the false All this section exists to remove. And "local
predicates" is not something that signature can evaluate, because it
has no parameter for them.

Resolved by not asking it to. truncate calls the existing formatter
UNTOUCHED, with the same arguments in the same units, so its output is
byte-identical by construction rather than by assertion, and every
existing formatter test stays valid --- including the GPU's
format_scroll_indicator(0, 10, 1, 0) == "All", which correctly pins
line-space behavior. wrap calls a new classifier:

  classify(first_visible: bool, last_visible: bool,
           byte_pos: u64, byte_len: u64) -> ScrollPosition

All = first && last; Top = first && !last; Bot = last && !first;
otherwise Percent from bytes. No count of rows enters it, so the unit
mixing is not avoided but unrepresentable.

The actual mistake was trying to serve two genuinely different
contracts from one four-count signature. It could only do that by
making units implicit, which is how the contradiction arose. This is
5b.5's identity-case strategy applied to the indicator itself: the old
mode keeps the old code, and the new mode gets code shaped to it.

Leaves one question open. pmacs-gpu depends on pmacs-protocol only,
never on the pmacs lib, so format_scroll_indicator is duplicated
STRUCTURALLY and the classifier faces the same fork. Duplicating it
preserves exactly the condition that produced the earlier defect, where
one copy was fixed and the other was not. Sharing it through
pmacs-protocol makes the agreement structural rather than maintained,
which is the principle that chose byte-anchoring, additive sub_row, and
a content-derived cache key --- but it widens that crate from wire
vocabulary toward presentation, which is a COHERENCE section 16
layering question and not this lane's to settle alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-06 22:56:35 +02:00
Levi Neuwirth d6b5285234
docs: the GPU cannot count rows it never shaped
Revision 16 gave the GPU its scroll-indicator total on the premise that
"cosmic-text already knows each line's visual height". It does not.
rebuild_code_slice feeds cosmic-text current_text[vstart..vend] and
nothing else, because Session S1 found that feeding the whole rope made
large-file editing O(file) per keystroke. The GPU's layout holds the
viewport slice plus overscan; it cannot yield total visual rows, nor
the cursor's or top's row ordinal.

Re-shaping the whole document to recover them would reintroduce exactly
the cost that design exists to prevent --- for a status-line readout.

So the aggregate is abandoned rather than relocated. NN% is computed
from BYTE POSITION in both frontends; truncate keeps today's
visible-line percentage. No total, no cache, no invalidation.

Three reasons it is abandoned rather than approximated. Rows-per-line
could be computed as ceil(width / cols) without shaping, but
cosmic-text decides the real break points, so the number could disagree
with what is on screen --- the same approximate-parity trap Q#LL5
rejected for whitespace wrapping. Letting the TUI use row ordinals and
the GPU use bytes would show two percentages for one buffer, which is
this lane's own defect a third time. And All/Top/Bot are unaffected
either way: they are local predicates, they stay exact, and they are
the states a user actually reads.

This retires the cache from revision 15 and the fold-key correction
from revision 16. That correction was right for the design as it stood;
the design moved under it. Section 5d.2 is marked SUPERSEDED rather
than deleted, because "the key was fixed" and "there is no key" are
different states and a later reader should be able to tell which one
happened here.

Adds the large-file guard as a witness rather than an intention: an
indicator paint must leave the GPU's view_range and shaped_top
untouched, must not lay out beyond the viewport in the TUI, and
open_100mb_under_200ms must still pass with wrap as the default mode.
Without those, the decision is an unenforced comment and a later
"improvement" to a real row count would silently restore O(file) work.

Known imprecision, stated rather than left to be discovered: under
folds a byte percentage counts hidden bytes. Folding is TUI-only today,
and it matches Emacs. It belongs to the GPU folding lane, not this one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-06 22:51:03 +02:00
Levi Neuwirth b95506f6ca
docs: LL8 fixed one frontend and keyed a cache on the wrong state
Two corrections to Q#LL8, both found by review of 1c9ff6a, and both the
same shape: a fix that read as complete because it was correct in one
of two places.

The GPU has its own scroll indicator. format_scroll_indicator is
DUPLICATED, not shared --- src/editor.rs:5509 and
pmacs-gpu/src/main.rs:10114, each with its own tests --- and the GPU
passes current_line_starts.len(), a source-line count. So revision 15
would have fixed the indicator in the TUI and left the GPU reporting
"All" for a one-line wrapped buffer.

That is worth naming plainly: it is this lane's own defect, reproduced
inside the section written to close it. The lane exists because two
frontends disagree for reasons nobody chose, and the fix disagreed for
exactly that reason one more time.

Both copies keep their signature. The formatter is a pure function over
counts and is correct as written; what changes is what the callers
pass --- visual rows rather than source lines, visible rows rather than
visible lines. Every existing formatter test stays valid, including the
GPU's own format_scroll_indicator(0, 10, 1, 0) == "All", which pins
line-space behavior and must not silently change meaning.

The lazy total's cache key omitted fold state. Folds are built per
rendered window and can be collapsed or expanded with no edit, no
resize and no mode change --- so all three of revision 15's key
components sit still while the projection underneath them moves. Cache
NN%, toggle a fold, and the stale total is served for the new
projection.

Now keyed on (buffer generation, content width, mode, fold projection),
and specifically on the projection's own components rather than a
revision counter on the fold registry. Components is one entry per
collapsed region, so comparing it is O(folds), and the key IS the thing
it guards --- it cannot be forgotten. A maintained counter can, on
every present and future mutation path, which is the same
did-you-remember hazard as LL7's buffer-switch trigger. Same principle
as byte-anchoring over a row index and additive sub_row over redefining
row: self-validating beats maintained.

Content width, not window width. Wrapping happens in the text area, and
the gutter's width changes at the line-count digit boundary (9 -> 10,
99 -> 100) --- something the GPU's sync_buffer_dimensions comment
already records for its own shaping. Keying on window width would serve
a stale total across that boundary.

Three witnesses added: the reported case in BOTH frontends, a "cache,
then toggle a fold" case that fails against revision 15's key, and a
digit-boundary case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-06 22:47:14 +02:00
Levi Neuwirth 1c9ff6a033
docs: the GPU had no wire, and "everything is local" was false
Two holes in revision 14, both found by review of this lane's own first
commit, and both would have been discovered during implementation at
much greater cost.

Q#LL7 --- the mode never reached the GPU. Section 4 resolves
ui.line-wrap into Viewport, which reaches the GRID renderer. The GPU is
not a grid consumer: it lays out locally, ignores CellDelta,
BufferSnapshot carries only CRDT bytes, and no InstanceMessage variant
expresses a wrap mode. So truncate would have changed the TUI and left
the GPU wrapping --- the two frontends still disagreeing, which is the
one thing this lane exists to fix. Q#LL5's "character wrap in both" was
equally unreachable: setting Wrap::Glyph at startup is not honoring a
mode that can change.

Specified as an additive variant at v22, appended after the current
final variant with the advertised baseline left at 20 --- the path
FontFacts took at v17 and the panel shapes at v21, and the baseline
constant's own doc reserves moving it for changes that cannot be
expressed additively. This one can.

It carries buffer_id, and is resent on attach, on config change, AND on
buffer switch. The third trigger is the one a FontFacts-shaped design
misses: font size is global, wrap mode is per buffer, so switching from
a truncate buffer to a wrap buffer changes the effective mode with no
config event at all. A design listening only to on_change is silently
wrong and passes every single-buffer test.

Q#LL8 --- the scroll indicator falsifies the locality claim. Revision
14 asserted every vertical consumer is local. format_scroll_indicator
is not: a one-line buffer wrapping to fifty screen rows has
total_lines == 1, so the first branch returns "All" while forty-nine
rows sit below the viewport. The indicator claims the whole buffer is
visible when almost none of it is.

The claim is narrowed rather than abandoned, because the distinction
that bounds the cost survives: a TOTAL is one number, lazily computed
and cached; a PREFIX-SUM INDEX is O(N) resident storage. Stage 3 needs
the first and still does not need the second. All, Top and Bot need no
aggregate at all --- each is a local predicate falling out of the
render walk --- so only NN% pays, which matters because the M1 gate
measures open time on a 100MB file.

Also fixes a notation hazard. Section 7 said pos_to_display returns
"the visual row", which reads as redefining row --- the exact thing
5b.5 forbids, stated two sections apart. Every wrap-point example is
now the explicit triple {row, sub_row, col}. Writing them out makes the
point visible: both coordinates at a soft break share the same row,
because a wrap does not cross a source line. That is the information a
redefinition would have destroyed, and the pair notation hid it.

Status returns to not-approved. Both questions change what gets built,
not how, so implementation waits on them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-06 22:38:51 +02:00
Levi Neuwirth bd752f242b
docs: frame QoL Stage 3, long lines
A line wider than the window cannot be read past the edge in the TUI.
The GPU is not in that state --- it already wraps --- so the
cross-frontend defect is not unreadability. It is that NEITHER behavior
was chosen: the TUI truncates because a cell walk breaks at max_cols,
the GPU wraps because cosmic-text's Wrap::WordOrGlyph default was never
overridden. Two accidents that disagree, and no way to state a
preference in either.

Eleven revisions of review, and the corrections were the substance.
Revision 1 claimed both frontends render from the same CellGrid, which
inverted the entire cost model --- pmacs-gpu ignores the grid variants
and lays out locally, and its own comment says so. Later rounds caught
an impossible round-trip invariant, a "collision" between two
coordinates that were one position, an off-grid argument against a
function that has no grid, a false binary for view_top, and a
renumbered visible-line space that does not exist. Each is recorded
with its reasoning rather than quietly fixed, because the pattern ---
an assertion that reads as precise while resting on something
unverified --- is more useful to the next reader than any single fix.

All six questions are answered and the framing is approved:

  LL1  wrap + truncate, default wrap; horizontal scroll is Stage 4
  LL2  buffer-local mode, resolved into Viewport like folds
  LL4  do not adopt editing.fill-column; ours is ui.line-wrap
  LL5  character wrap in BOTH frontends
  LL6  no global map; byte-anchored view_top; additive DisplayCoord

Two of those deserve to be found later rather than discovered:

GUI users lose word wrap. Character-wrap parity is cheap, true, and
matches Emacs, whose default wrap is also a character wrap. But the GPU
has word-wrapped since it existed and nobody opted into losing it. The
alternative was a UAX #14 dependency, because a whitespace-based grid
wrap would give only APPROXIMATE parity against cosmic-text's Unicode
line breaking --- which is worse than honest divergence, since it looks
unified until it is not.

The audit strategy is asymmetric deliberately. The coordinate functions
gain a required context parameter so the compiler enumerates every call
site; DisplayCoord gains an additive sub_row so untouched consumers
stay CORRECT rather than merely findable. Compiler-enforced where
enforcement is possible, correct-by-default where it is not.

Also corrects two ledger headers that still said OPEN for PRs merged
earlier today (#218 at 09:59Z, #217's absorption; #219 at 13:41Z).
Neither lane is removed --- rule 4 removes a lane when its ARC is done,
not when a PR merges, and the QoL arc has this stage left. Read each
block before cutting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-06 22:29:54 +02:00
Levi Neuwirth 95d17a11ea
docs: a tip SHA goes stale in the commit that writes it
The zoom lane recorded `1e054f7` as the authoritative tip. Committing
that line advanced the tip to b645fe7, so the claim was false before it
was pushed --- self-invalidating, not merely out of date.

The ledger already says this, twice: the docs-absorption and
signal-integrity lanes both note that any edit to their block advances
past whatever SHA it records, so the REF is the thing to trust. I wrote
a new lane without following the convention the file states about
itself.

Removes the SHA and says why, so the next lane inherits the reason
rather than the rule alone.

The base SHA stays. 218d2e7 is a merge commit that already exists and
nothing here can move it --- it is a fact about where the branch
started, not a claim about where it is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-06 17:55:48 +02:00
Levi Neuwirth b645fe7f3b
docs: the zoom lane recorded revision 4 and three findings
The lane went stale across two review rounds. It said framing revision
4, 13 acceptance tests, and "the three findings review caught" --- all
true when written, none true now.

Brings it current: revision 5, the pushed tip, 15 tests, and the two
later findings. The quantization one gets a real entry rather than a
mention, because its content is a REUSABLE fact about this codebase ---
ConfigKind::Number validates finiteness and bounds and nothing else,
and on_change cannot veto --- so any future setting needing a stronger
predicate than a range meets the same wall.

Also records the verification asymmetry that mattered: the raw-step
bite fails the new witness while the pre-existing 0.37 test still
passes. That is why it had to be a separate test, and it is the kind of
detail a lane summary usually drops.

New section for pre-checkout CI reds, which #220 hit three times. The
job dies in "Set up job" before actions/checkout; grepping the full log
for checkout/cargo/test-result returns 0. No code is fetched, so the
red says nothing about the commit in either direction --- re-running it
is a first execution, not a retry-to-green.

It gets a section here and not a registry row on purpose: matching
requires an exact test selector plus fragments, and no test ran. Making
it a row would mean deforming the row shape. Whether the registry wants
a non-row section for the class is left to its owner.

Written down rather than acted on silently because the registry's
opening instruction is to read it BEFORE attributing a red run to the
environment, and that is exactly the attribution being made here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-06 17:52:11 +02:00
Levi Neuwirth 1e054f7109
docs(zoom): the ties both round up, they do not oppose
Review caught the explanation of the 0.015 round-trip break, not the
fix. Three copies of it claimed 16.015 rounds up while 16.005 rounds
down --- "opposite directions". Both round UP.

Verified rather than reasoned about: at the point the quantizer sees
them, 16.015 * 100 is exactly 1601.5 and 16.005 * 100 is exactly
1600.5. Both are exact ties, and half-up sends both away from zero.

So the mechanism is not opposed rounding, it is that half-up is not
symmetric under negation. Rounding up on the way in adds half a
centi-pixel; rounding up on the way out adds another, so the two
errors ACCUMULATE instead of cancelling, and 16.00 -> 16.02 -> 16.01
ends one centi-pixel high. "Opposite directions" would have predicted
them cancelling, which is the reverse of what happens.

Corrected in all three places that carried it: the module comment, the
test's doc comment, and framing section 3.2.

Comments only --- no behavior change, and the witness values in the
tests were already right. Zoom suite still 15/15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-06 17:50:03 +02:00
Levi Neuwirth 828f57debb
fix(zoom): quantize the step, or the round-trip guarantee is false
THE BOUNDS WERE NOT SUFFICIENT AND THE TEST COULD NOT SEE IT.
`ConfigKind::Number` validates finiteness and bounds and nothing else,
and `on_change` listeners are notified after a value is stored — they
cannot veto. So 0.015 is a perfectly settable step and nothing in the
registry can refuse it.

Used raw it breaks the framed guarantee, because each operation rounds
independently and 16.015 and 16.005 round in OPPOSITE directions:

  step 0.015:  16.00 -> 16.02 -> 16.01     broken
  step 0.37 :  16.00 -> 16.37 -> 16.00     holds

The existing round-trip test used 0.37 — centi-pixel representable — so
it passed against the defect. Bitten now: with the raw value the new
case lands on 16.01, while the 0.37 case still passes, which is exactly
why it needed to be its own witness.

QUANTIZED WHERE USED, not at `set`. Sizes live in integer hundredths
end to end and `validate_font_size` already range-checks the original
and then rounds to the nearest hundredth; rounding the step is that
same operation one level up. A step of 0.015 is not a finer step in
this domain, it is 0.02 written imprecisely.

Enforcing at set time was considered and rejected: the registry cannot
express a precision constraint, and a validating wrapper is bypassed by
a direct `pmacs.config.set` — the seam `autosave` already documents
about its own interval_ms wrapper. Quantizing at the point of use
cannot be bypassed. Both descriptions say "quantized to hundredths", so
`describe-setting` shows it.

The framing header also still said "proposed, awaiting approval" while
the lane and this PR recorded it approved and implemented. Revision 5,
with §3.2 recording the gap and why quantization rather than rejection
closes it.

Verified: fmt, clippy, diff-check, --lib 1900/0, gui_zoom 15/15.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:24:56 +02:00
Levi Neuwirth 8fd8f585aa
docs: the zoom lane records PR #220
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 16:22:05 +02:00
Levi Neuwirth aa99ab39d2
feat(zoom): GUI zoom over the font preference that already existed
Ctrl +/- had no effect whatsoever in the GPU frontend. Stage 1 (#219)
fixed what zoom did to the TUI; this is the other half.

NO RENDERING WORK. FontMetrics::scale already derived every GUI
dimension — code size, line height, status band, divider, menu rows,
minibuffer dropdown, gutter advance — and apply_font_facts already
re-metriced all seven buffers in one transaction. This drives the
preference that existed: two settings, three commands, and a restore.

Q#Z1 = (c). Relative zoom needs an origin and the daemon is built never
to know one — font_pref.rs is explicit that it "never learns metrics,
advances, or what resolves". Hardcoding 16.0 would put a pixel constant
on the daemon side; always sending a size would destroy the `None`
state for everyone who never zooms. A configured base is the only
option where the daemon still infers nothing, and the untouched path
stays byte-identical.

THREE THINGS REVIEW CAUGHT THAT REVISION 1 HAD WRONG.

Q#Z3 was not implementable as framed. `keymap_stack::Scope` is
Buffer | Mode | Global and carries no frontend identity, so "bind on
GPU frontends only" does not exist; and FrontendEvent has no
command-invocation variant, so the GPU cannot ask for a command by name
either. A global binding would capture the chord in the TUI and take
away the terminal's own zoom — the very thing the user is pressing it
for. Commands ship; the binding waits on capability-aware keymap
resolution, which is now a named follow-on rather than something
smuggled in here.

The restore seam did not exist. Builtins and init.lua both run BEFORE
install_state_dirs, so a pmacs.state.read at module load returns
nothing, always. saveplace and recentf never meet this because both
read lazily inside functions; zoom must apply with no user action,
which makes it this project's first eager state consumer. Restore lives
at the end of install_state_dirs — by definition the moment state
becomes readable, so it cannot be ordered wrongly and a future third
startup path gets it without knowing it had to ask.

Every size write clobbered the family. set_font replaces both fields
unconditionally, so { size = n } alone silently cleared a configured
family until restart.

BITTEN, THREE WAYS. Dropping family preservation fails 3 tests.
Reverting to the framing's own first parser `^(%d+)$` fails 4 including
the seam restore — it anchors to end-of-subject and rejects the
newline-terminated file the writer emits, which is the contradiction
review caught in the framing before it reached code. Hardcoding the
16.0 origin fails the base test.

Also recorded: a loaded crdt run failed two m6_1 PTY tests with
`stty -a output was: ""`. That is R4/R6's empty-content readiness
family, and it means the readiness-helper audit's scope is wider than
three wait_for_file copies under tests/ — src/process.rs's own tests
carry the shape. Undiagnosed, load-sensitive, green isolated and on a
quiet full run; a scope note for that lane, not a registry row, since
the registry judges red CI runs and these were local.

Verified: fmt, clippy, diff-check, --lib 1900/0, crdt 2085/0,
gui_zoom 13/13, journey 47/0, m4 150/0, gpu 221/0, full_grid 1/1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 16:20:46 +02:00
Levi Neuwirth 218d2e7acd
Merge pull request #219 from levineuwirth/full-grid-resync
fix(frontend): honor full_grid so a resize blanks before repainting
2026-08-06 13:41:32 +00:00
Levi Neuwirth 6bffa52064
docs: the framing said "awaiting approval" after being approved
Two places, not one. The status line said proposed, and §3 still said
Q#FG1 "needs approval before implementation" — while the lane, the
branch, and PR #219 all record it decided as A. A framing that
survives the lane is the durable record; one that describes its own
state wrongly is worse than no record, because it reads as authoritative.

Also records what implementation corrected about the framing rather
than leaving §5.2 describing a witness that was not built: the
time-based settle it specified cannot work, since a settled pmacs
screen emits per-frame bytes forever and "output stopped growing" never
becomes true. The shipped test anchors to content instead, which
excludes startup's clears by construction rather than by timing — and
its repaint-ordering assertion has to be scoped after the new clear,
because the fixture repeats its marker and the suffix opens with the
tail of startup's own frame.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 15:22:03 +02:00
Levi Neuwirth 3361da08df
docs: the full_grid lane records PR #219
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 15:08:45 +02:00
Levi Neuwirth 899aaf2249
fix(frontend): honor full_grid — the flag existed and nothing read it
Zooming a terminal with Ctrl +/- left the TUI showing the previous
frame through the new one. Q#FG1 = A, as approved.

THE RULE WAS ALREADY WRITTEN DOWN, ON A PRIVATE FIELD.
src/instance_render.rs:36 says remote frontends "must blank their local
buffer before applying the deltas" — the binding contract, in the one
place a consumer author will never look. The protocol type said only
that full_grid marks "the initial sync ... versus an incremental
frame": a label, from which no obligation follows. So FG-INV now lives
on InstanceMessage::CellDelta, where whoever writes the next frontend
reads it. A resync is a picture of the screen's INK, not of the screen.

The producer diffs against a blank grid, so a cell that should be blank
produces no span. src/frontend.rs then took `CellDelta { spans, .. }`
and discarded the flag. That was correct for exactly one frame — the
fresh-attach frame, which follows Frontend::new's Clear — and wrong for
every resize after, which follows nothing. A font-size change is the
worst case because the terminal reflows in place rather than dropping
content, so the maximum number of stale glyphs survive.

emit_cell_delta joins emit_span and emit_status_overlay as a pure
helper over a writer; apply_message routes through it. No struct
change, no generic parameter, no new pattern.

WHY SEVEN TESTS MISSED IT. Every one asserts the producer SETS the
flag; none asserted a consumer ACTS on it, and no runtime reader
existed workspace-wide. "Add a test for the flag" had already been
done and did not help. Handoff §5's enforcement-vs-documentation drift,
in a second register.

Three unit witnesses, each bitten independently. The empty-spans case
earns its own test rather than folding into the others: under the
plausible `spans.is_empty()` early return the ordering test still
PASSES and only that one fails — and an empty resync is exactly the
frame whose entire content is the blanking.

The PTY acceptance drives a real SIGWINCH, and its mark is anchored to
CONTENT rather than time. A time-based settle was written first and is
unusable: a settled pmacs screen emits per-frame bytes forever, so
"output stopped growing" never becomes true. Anchoring just past the
first painted byte excludes both startup clears by construction —
Frontend::new clears before any frame exists, and the first frame is
itself a resync whose clear precedes its own spans. Bitten against the
original defect: 34,831 bytes after the first painted frame, no CSI 2 J
anywhere in them.

What it does not prove, stated here rather than found in review: the
suites assert on raw bytes, with no screen model and no vt100/termwiz/
vte dependency. This shows pmacs emitted a blank at the right moment,
not that the screen ended correct.

Verified: fmt, clippy, diff-check, --lib 1900/0, crdt 2085/0, m4 150/0,
gpu 221/0, and the grid-driving suites — full_grid_resync 1/1, vterm
1/2/3 9+9+5, m5_5 15, m5_8 5, bottom_panel_stage1 47.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 15:05:59 +02:00
Levi Neuwirth da56becb3b
Merge pull request #218 from levineuwirth/docs-absorption-217
docs: retire the tree and macOS-CI arcs, and file two unrecorded reds
2026-08-06 09:59:04 +00:00
Levi Neuwirth b172347945
docs: four review defects, and a documented error that never happens
1. THE CENTRAL RULE LACKED ITS TIME QUALIFIER. It said any red matching
a retired row is a recurrence — under which this PR's own R2 finding
reopens R2. Now: a match POSTDATING the retirement challenges the
disposition; a match predating it corroborates. That is not a
technicality. An occurrence scan reaches backwards by construction, so
most matches it finds are the earlier kind, and the old wording would
reopen every retired row the first time anyone scanned.

2. R5 AND R6 SAT UNDER "RETIRED ROWS" while declaring themselves live
and undiagnosed. I inserted them before R2's heading, which put them in
the wrong section — presentation contradicting classification in a file
whose whole job is classification. Moved under Live rows.

3. RECOVERY ANCHORS STILL DECLARED 12f2970, so the check accepted a
checkout lacking #216 and #217 while the same file described both as
complete. Advanced to db1bbe9, with the ancestry verified rather than
assumed.

4. THE HANDOFF'S DURABLE TREE FACTS STILL SAID IDS ARE OPAQUE AND
COMPARED BY EQUALITY — the contract ef99b64 deliberately narrowed. A §5
lesson explaining a correction does not fix a summary that still states
the uncorrected fact; the summary is what a new agent reads first.
Corrected there and in the header, which also still called Stage 2 in
flight and anchored main at f186253.

AND ONE FINDING FROM RE-EXERCISING THE RECOVERY PATH RATHER THAN
SWAPPING ITS SHA. This file claimed `git worktree add <path>
githubsucks/<branch>` fails with "fatal: invalid reference". It does
not fail. On git 2.55.0 it SUCCEEDS and leaves a detached HEAD — no
branch, no upstream.

Still use -b, but the reason is the opposite of the one recorded: the
hazard is not an error that stops you, it is that nothing stops you.
Work committed there sits on no branch and is not pushed by a bare
`git push` — the "uncommitted work does not travel" hazard wearing the
shape of committed work. A documented error message that never appears
is worse than no documentation, because the reader waits for a signal
that is not coming.

Verified: fmt, diff-check, --lib, listview 26/26. Recovery path re-run
from an empty directory at the new base; all four steps clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:40:54 +02:00
Levi Neuwirth d4d7ea605c
docs: the absorption lane records PR #218
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:18:43 +02:00
Levi Neuwirth d4931acba6
docs: this lane's own block, written before its PR
The ledger requires a lane for every open PR. #171 drifted 153 commits
while invisible here and #215 had no lane until review caught it, so
the block goes in with the lane's own commits rather than after someone
asks for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:18:05 +02:00
Levi Neuwirth 2c59fac510
docs: retire two arcs, and file two reds the scan turned up
RULE 4 REMOVES A LANE AFTER MERGE, and two were overdue: the tree
primitive (#217) and macOS CI signal integrity (#215, #216).

The macOS arc could not simply be deleted. It owned R1 and R3, and a
lane removed while it still owns undone work does not close that work,
it hides it. R1 goes to a new async-runtime block; R3 goes to the
reap-ledger lane, which already parks every disposition change pending
exactly its question. Re-homing first is why this did not happen at
merge.

AN OCCURRENCE SCAN (last 25 main runs: 23 green, 2 red) found both reds
unrecorded, and I misattributed both on the first pass — by theme
rather than by required fragment, which is the exact error the registry
exists to prevent.

- Run 30710662474 is NOT R3. Same test, same EPERM, same
  measured_group=unobservable(ESRCH...) — but R3 requires `leader=live`
  and this reads `leader=exited(signal SIGUSR1)`, R2's exact fragment.
  It is a second R2 occurrence, four days BEFORE R2's retirement, so it
  corroborates the row rather than falsifying its disposition. It also
  adds something: macOS luajit where R2's evidence was lua54, so the
  mechanism was never flavor-specific. Filing it as R3 would have
  attached a live possible product defect to an occurrence of a fixed
  test race.
- Run 30555667095 is NOT R1. Different test, different module,
  different assertion; they share only "supersede under a deadline on
  macOS". Sharing a subject is not sharing a signature. Filed as R5,
  undiagnosed.

R6 is the acc28 readiness timeout from #217's CI. Its scope is the
AUDIT, not the call site: three independently written readiness helpers
now exist and they disagree, with bottom_panel_stage1's carrying only
the zero-byte half of #216's hardening. R4's disposition predicted this
recurrence under a new selector. Patching acc28 alone repeats the
mistake this arc already made once, when the empty-file predicate was
fixed in one helper and left in its neighbour.

The registry carries a rate for the first time — a floor, not a
measurement: main only, 25 runs, readable reds only.

Four durable lessons to the handoff, two from the tree review: an
optional field the shape depends on is not optional, and a contract two
mechanisms must honour is only as strong as the weaker one.

Verified: fmt, diff-check, --lib, listview 26/26. Docs-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:17:46 +02:00
Levi Neuwirth db1bbe94a6
Merge pull request #217 from levineuwirth/tree-primitive-framing
feat(listview): the tree primitive — optional depth/id, primitive-owned folding
2026-08-05 22:15:20 +00:00
Levi Neuwirth ef99b64f95
fix(listview): ids must also be unique and not NaN
The scalar contract said "identity" and enforced only "scalar", so two
ways to hold an id that is not one survived.

NaN passes `type(x) == "number"` and then errors at
`p.collapsed[row.id]` with "table index is NaN" — the one scalar Lua
accepts as a number and refuses as a key. Bitten with the check
removed, it reports exactly that, from inside listview, naming no row.

DUPLICATES do not merely collide. Every lookup here — `line_of_id`, and
toggle's scan for the row index — resolves an id to the FIRST row
bearing it, so selecting the second such row toggles the first and
re-seats the cursor onto it: a stray jump with nothing pointing at the
id. Bitten with the check removed, nothing is raised at all.

Both are enforced in `check_ids`, where rows already enter, so the
error names the offending row (and, for a duplicate, both of them)
instead of surfacing as a low-level error or a wrong jump later. The
error text says why, not just what, since the reason is not guessable
from the rule.

Verified: fmt, clippy, diff-check, --lib 1897/0, crdt 2082/0, listview
26/26, m4 150/0, gpu 221/0, bottom_panel_stage1 47/47.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 23:06:25 +02:00
Levi Neuwirth 7e27de63d8
fix(listview): item was load-bearing, and the id contract was two contracts
TWO REVIEW FINDINGS, both real, and neither reachable from the existing
tree tests.

1. `item` WAS EFFECTIVELY REQUIRED. `render` writes `line_to_item[n] =
row.item`, so that map is SPARSE whenever a row omits the optional
`item` — and `seat_cursor` took `#` of it. A display-only tree (a
grouping node with `on_visit` unused, which the API explicitly allows)
made that length 0, so the cursor never left the header, TAB found no
row, and folding was unusable. It now counts visible rows explicitly.
The old tests could not catch this because every one of them supplies
`item`: under the reverted fix `tr_5` fails `left: 0 / right: 1` while
`tr_1` still passes.

2. THE ID CONTRACT WAS TWO CONTRACTS. The docs said "opaque, compared
by equality". Selection does compare with `==`, honouring `__eq` — but
collapse state stores ids as TABLE KEYS, and Lua indexes tables by raw
identity, consulting no metamethod. So a table id would satisfy one
half and quietly fail the other: after a refresh minted fresh id
tables, the cursor would be restored and the fold silently lost. A
divergence that shows up as a missing fold, arbitrarily later, with
nothing pointing back at the id.

Narrowed rather than generalized. Equality-aware collapse lookup is the
alternative and it is worse: `hidden_by_ancestor` runs per row, so it
turns a linear render quadratic to support a key type no consumer has
asked for. The contract is now the one both halves can honour — string
or number, compared by value — enforced by `check_ids` where rows enter
(`open` and `refresh`), so a bad id is a named error at the call site
instead of a lost fold much later. Q#TR3 in the framing records the
narrowing and why.

Verified: fmt, clippy, diff-check, --lib 1897/0, crdt 2082/0, listview
24/24, m4 150/0, gpu 221/0. Both fixes bitten independently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 18:06:11 +02:00
Levi Neuwirth 0055a34867
docs: a second local occurrence of the composition-overhead red
It went red again in the full crdt lib run at the merged tip, this time
at 10.3% — clearing the 10% budget by 0.3 points. Ten isolated runs are
now green at -2.3% to +1.5%, and two full-suite runs at the same tips
were green too.

Still recorded as measurements rather than a cause. Two reds against
two greens in-suite is intermittence, not a mechanism, and isolated
greens reproduce nothing about a load-sensitive failure. The one thing
the second occurrence does add is that the budget is marginal rather
than comfortably clear.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:46:32 +02:00
Levi Neuwirth 3f6a2837de
Merge githubsucks/main into tree-primitive-framing
#216 landed while this branch was open. Both conflicts are in the
ledger and the handoff, and both sides had independently written up the
same shared-CARGO_TARGET_DIR hazard.

THE TWO WRITE-UPS ARE NOT ABOUT THE SAME OCCURRENCE, and merging them
carelessly would have been a real error. Stage 2's is established: seven
failures against a clean baseline, failure text naming its own cause,
pgrep confirmation, and a dedicated-target-dir re-run at 41/41. This
lane's is the one whose signatures were destroyed before being read —
it has no captured text to match against Stage 2's, and it keeps two
non-causal hypotheses. A mechanism established in one occurrence is not
evidence about a different occurrence that was never characterized, so
the merged bullet says so explicitly rather than letting proximity
imply it.

The ledger records #216 merged and stops saying Stage 2 is in flight.
It does NOT retire the arc, though rule 4 now would: R1 belongs to the
async-runtime lane and R3 is an unresolved possible product defect for
the process-signal lane, and neither has a block yet. Re-homing them is
an absorption pass — not something to fold into a feature PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:42:03 +02:00
Levi Neuwirth 2657568ade
Merge pull request #216 from levineuwirth/ci-signal-hardening
CI signal hardening — retire R2 and R4 with discriminating witnesses
2026-08-05 15:39:28 +00:00
Levi Neuwirth f36b1fcf20
docs: the tree lane records PR #217
The ledger requires a lane for every open PR, and this one described a
hold that has ended.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:29:27 +02:00
Levi Neuwirth d92f0ad4aa
docs: the tree lane was calling a pushed branch unpushed
It said "Unpushed while held" while githubsucks/tree-primitive-framing
sat at the same tip as HEAD. Held means no PR is open; it does not mean
the work is stranded locally, and the handoff's portability rule cares
about the latter.

The commit list also claimed to be exhaustive and then excluded the
commit that updated it — a list of that shape is wrong the moment it is
written. It is now the substantive arc, with `git log` named as the
place to get the complete one, and the remote ref rather than a pinned
SHA named as the authoritative tip: any edit to this lane advances past
whatever SHA the lane records, including this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:23:22 +02:00
Levi Neuwirth 62993e068f
docs: pin the tree lane to the review-round commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:17:41 +02:00
Levi Neuwirth c59de959e7
fix(listview): flat panels keep their TAB, and a selection test that bites
FOUR REVIEW FINDINGS, and the first two were right about the tests.

1. THE SELECTION ACCEPTANCE WAS VACUOUS. `tr_1` toggles the selected
root, which sits on line 1 before and after collapsing — so it passes
unchanged under the line-based re-seating that id-keyed re-seating was
built to replace. It proves collapse hides descendants; it proves
nothing about selection. `tr_4` adds the case that discriminates: an
`on_refresh` inserts a row ABOVE the selected node, so the node moves,
and the assertion is that selection follows the NODE. Bitten by
restoring `seat_cursor(p, saved)`: `tr_4` fails with left "  kid2",
right "sibling", while old `tr_1` passes — which is the finding,
reproduced.

The substantive assertion is deliberately ordered first. It was second
at one point, behind the fixture check that the node moved, and a
regression then reported as "the insert must move the selected node" —
reading like a broken fixture rather than a broken re-seat.

2. FLAT PANELS WERE NOT BEHAVIOUR-IDENTICAL. `bind_local_keymap` binds
TAB on every listview, so a depthless panel that previously fell
through to the global binding — and to Q#P3's read-only intercept —
began answering "listview: no node here". `listview.toggle` now
delegates to `buffer.tab` when no row carries an id, restoring the
prior path exactly; leaf feedback is kept for panels that really are
trees. `tr_3` asserts the absence of both tree messages rather than
merely that the panel still renders.

3 and 4 are documentation. The lane now lists 38e94dc, and no longer
says the PR is held "pending review of the documentation" that the same
commit supplied — it is held pending the decision to open it. §20 said
to BUILD the tree primitive while §14 already carried ◐; it now says
what actually remains, which is adoption: dired's `i` is the next
constraint source, DAP's variables view is why this was worth building
before them.

ONE RED, CLASSIFIED RATHER THAN RERUN AWAY. The crdt lib gate failed
`composition_overhead_under_ten_percent` at 30.7%. It is an incumbent
handoff hazard, and the branch cannot reach it — the diff versus main
touches no src/, no crate, no manifest. Alone it ran 5/5 green at
-0.6% to +0.2%; the next full run was green. Recorded in the handoff as
a MEASUREMENT, not a cause: five isolated greens establish that the
ratio is nowhere near the threshold when alone, not that contention is
what pushed it over. Not a registry row either — that file judges red
CI runs, and this was local.

Verified: fmt, clippy, diff-check, --lib 1896/0, --lib --features crdt
2081/0, listview 22/22, m4 150/0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:17:33 +02:00
Levi Neuwirth 38e94dc33f
docs: give the tree lane its entry, and move Tree off ✗
Three updates, one of which was a broken cross-reference of my own
making.

THE DAEMON-LEAK ENTRY POINTED AT A LANE THAT DID NOT EXIST. It said the
unclassified failure was "recorded in the tree lane below"; this branch
had no tree lane. A pointer to nothing is worse than no pointer — it
reads as though the record exists and sends the next reader looking. The
tree lane is added, and since it now sits ABOVE that entry the direction
is corrected too, with a second pointer to the framing §6a where the
occurrence is recorded in full.

The lane carries branch, base, framing revision, every commit, the gate
table with both sweeps and their exact reconciliations, the bite
verification for both behavioural claims, the held-PR state and the
recovery command. It also names what is NOT in scope, because the four
unadopted §14 consumers and dired's `i` will otherwise read as omissions
from this stage rather than as later ones.

COHERENCE.md §14: Tree moves ✗ -> ◐, implemented with ONE consumer. Not
✓, and the row says why: the LSP outline is the only adopter, dired's
`i` remains the deferral in its §13, and the other four named consumers
have not adopted. The organising fact goes in the section rather than
only in the framing — folding is LOCAL PROJECTION STATE, NOT A REFRESH
PROTOCOL, which is why a consumer with no `on_refresh` can fold at all.
The §0 scorecard row moves with the body; a grade table disagreeing with
its own section is the defect this document keeps correcting in others.

The framing's status moves from "approved" to implemented-and-gated,
held for PR review, and four durable facts go to the handoff §1: folding
as projection state; identity being consumer-supplied and compared by
equality, with `line:col` chosen because the `::` parent chain collides
on overloads; `has_children` having to read the full row array rather
than the rendered subset, since a collapsed node's children are absent
from it by construction and the bug would look like fold working and
unfold silently not; and that a bite which passes validates the pair
rather than the test.

The daemon leak stays a separate reap-ledger candidate. It is not tree
scope, it predates this work, and folding it in would make a lane
responsible for a leak it did not cause.

Verified: fmt, diff-check, listview 21/21, --lib 1896/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 16:50:29 +02:00
Levi Neuwirth 8f64c3b2a1
test(listview): byte-identity for the flat consumers, and two findings
ACCEPTANCE 5, and it needed a real test rather than a weakened claim.
`listview_acceptance` says in its own header that the references panel
"needs a live LSP and is validated manually / via the m4 harness", so it
does not exercise `*references*` at all; the m4 hover test asserts
content PRESENCE, not exact output. Neither would notice a flat consumer
silently gaining an indent column — the regression a tree extension can
introduce. So the coverage is written against the real entry points
through the fake language server.

`*references*` is pinned EXACTLY: the row is the location string and
nothing else. `*lsp*` formats its own two-space indentation, so
"starts with a space" is not a violation there; what must hold is that
the primitive reproduces the consumer's text verbatim, matched as a
WHOLE LINE — a substring would still be found inside a further-indented
copy of itself. Volatile parts (pid, elapsed) are deliberately excluded,
the same normalization reasoning the CI registry uses.

THE FIRST BITE PASSED, AND THAT WAS THE FINDING. Injecting
`string.rep("  ", row.depth or 0)` did not fail the test — flat rows
carry no depth, so it added nothing. I had simulated a regression the
flat path is immune to and would have recorded the test as verified.
The regression this criterion actually guards is an UNCONDITIONAL
column, a fold gutter on every row; with that injected the test fails on
"the flat references row renders verbatim". A bite that passes validates
the pair, not the test — and injecting the wrong defect teaches nothing
while feeling like assurance.

A VERIFICATION RECORD, including one unclassified occurrence. The first
local crdt sweep of this branch reported 7 failures and its SIGNATURES
WERE DESTROYED before being read, piped through an aggregation that
emitted only totals. That is the failure the CI registry exists to
prevent, committed one lane after writing it, and it is why the cause
cannot now be established rather than merely being unknown.

It is recorded in this lane's own framing and deliberately NOT as a
registry row: that registry keys on a normalized signature, and an
occurrence with none would be granted a recognisability it cannot
support — the same reasoning that made the unevidenced incumbents audit
notes rather than rows.

Four re-runs are tabulated with what each supports. Two were not
isolated, including one where my own guard printed "aborting" and did
not abort. TWO GENUINELY ISOLATED RUNS ARE BOTH CLEAN, which supports
repeatability under isolation and establishes nothing about the cause.

Two mechanisms are recorded as NON-CAUSAL hypotheses, because both were
present and neither can now be tested: a shared CARGO_TARGET_DIR (whose
reciprocal case another lane observed independently, with `pgrep`
evidence and failing text that named its own cause), and ~40 resident
leaked daemons. Having two plausible mechanisms and no way to
discriminate IS the result; naming either would repeat the reasoning
this project has rejected — concluding something about an occurrence
from something that was not about that occurrence.

Both mechanisms are recorded as standing hazards in the handoff, and the
daemon leak gets its own candidate lane: 42 orphans, oldest four days,
reparented to systemd with deleted sockets, from
`gpu_invocation_acceptance`'s one-command tests, leaking 3-4 per sweep
as measured rather than estimated. It predates this work and belongs to
the reap-ledger family — a process outliving its supervisor with nothing
watching it — but the existing ledger arms only for `spec.group` and so
does not cover it.

Verified: fmt, diff-check, luajit sweep 3453/0 and crdt 3722/0, each
exactly +4 on its baseline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 16:31:31 +02:00
Levi Neuwirth 8ab20b5d68
docs(ledger): the second CI checkpoint, and the row a table cannot carry
Both heads that carry code are 14/14 green — `2d9c678` (the fixes and
the framing) and `668fc72` (this block). The tip row is explicit rather
than missing: a checkpoint table can never record the head that adds
the checkpoint, and #215's lane used the same convention. The branch
tip stays authoritative over any row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 14:51:28 +02:00
Levi Neuwirth 668fc72f23
docs(ledger): the lane's PR and CI checkpoint, and the sweep that could not attribute itself
Fills in what could only be filled in after the fact: PR #216, the
opening head's CI run, and the four-way sweep table. Both branch totals
are exactly +4 on their baseline — the four witnesses, and nothing else
moved.

**A shared `CARGO_TARGET_DIR` makes a local sweep unattributable, and it
bit this lane.** The branch `crdt` sweep first reported seven failures
in three suites while the baseline `crdt` sweep was clean. All three
spawn the REAL `pmacs` binary out of the target directory, and the
failure text named its own cause — "daemon does not advertise required
capabilities … start the daemon built with the `crdt` feature". A
concurrent `cargo test --workspace` in a DIFFERENT WORKTREE, at default
(non-`crdt`) features and the same `CARGO_TARGET_DIR`, had overwritten
`target/debug/pmacs` mid-sweep. Confirmed with `pgrep` while it was
happening, and discriminated by re-running the same three suites from
the same tree with a dedicated target directory: 41/41 green, then the
whole configuration swept again there. Recorded in the handoff's
standing hazards, beside the feature-blindness rule it rhymes with: a
feature-flavored binary is a shared mutable file, not an artifact
private to your invocation.

The clean re-sweep left ONE failure, and it is recorded rather than
rerun away: `lsp_dispatch_seams_acceptance acc33_...`. It is a new
incident by the registry's rules, and it is not attributable to this
branch on a STRUCTURAL argument rather than on its green rerun — the
only Rust change lives in `#[cfg(test)] mod tests`, which compiles into
the lib test target alone, so an integration-test binary linking the
non-`cfg(test)` lib is exactly what `main` builds. CI's `Test (crdt)`
job passed at the same head and runs that suite. The 15/15 repetition
set is the weakest of the five points, not the argument. No registry
row is opened: the registry judges red CI runs and keys on linked CI
occurrences.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 14:33:36 +02:00
Levi Neuwirth 49a42ec9dc
feat(listview): the tree primitive — depth, collapse, and identity
COHERENCE.md §14's last missing workbench primitive. Q#TR1-TR4 decided
at review; this implements them.

EXTENDS LISTVIEW rather than adding a treeview (Q#TR1). A separate
primitive would either duplicate ~200 lines of panel discipline —
Q#GB18 handle identity, Q#GB13 `<2>` disambiguation, the read-only
intercept, `prev` capture, the quit chain, generated-buffer writes — or
require extracting them from a shipped primitive first, which is the
riskier change. Rows gain OPTIONAL `depth` and `id`; absent, they behave
exactly as before, which is what keeps the three flat consumers
untouched.

THE OBSERVATION THAT MADE THIS CHEAP: collapse only ever HIDES rows and
never changes a surviving row's depth. Combined with consumers emitting
parents before children in document order, a node's descendants are a
CONTIGUOUS RUN of following rows with greater depth. So collapse is
filtering an existing array, not re-deriving one — the primitive never
calls the consumer to re-render a fold, and pre-rendered indentation
stays correct. That is why `text` remains consumer-supplied (Q#TR4),
which also sidesteps the future conflict with dired's fixed-width
`_layout` column contract.

It is also why a panel with NO `on_refresh` can still fold. The anchor
consumer is exactly that panel: the outline has no refresh at all
(framing §1.5a), so a design requiring the consumer to re-supply rows on
every fold would not have worked for the only consumer that exists.

SELECTION IS RE-SEATED BY ID, NOT BY LINE (Q#TR3). A fold inserts or
removes rows above the cursor, so a line-keyed restore lands on an
unrelated node — the defect `listview.refresh` already had in milder
form. `id` is consumer-supplied and compared by equality; the primitive
never derives one. The outline uses `line:col`, unique per document and
stable across re-render, rather than the `::` parent chain, which
collides on overloads and same-named siblings — precisely where a stale
expansion would reattach to the wrong node.

`has_children` reads the FULL row array rather than the rendered subset.
A collapsed node's children are absent from `line_to_row` by
construction, so asking the rendered view would answer "no" for every
collapsed node and make expanding impossible.

TAB ON A LEAF REPORTS rather than silently doing nothing. The outline's
`g` is already a dead binding — bound, dispatched, no feedback — and
this primitive must not add a second one.

Tests: fold hides ALL descendants while the node and its SIBLING
survive; state and selection survive a re-render; a leaf reports; and a
depthless panel is unchanged by TAB. The fold test is bite-verified —
disabling only the ancestor filter fails it on "descendants hidden".

Verified: fmt, diff-check, clippy with and without crdt, --lib 1896, m4
149, listview 21/21, and the full serialized luajit sweep at 3453
passed / 0 failed. That count reconciles exactly: main is 3450 (Stage
3's 3449 sweep predated its capability-fallback pin) plus these three
tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 14:31:31 +02:00
Levi Neuwirth 5186bfd67a
docs(tree): revision 4 — a bound key that does nothing is not an absent key
"NO `g`" WAS LITERALLY FALSE, in revisions 2 and 3 both.
`bind_local_keymap` binds `g -> listview.refresh` on EVERY panel
unconditionally (listview.lua:147). What three of the four consumers
lack is an `on_refresh`; `listview.refresh` then returns immediately.

I had been collapsing three distinct facts into one word: whether `g` is
BOUND, whether refresh is ADVERTISED in the header, and whether refresh
is FUNCTIONAL. The §1.3a table now separates them, because a reader
checking "does the outline have g?" against the source would have found
the framing wrong and had no way to tell which claim was the intended
one.

The consequence is worth recording on its own: THE OUTLINE HAS A DEAD
REFRESH BINDING. `g` is bound, dispatched, and silently does nothing —
no status, no feedback. That is a small UX wart independent of anything
this framing proposes, and it is recorded rather than fixed here.

COHERENCE.md §14 IS CORRECTED IN THIS BRANCH rather than deferred to
implementation or split into its own lane. §25 is explicit that when a
PR changes an audited claim, updating the file RIDES THAT PR — #204
added `*lsp*` and did not update the "exactly three call sites"
measurement, so the correction rides the framing that found it. The
ad41cf1 audit fact is retained as history rather than overwritten, with
the current count of four and `*lsp*` named as the post-audit addition;
§25 also says symbols are authoritative and notes the line numbers have
drifted.

The §0 scorecard row carried the same "3 call sites" and moves with the
body. A grade table that disagrees with the section it summarizes is the
same defect one screen apart.

§14 also now records that `*lsp*` is the only one of the four with a
working refresh, and that the other three carry the dead binding —
which is what makes the tree framing's refresh-scoping conclusion sound
rather than lucky.

Framing only, still unapproved. COHERENCE change is a correction of an
existing audited claim, not a new grade.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 13:53:28 +02:00
Levi Neuwirth 2d9c6787e4
docs: record Stage 2 in the framing, and fence R3 off from R2's retirement
Revision 4 of `docs/macos-ci-signal-integrity-framing.md` records
implementation findings, not a new design round: §4's acceptance is
unchanged and was approved at revision 3. Two of the four findings
correct this document.

- **§1.3 named the right window and the wrong assertion.**
  `leader=exited(signal SIGUSR1)` is rendered only on a FAILED `kill`,
  and the USR1 cannot be the call that failed — it is the call that did
  the killing. The failing call is the SIGTERM that follows, so
  `.expect("TERM delivers")` is what blew up, not the `Running` state
  check; `ProcessState` never carries that value and nothing ticks
  between the two calls. The row's fragment and mechanism were both
  right. Why a group-directed TERM found no group is NOT established
  here, and the fix does not depend on it.
- **§1.5 scoped the fix one function too narrowly.**
  `wait_for_published_file` gates the real-TUI smoke on the identical
  predicate. §1.5's note about the bottom-panel helper is about a
  different file and correctly refuses creep there; it does not reach
  this one.
- The fixture also had an unnamed second dependency: these signals are
  group-directed, so a forked `sleep` is an untrapped group member, and
  survival depended on bash and dash suppressing the fork for the last
  command of a `-c` script.
- R2's witness could not reproduce the row on Linux, so it widens the
  pre-trap window deliberately and proves survival by exit disposition
  rather than by an absence observed within a window.

`docs/ci-red-signatures.md` gains the sentence R3 needs most: **R2's
retirement does not touch it and must not be read as touching it.** The
hardening changed a fixture and no product code; the same
group-directed `kill` runs. Because the fixture changes the shape of the
signalled group, a change in how often R3 appears would be evidence
about FREQUENCY, not about cause — and its retirement is still a
diagnosis by the process-signal / reap-ledger lanes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 13:51:40 +02:00
Levi Neuwirth bd532dde6b
fix(test): retire R2 and R4 — two readiness predicates weaker than their assertions
Stage 2 of `docs/macos-ci-signal-integrity-framing.md` revision 3
(acceptance 6–9). Two test races, both the same shape: the thing waited
for was weaker than the thing asserted, so the wait could return inside
the window the assertion needs closed.

R4 — `wait_for_file` returned as soon as `fs::read` succeeded, which
succeeds on a ZERO-BYTE FILE. The probe publishes with
`open(path,'wb').write(b'1')` and `open()` creates the file before
`write()` fills it, so the helper handed `[]` to a caller asserting
`== b"1"`. It now takes the expected bytes and waits while the file
holds a STRICT PREFIX of them — the states a write in flight can be
observed in — returning anything else immediately so the caller's
`assert_eq!` stays the discriminating assertion rather than becoming a
timeout inside a helper that does not know what was expected.

All four callers pass their expectation. `wait_for_published_file`, one
function away in the same suite, gated the real-TUI smoke's
`assert_eq!(…, b"1")` on the identical predicate and is fixed with it:
leaving it would have let R4 recur under a different selector, which the
registry would then have had to judge a new incident.

R2 — the USR1 fixture waited on `ProcessEventKind::Started`, emitted at
SPAWN, not when `/bin/sh` has parsed `trap '' USR1`. SIGUSR1's default
disposition is terminate, so a signal inside that window kills the
child. The child now publishes a marker AFTER the trap and the test
waits for that marker's CONTENT (the same zero-byte trap applies to a
shell's `>` redirection). `exec` replaces the forked `sleep`, so the
group holds exactly one process and the ignored disposition survives by
POSIX rather than by the shell's fork-suppression optimization — an
unstated dependency the old fixture had, since these signals are
group-directed and a forked `sleep` is an untrapped group member.

Four witnesses, each verified by REVERTING the fix and observing the
failure rather than by reasoning about it:

- `wait_for_file_does_not_return_a_zero_byte_readiness_file` fails
  `left: []`, `right: [49]` — R4's two required fragments, verbatim;
- `wait_for_file_does_not_return_a_partial_write` fails on the torn read
  a length check alone would admit;
- `wait_for_file_returns_divergent_content_rather_than_timing_out` fails
  against an over-strict helper that waits for an exact match;
- `usr1_readiness_waits_for_the_trap_not_for_the_spawn` fails
  `left: Some("SIGUSR1")`, `right: Some("SIGTERM")` with the readiness
  wait removed. Its fixture sleeps before `trap` so the pre-trap window
  is deliberate rather than load-dependent, and it proves survival by
  the child's EXIT DISPOSITION rather than by an absence observed within
  a window.

R1 is NOT touched — referred to the async-runtime lane (Q#MCI3), because
widening its budget would make it pass and measure nothing more. R3 is
NOT touched and remains UNRESOLVED, owned by the process-signal /
reap-ledger lanes.

`docs/ci-red-signatures.md` moves R2 and R4 to a "Retired rows" section
with their dispositions and adds the rule the file needed and lacked: a
red matching a retired row is a RECURRENCE that puts the retirement in
question, never a known flake. `docs/active-work.md` carries this lane
from its first commit rather than after review asks for it.

Repetition sets, not single runs: the two `--lib` process tests 15/15,
the whole `vterm_stage2_acceptance` suite 15/15 at default parallelism.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 13:41:05 +02:00
Levi Neuwirth 932b3ab179
docs(tree): revision 3 — an example is not a consumer
Three review findings, all verified in source before applying, plus one
found while verifying them.

`*references*` HAS NO `g` AND NO `on_refresh`. Revision 2 asserted it
twice. Its header is "RET visit  n/p move  q quit" and it supplies only
`on_visit` (lsp.lua:2442). The consumer with refresh is `*lsp*`
(lsp.status, lsp.lua:3004).

The error is worth naming precisely because it will recur otherwise: I
read `listview.lua`'s MODULE-DOCSTRING EXAMPLE, which illustrates the
API using `name = "*references*"` and a header containing `g refresh`,
and treated it as the real consumer. An example written to show the
shape of an API is not evidence about any caller of it. The
refresh-scoping conclusion is unaffected — it rested on the OUTLINE
lacking refresh, which holds.

THE BRANCH PLAN STILL NAMED buffer-list. Acceptance 5 had already been
corrected for exactly that error in the previous round; the same claim
survived one section further down. Fixing a mistake where it was
reported is not the same as fixing it where it occurs.

"BYTE-IDENTICALLY, PINNED BY THEIR EXISTING SUITES" WAS UNSUPPORTED.
`listview_acceptance` says in its own header that the references panel
needs a live LSP and is validated manually or via the m4 harness — it
does not exercise `*references*` at all — and the m4 hover test asserts
content PRESENCE, not byte-exact output. So the criterion claimed
coverage that does not exist. It is now posed as a decision rather than
patched: either byte-identity becomes a new test this stage writes
(needing the fake-LSP harness for references), or the claim weakens to
the substrate behaviours actually pinned. Leaning recorded toward
writing the test, because a flat consumer silently gaining an indent
column is precisely what this criterion exists to catch and
content-presence would not see it.

FOUND WHILE VERIFYING: §14's "exactly three `pmacs.listview.open` call
sites" is STALE. There are four. `*lsp*` arrived with Journey Stage 1b-2
(#204), after §14's audit at ad41cf1, and it is the ONLY listview
consumer with refresh — which is why §1.5a's conclusion holds rather
than being luck. §14's line numbers have drifted too. Recorded in a new
§1.3a so the next reader does not inherit "three".

Framing only, still unapproved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 13:34:22 +02:00
Levi Neuwirth cf4ac1c5ef
docs(tree): revision 2 — five review corrections, all verified in source
REFRESH WAS UNSCOPED FOR THE ANCHOR CONSUMER, and this is the one that
would have wasted implementation time. Two acceptance criteria rested on
`g` refresh preserving collapse and selection. The outline's header
offers "RET visit  n/p move  q quit" — NO `g` — it supplies no
`on_refresh`, and `listview.refresh` opens `if not (p and p.on_refresh)
then return end`. The criteria were unreachable for the only consumer
that exists.

Refresh is now out of scope, with the question it actually raises stated
rather than hidden: an outline refresh means re-requesting
textDocument/documentSymbol, which is an async round-trip with its own
await, failure and staleness handling, and it raises who owns the result
when it arrives against a buffer the user may have edited or left. That
is LSP request-lifecycle work; bundling it here would make the tree lane
responsible for it. Acceptance is re-scoped to what the primitive
controls — collapse and selection surviving a RE-RENDER — and the
refresh follow-on is parked with its precondition.

ACCEPTANCE 1 DECIDED Q#TR4 WHILE CALLING IT OPEN. "No `string.rep`
indentation in lsp.lua" commits to primitive-owned indentation, which is
exactly the question Q#TR4 leaves unresolved. The criterion is
representation-neutral now: the outline renders its hierarchy THROUGH
the primitive rather than by pre-formatting it, and whether the
primitive emits the indentation or the consumer still supplies a string
alongside structural depth stays open.

Q#TR1 MISREAD §14, and the correction changes the tradeoff rather than
softening it. Revision 1 said a separate treeview would be "exactly the
second primitive §14 warns about". §14 EXPLICITLY LISTS A TREE in the
reusable set it wants, alongside virtual list. What it warns against is
bespoke per-consumer plumbing — each subsystem inventing its own UI
vocabulary. A treeview sharing the existing buffer/panel disciplines is
not that; a tree hand-rolled inside lsp.lua would be. The real tradeoff
is narrower and is recorded without a leaning, because the scout still
found nothing that decides it.

THE REGRESSION CRITERION NAMED THE WRONG CONSUMERS, and worse, named the
exact ones §14 exists to correct. `*buffer-list*` and project search do
NOT use listview; §14 measured three call sites, all in lsp.lua —
`*references*`, `*outline*`, `*lsp-help*` — and calls the older claim a
documentation error. Repeating it would have re-introduced a mistake
that document was written to fix. The criterion now protects the actual
siblings, and says what to do if the broader surfaces are ever in scope.

CONSUMER ACCOUNTING TIGHTENED. Five named future consumers, not six. And
the split that matters: ONE existing anchor consumer (the outline) plus
ONE future constraint source (dired's deferred `i`) — dired constrains
the design but cannot validate it, because nothing has been built
against it. Calling them "two consumers that exist" overstated the
evidence by exactly one. "Every input a tree needs" is qualified to
"every input a tree needs to RENDER", since stable node identity is
precisely what no existing field supplies.

Framing only, still unapproved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 13:22:24 +02:00
Levi Neuwirth 61b1062c5f
docs: frame the tree primitive — anchored on two real consumers
COHERENCE.md §14 grades Tree as the last missing workbench primitive,
and §20 Priority 5 names it as what remains after the bottom panel. Its
argument is to build it once "before dired's directory view and the
workers tree harden their own conventions".

THIS FRAMING NARROWS THAT ARGUMENT DELIBERATELY. §14 lists six future
consumers, and designing a shared primitive against six hypothetical
ones is how you get a model that fits none. The scout found a better
basis: one consumer already ships a tree and fakes it, and a second is
already scoped and deliberately deferred.

THE HIERARCHY ALREADY EXISTS AND IS ALREADY DISCARDED. `Symbol::
push_hier` walks a genuine LSP DocumentSymbol tree — it recurses on
`children` — and flattens it, preserving `depth`, a `::`-joined parent
chain, and document order with parents before children. `lsp.lua` then
re-renders that depth as LEADING SPACES INSIDE THE ROW TEXT, under its
own comment "FLAT with a `depth` field --- indent, don't recurse". So
the outline has no collapse, no expand, no parent/child navigation, and
every input a tree needs is already computed. It is the anchor consumer
because it needs no new plumbing and its limitation is observable today
rather than hypothetical.

Dired is the second: it landed a flat listing for Emacs parity and
deferred `i` insert-subdirectory in its own §13 — the restraint §14
credits, and what keeps the door open.

The workers view is NOT a consumer yet: it is a Rust-generated text
buffer raw-switched into the active window, with no rows.

REFRESH RESTORES A LINE, NOT A NODE, and that is the crux rather than a
detail. `listview.refresh` saves `cursor_line()`, rebuilds rows wholesale
from a freshly produced array, and re-seats by walking `move_down`.
Today that is a mild wrong-restore. Collapse breaks it outright, because
expanding a node inserts rows ABOVE the cursor — and collapse state
itself must survive refresh, which requires recognising "the same node"
across two independently produced arrays. Neither `line_to_item` nor an
opaque `item` can do that. This is why the stable-identity question
decides whether selection and expansion survive a model update at all.

Four questions are left genuinely open: extend listview versus a
separate treeview (no leaning recorded — the scout found nothing that
decides it); who owns collapse state; what a stable node identity is;
and whether the row still carries pre-rendered text. On identity the
scout did establish constraints: listview cannot derive one because
`item` is opaque; the outline's parent chain plus name is nearly
sufficient but collides on overloads; dired's path would be genuinely
stable. So identity is almost certainly consumer-supplied, which makes
it part of the public contract rather than an internal detail.

NO INTERACTION ISLAND unless evidence forces one. Expand/collapse are
buffer-local bindings on a generated buffer, exactly as RET/n/p/g/q
already are. §6 grades islands "weak, and growing"; this must not add to
that count, and if some behaviour cannot be expressed that way it is a
finding to report rather than a licence.

§1.6 states plainly what this document is: unlike the last two lanes
there is no fallout to census and no baseline to diff, so it ARGUES a
model rather than measuring one — the shape that has historically needed
the most review rounds here.

Framing only. Acceptance is explicitly not final pending the open
questions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 13:13:34 +02:00
Levi Neuwirth 12f2970ad8
Merge pull request #215 from levineuwirth/macos-ci-signal-integrity
CI red-signature registry — Stage 1 of macOS signal integrity
2026-08-05 11:07:53 +00:00
Levi Neuwirth 2e0617fddd
review round 2: precision fixes, and a checkpoint table that cannot go stale
Three corrections.

"NO OCCURRENCE WAS EVER OBSERVED" OVERSTATED WHAT THE AUDIT CAN SAY.
Someone may well have seen one of these fail and simply not recorded it;
what is established is the absence of a RECORD. The framing now says
"linked or captured" and states the distinction explicitly, because an
audit that claims to know what nobody saw is making the same kind of
unfounded assertion this lane exists to remove — one level up.

A missing closing quotation mark in acceptance 3.

THE LANE CHECKPOINTED THE REVIEWED HEAD, WHICH GOES STALE ON THE NEXT
PUSH — the exact mechanism by which #171 became invisible while it
drifted 153 commits. Recording one head is a snapshot; what a resume
ledger needs is the sequence. It is now a table of head -> CI run ->
result, newest last, with the rule stated: the branch tip is
authoritative over any row, verified by `git rev-parse`, because the
table is written by hand and the tip is not.

Verified: fmt, diff-check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 11:11:57 +02:00
Levi Neuwirth f76897c284
review round 1: audit notes are not rows, and R2's signature was weakened
Five corrections, one blocking.

BLOCKING — the contract, not the implementation, was what needed
changing. Revision 2's acceptance 3 offered a binary: carry an incumbent
in with a signature and evidence, or remove it as never substantiated.
Stage 1 shipped a THIRD state because both incumbents are neither. The
framing is now revision 3 and names all three states, because a
governing criterion that says "two" while the branch does five is the
framing describing something that does not exist.

"MECHANISM NAMED" OVERSTATED THE EVIDENCE, and the phrase is retired.
The a33 audit proves an assertion string exists; the m6_8 audit proves a
test is timing-based. NEITHER ESTABLISHES A FAILURE MECHANISM — no
occurrence was ever observed, so nothing is known about how, or whether,
either fails. They are now AUDIT NOTES A1/A2 rather than registry rows,
with `R`-numbers reserved for signatures carrying linked evidence. The
distinction is not row-versus-weaker-row: a row says "this was seen,
here is the evidence", a note says "someone recorded a belief and no
occurrence backs it". Both remain unmatchable, so a red in either test
is a new incident.

R2's SIGNATURE WAS WEAKENED AND IS RESTORED. Splitting
`leader=exited(signal SIGUSR1)` into `leader=exited(` plus `SIGUSR1`
would match a child that exited by some OTHER disposition while SIGUSR1
appeared elsewhere in the output — precisely the name-style
over-matching this registry exists to refuse, reintroduced one level
down as fragment-style over-matching. It is one exact fragment again,
and the row says why.

THE HANDOFF STILL ISSUED LIVE IMPERATIVES. Its historical block opened
with "rerun isolated before treating a sweep failure as a regression"
and closed with "rerun the test alone before investigating", so the
supersession note I added sat between two instructions it contradicted.
Both are rewritten as record: the block now reports what that lane
OBSERVED, and the retired instruction is marked retired with its reason
— an isolated green reproduces nothing about a load-sensitive failure.

#215 HAD NO LANE, in the file that requires one for every open PR and
records why: #171 drifted 153 commits while invisible there. That is the
same defect, caught in review rather than 153 commits later. The lane
now carries branch, base, PR, reviewed head, what Stage 1 ships, the
verification, what Stage 2 owes, and the recovery worktree command. Both
snapshot headers are bumped.

Also narrowed a claim the PR body overstated: the registry is NOT
macOS-only. All four EVIDENCED rows are macOS, which is a property of
these occurrences; A1's job is `GPU Render (headless)`, which runs on
Ubuntu, and A2's job was never recorded. A future row from any job
belongs in the same table.

Verified: fmt, diff-check, clippy, --lib 1896, --lib --features crdt
2081.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 10:52:07 +02:00
Levi Neuwirth d33bf4df78
docs(ci): the red-signature registry, and an audit that found no immunity
Stage 1 of the macOS CI signal-integrity lane. `docs/ci-red-signatures.md`
is now the single authority for judging a red CI run.

NOT NAMED "FLAKES", DELIBERATELY. One of its rows is a possible product
defect, and a filename calling it a flake would confer immunity the
evidence does not support.

A ROW MATCHES ON SIGNATURE, NEVER ON TEST NAME. All three of selector,
job/flavor and every required fragment must hold. Fragments are
normalized rather than pasted: PIDs, elapsed times and rendered
OS-error suffixes vary between runs, so a verbatim key would match
nothing, and where a fragment lists alternatives (ESRCH / "No such
process") those are one condition rendered differently by platform. A
failure in a listed test WITHOUT that row's fragments is a new incident.

The process test is why that rule exists: it produced TWO signatures
with different mechanisms and different causal status, and only one is a
test bug. Four incidents, three tests, four signatures — the registry
counts signatures.

THE RERUN RULE IS REPLACED, NOT SOFTENED. A green rerun establishes
INTERMITTENCE ONLY — never environmental cause, harmlessness, or
retirement. The same signature again is a second occurrence and stays
blocking pending investigation or a merge-base control. A different
signature is a new incident.

RETIREMENT IS CAUSAL. A test race retires by hardening that removes the
mechanism plus a discriminating witness; a measurement-design row by its
owning lane replacing or justifying the measurement; an unresolved row
by diagnosis and disposition. Main-branch greens accumulate as
occurrence evidence and retire nothing.

THE AUDIT FOUND A THIRD CATEGORY the framing's acceptance 3 did not
anticipate. It said each incumbent is either carried in with a signature
and evidence, or removed as never substantiated. Both incumbents are in
between: the tests are real and the mechanisms plausible — a33's "blue
pixels" is a genuine assertion string at pmacs-gpu/src/main.rs:17973,
and m6_8 exists and is timing-based — but NEITHER HAS A LINKED
OCCURRENCE. Deleting them would discard a real observation; carrying
them as peers of the evidenced rows would grant exactly the reputation
this lane exists to deny.

They are recorded as "mechanism named, no occurrence recorded", with it
stated that nothing there confers known-flaky status and that a red
matching one is a FIRST recorded occurrence to be investigated. R6 goes
further: with no signature ever captured it CANNOT BE MATCHED AT ALL, so
a red in that test is a new incident by default. That is the correct
outcome for an entry that never carried evidence.

LIVE POLICY IS CENTRALIZED; HISTORICAL EVIDENCE IS NOT MOVED. The
handoff's hazards rule becomes a pointer. Its landed-lesson block at
§5 keeps its own evidence and gains a note that the registry's rerun
rule supersedes "rerun isolated". active-work.md's two mentions are
verification records for the reap-ledger and bottom-panel lanes and are
left alone; only the CI-weakness block's triage half points at the
registry, because the job-cost question there is genuinely separate.

Verified: fmt, diff-check, clippy with and without crdt, --lib 1896,
--lib --features crdt 2081, pmacs-protocol 19, m4 149, required GPU 221.
All three tests the registry names pass locally on Linux, which is
consistent with every row being macOS-only and is not evidence about
any of them.

Docs only. Stage 2 (hardening) is a separate PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 22:55:25 +02:00
Levi Neuwirth a1c6c93adf
docs: tighten macOS CI signal-integrity framing
Define signatures as normalized selector/job/output matches, preserve
historical evidence while centralizing live triage policy, and replace
green-run retirement with causal disposition.

Also fix the rerun-rule contradiction and move the questions out of the
inline-math namespace.
2026-08-04 22:48:37 +02:00
Levi Neuwirth d6bf0c3233
docs: frame macOS CI signal integrity — a signature registry, then hardening
Four red CI incidents across #213 and #214 were each judged "not caused
by this PR", and #214's case is airtight: it is docs-only and its tree is
byte-identical to a green main. THAT PROVES THE PRs DID NOT CAUSE THEM.
It does not prove they are harmless environmental noise, and three of the
four have a specific, findable mechanism. This lane separates those two
claims, which the current process conflates.

FOUR INCIDENTS, THREE TESTS, FOUR SIGNATURES. The registry counts
signatures, not test names, because the process test alone produced two
with different mechanisms and different causal status — collapsing them
under one name is how a possible product defect acquires a flake's
immunity.

  1. supersede_cancels_in_flight_job_within_50ms —
     "supersede did not cancel within 50ms". MEASUREMENT DESIGN. Its
     premise is a 15ms sleep asserted-by-comment to mean "the worker
     picked the job up"; on a loaded runner it may not have, in which
     case the test measures the QUEUED path while claiming the running
     one. And the 50ms clock starts before the second dispatch and is
     consumed by the test's own tick+sleep pump, so the measured
     interval is dominated by when THE TEST got scheduled. Widening the
     number would make it pass and measure nothing more.

  2. a_successful_signal_disposition — "leader=exited(signal SIGUSR1)".
     TEST RACE. Readiness is ProcessEventKind::Started, emitted at
     SPAWN, not when /bin/sh has installed `trap '' USR1`. USR1's
     default disposition is terminate, so a signal in that window kills
     the child. The fixture's own comment states the requirement it does
     not enforce.

  3. a_successful_signal_disposition — "EPERM,
     measured_group=unobservable(ESRCH), leader=live". NOT a test race:
     the group-target behaviour #176 and #200 circled and the
     reap-ledger lane parked disposition changes pending. Recorded
     UNRESOLVED, POSSIBLE PRODUCT DEFECT, with a diagnosis — never a
     green rerun — as its retirement condition.

  4. terminal_escape_gates — "left: [], right: [49]". TEST RACE.
     `wait_for_file` returns as soon as `fs::read` succeeds, which
     succeeds on a ZERO-BYTE file; the probe's `open()` creates the file
     before `write()` fills it. The predicate is "readable", the
     assertion is "contains 1" — the same shape as signature 2, fixable
     at the helper so every caller inherits it.

THE EXISTING PROSE IS DUPLICATED AND KEYED BY NAME. Flake claims live in
at least six places, disagree in detail, carry no signatures or evidence
links, and the handoff's list names three tests — two of which are not
among these four incidents, while three of the four are absent from it.
A list that is both stale and incomplete is worse than none: it confers
"known flaky" on whatever happens to be named and withholds it from
everything else. Acceptance 3 therefore AUDITS the existing three: each
is carried in with a signature and evidence, or removed with a note. No
entry survives on reputation.

The rerun rule is REPLACED rather than softened: one rerun reproducing
the SAME signature is evidence of intermittence only; a DIFFERENT
signature, or the same one twice consecutively, requires investigation
or a merge-base control before the red is attributed to the environment.

Quarantine, if hardening fails, is a separate STILL-BLOCKING CI step —
never #[ignore], continue-on-error, or silent retry-to-green. A
quarantine that stops failing the build is a deletion with extra steps.

Framing only. Sequencing is registry first, hardening second, per
review.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 22:23:43 +02:00
Levi Neuwirth bfb97c67c6
Merge pull request #214 from levineuwirth/docs-absorption-213
docs: retire the bottom-panel lane — Arc 7 is done
2026-08-04 22:11:03 +02:00