Commit Graph

1578 Commits

Author SHA1 Message Date
Levi Neuwirth 1180888291
docs(lane): the R9 conclusions were still here, and the onset is datable
779a6bd corrected four claims but left the ones that mattered most. This
ledger still said --workspace unification was refuted, that R9 ran the
"same binaries, same order, same tests", that later packages cannot be
implicated, and that the cause is cumulative across the preceding 37
binaries. All four are withdrawn:

  - R9 executed gpu_initial_target_acceptance-91f51d0b and
    gpu_invocation_acceptance-6b4b8223; the sweeps executed -5d9105cb
    and -d4dae4f0, byte-different. Command shape changes Cargo's
    fingerprint, so the comparison was never made. --workspace
    selection and the preceding tests are both OPEN, not refuted.
  - "Later packages cannot be implicated because their targets run
    after the failure" ignores that they affect the build graph and
    fingerprints before their tests execute.
  - No cumulative cause follows from reductions that ran different
    binaries. What is established is narrower: reproducible in the full
    sweep, not reproduced in any subset attempted.

And a finding that reframes the defect. sweep-crdt appears SEVENTEEN
times in this target dir's gate logs. The ctrl_c failure appears in
exactly the last three, and the test passed --- both copies, "... ok" ---
inside the stage before them. Last green 20260815T185708Z, first red
20260816T063330Z, no reboot between; the three earlier red sweeps failed
on unrelated rows. "Pre-existing on main" still holds, since 72da24a
reproduces it, but "always broken" is contradicted, and bisecting that
window is now the sharpest lead. The red full-sweep count is SEVEN, not
five, each enumerated with its own log digest in the teardown lane's
docs/probe-sigint-evidence.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 6adcf17c25
docs(lane): correct the claims this held branch was still transporting
Pushing 16cf3a2 made the retraction portable but not the correction.
This ledger still carried four falsified statements, and a held branch
that transports them is worse than one that never recorded them.

  - "119 binaries green, one red" -> 119 green result summaries and TWO
    red binaries. `gpu_initial_target_acceptance` includes the suite as
    a module, so a reproducing sweep reds twice (:3097 and :3131).
  - The ">=8s lifetime" arithmetic behind the retraction is FALSE. Both
    reproducing binaries finish in ~5.19s INCLUDING the 5s timeout, so
    the failing launcher lives about 5.1s --- inside what the sampler
    saw. The ">6s selector" proposed as the remedy would have captured
    nothing. The "mechanism located" claim is therefore not refuted by
    that argument; it stays unproven because the suite spawns root
    launchers from six call sites under --features crdt, so command
    line alone cannot attribute one to this test.
  - "the probe should die on SIGINT's default action" -> withdrawn.
    Absence of handler code does not establish default disposition;
    SIG_IGN is inherited across fork and survives exec, which is why
    inherited ignore is the leading hypothesis.
  - "never blocked indefinitely" -> only the event loop is bounded, at
    50ms. The process is not: the stdin reader blocks in read_to_end
    and the loop leaves only when stdin closes.

Also records that the defect now has its own lane,
`gpu-probe-sigint-teardown`, whose framing supersedes every diagnostic
claim here, and that §5b is held behind it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth c1cc98ad5a
docs(lane): retract the "mechanism located" claim --- it was unsupported
a92ef7f said the mechanism was located: the launcher blocked in
`do_wait` on a GPU probe child stuck in `futex_do_wait`. Checking the
instrument against the claim shows it does not support it.

The sampler caught 394 distinct launchers across a reproducing sweep and
the longest-lived was 5s TOTAL. For this test to fail, a launcher must
outlive its SIGINT by 5s, so its lifetime would be 8s or more. The
failing instance was never captured. What I described is a healthy
launcher from one of the suite's other tests --- the normal teardown
shape, reported as the defect.

This is the same error as the `available`-memory reading earlier in this
lane: a measurement that looked conclusive, reported before checking
that it discriminated. Retracted here rather than left to be found.

Two facts do survive and constrain the next attempt: `pmacs-gpu`
installs no signal handling at all, so the probe should die on SIGINT's
default action; and its main loop is a 50ms `recv_timeout`, so it never
blocks indefinitely. The next instrument must key on the failing
instance --- launchers outliving ~6s, or a PID recorded by the test
itself --- rather than sampling every launcher and hoping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth a4117adef8
docs(lane): locate the sweep-crdt mechanism --- the GPU probe will not die
Sampled the process table twice a second through a reproducing sweep.
After the test SIGINTs the launcher's process group:

  - `pmacs --gpu --socket ...` sits in `do_wait` for the full 5s. It is
    waiting on a child, not ignoring the signal.
  - `pmacs-gpu --headless-managed-probe ...`, its child and in the same
    process group so it received the SIGINT, sits in `futex_do_wait`
    and never exits.

The deadline is missed because the GPU probe does not tear down under
SIGINT. The fix belongs in the probe's shutdown path; raising the 5s
would only hide it. Why the child hangs ONLY in a complete sweep is
still open --- every prior GPU suite has exercised the adapter by then,
which is where to look first.

Five explanations are recorded as refuted so nobody re-runs them: load,
leaked daemons, inotify, `--workspace` feature unification, and any
specific preceding test --- all 37 preceding targets plus the suite run
green, which is the genuinely strange part.

The tmpfs hypothesis got a real experiment rather than an argument:
/tmp went 21G -> 1.2G, available memory 27G -> 45G, and the sweep stayed
red. Recorded with the note that my earlier `available`-based dismissal
was itself unsound, since tmpfs pages are not reclaimable yet still
appear in buff/cache --- right conclusion, wrong reasoning, and it took
the experiment to know which.

Also notes the test exists in two binaries: gpu_initial_target_
acceptance includes it as a module, so a reproducing sweep fails it
twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 9562fa7930
docs(lane): the sweep-crdt red is pre-existing on main
Attribution is settled. The identical `build-crdt && sweep-crdt` pair,
run at the merge base 72da24a in the primary worktree with its own
target dir, fails the SAME test: 119 binaries green, one red,
`ctrl_c_on_launcher_group_does_not_reach_spawned_daemon`. This branch is
not implicated, and no branch can pass this gate stage on this machine
until the underlying defect is fixed --- main included.

The mechanism is unknown, and this records two explanations I offered
and then refuted, so nobody re-runs them:

  - Load: refuted. Red on a quiet machine, load 2.77 at launch.
  - Memory pressure: refuted by correcting my own instrument. The
    sampler showed free memory at 543MB, which looked damning, but it
    recorded `free` --- not the meaningful figure on Linux. `available`
    was 27G. The 543MB was reclaimable cache. I had already reported
    the memory story before checking that, which was wrong.
  - Leaked daemons: refuted. Peak 58, up only 8 during the sweep, and
    the green standalone runs already ran at 46-50.

What the bisect did establish: green in every smaller context tried ---
the test alone three times at 0.15s against its own 5s deadline, its
whole suite, a workspace run filtered to just it, the lib binary then
the suite, and the three GPU suites in sweep order --- and red 4/4 in
the full workspace sweep across two trees. Cumulative across the 37
binaries preceding it, and not flaky.

Also notes that /tmp is a 30G tmpfs holding 21G of an unrelated
project's stale target directories. Recorded as an observation about
this machine, not as the cause, and not touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth cb830df7a7
docs(lane): three record corrections, one of which cost two gate runs
Review corrections:

  - The header said "ACTIVE, framing only" and named 3c06176 as the
    checkpoint. Now code-complete at 5174f73, ledger tip fb40d88, gate
    red.
  - The gate-status entry claimed isolation established an
    ENVIRONMENTAL cause. It does not. Isolation establishes
    INTERMITTENCE. Narrowed to: intermittent; foreign load is a
    measured confound; cause unresolved. No experiment here separated
    sweep contention, foreign load, and a genuine defect in the row.

And one found while checking the user's "eleven-stage" phrasing against
what actually ran. Yesterday's runs printed ELEVEN stages, today's
printed TEN, and the difference is `05-acceptance-bottom_panel_stage2b_
daemon_acceptance`. `--acceptance` is an explicit repeated flag; the
gate derives nothing from the diff. Both of today's runs used bare
`--protocol`, so **no acceptance stage ran at all** --- the suites were
verified by hand instead, which is not the gate.

The `Gates:` line named suites but never the flag form, which is how
that happened. It now carries the exact invocation and the expected
stage count, with the note that a ten-stage run is missing every
acceptance stage. It also said "the four `bottom_panel_*` suites" when
there are FIVE on disk; `bottom_panel_stage2b_protocol_acceptance` is
the fifth and belongs in a protocol-bearing lane above all others. All
five are listed rather than guessing which four an earlier writer meant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 2c60509e75
docs(lane): the slice-completion gate is RED, and why no PR followed
Nine of ten stages green at 5174f73. `09-sweep-crdt` red twice on
`ctrl_c_on_launcher_group_does_not_reach_spawned_daemon` --- "child did
not exit within 5s" --- which is a NEW signature, absent from
`ci-red-signatures.md` under any id.

The row holds a fixed 5s wall-clock deadline for a spawned child to
exit after a signal, and it runs inside the heaviest stage the gate has.
It is green alone and green with all fourteen of its suite siblings, the
latter in 0.15s against that same 5s deadline. Nothing about it touches
panels or the wire.

Sweep contention could not be separated from foreign load, and that is
recorded as inconclusive rather than dressed up: running the sweep-crdt
command alone red the same row, but `uptime` hit 59.51 during that run.
An unrelated turso workload is running in a LOOP on this machine,
holding a 16-core box at load 20-60, so clean gate evidence is not
obtainable here. The remaining work on this lane is one clean gate run
and nothing else.

Recorded in the lane ledger rather than the registry, per this branch's
standing reason: the registry here ends at U9 while the unmerged replay
branch already holds a U10, and duplicate ids have survived a clean
merge in this file once before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth bf4ffe0995
fix(panel): a second press ends the gesture it replaces
Found by reading the arming path back after the G5 checkpoint, not by a
failing row. `arm_accepted_gesture` overwrote an already-armed latch, so
a dropped `Up` --- one lost to an outbox that closed under a stall ---
was followed by the next press silently discarding the first gesture's
record, without counting it as a cancellation.

Inert on this base, where records are only counted. Once
`panel-pointer-replay` attaches a child release to each record, the
discarded one leaves a button held down with nothing left to release
it, and the arming code that decides this is this slice's.

Mutation: restore the plain overwrite -> the new row alone.

Also records three CI-red observations from the slice-completion gate,
which is the first entry on this lane with a MEASURED confound instead
of the standing uncontrolled one. Three wall-clock-deadline rows red in
one run --- criterion_1 by 0.12%, a PTY lifecycle race, and a 5s child
-exit deadline --- all green in isolation, the last in 0.15s against
its 5s deadline. `uptime` during the run went 14.02 -> 28.35, from an
unrelated turso test suite on the same machine with one binary at 693%
CPU. Not a controlled experiment, but the same evidence U9's synthetic
-load control was meant to produce, and it points at load.

Two process traps are recorded with them, because both were made here.
The Bash tool caps a command at ten minutes and SIGTERMs it, which the
gate reports as `FAILED (exit 143)` on whatever stage was running and
which reads exactly like a real failure. And `pkill -f <pattern>` kills
the invoking shell when the pattern appears in its own command line, so
the intended target survives while the operator believes it died --- and
here `pkill -f "cargo test"` would have destroyed an unrelated
project's build. Identify by PID.

Verified: `cargo fmt --check`; `cargo clippy --workspace --all-targets
-- -D warnings`; the four §5b G5 rows. The full protocol gate follows on
a quieter machine; the run described above is not evidence for this tree
and is recorded as an observation only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 419b078c0f
feat(panel): the receiver rows --- dedupe, atomicity, and the TUI control
Closes the rest of this slice's G9-G15 obligations. One production
change, one design correction found by mutation, and the rest witnesses.

G9b is the production change. `panel_motion_is_new` compared the cell
alone, so the FIRST motion after the mapping moved was eaten --- the
pointer has not travelled, but the cell now denotes different text, and
that is exactly the motion the daemon needs to re-anchor the gesture.
Now keyed by `(generation, cell)`. Deliberately read at the motion site
rather than reset from the frame path: resetting on every accepted
repaint would re-arm within one generation and bring pixel-rate traffic
straight back.

G9a, G9c, G10, G10a, G10c are witnesses over behaviour that was already
correct: a generation change ships even when the visible cells are
byte-identical; an identical frame at a higher generation still moves
authority; invalid frames, zero generations and lower generations are
each refused with frame AND generation retained; `Absent` does not erase
the high-water mark.

G15 is the TUI structural control, and its fixture IS the control: the
session has no `SemanticRenderState` at all, so every local click, drag
and wheel effect below is reached without a producer in existence. That
is a stronger claim than asserting a value was not consulted. Both wheel
ticks must land, for the same reason the mapped family needs its
exemption.

The design correction: G10a's first version asserted the zero refusal
with a generation already HELD. Mutating the zero check away left it
green --- zero is also *lower* than the held value, so the nondecreasing
clause refused the frame and the row proved nothing about zero. Zero is
only isolable before any authority exists, which is also the case the
framing names: a sender that never initialised the field. Split into its
own row with that setup, and the row says why.

Mutations, each biting only its named row:

  - dedupe compares the cell alone -> G9b
  - the frame path re-arms the dedupe on every accepted frame -> G9b
  - daemon dedupes across the generation change -> G9a
  - return early on frame equality before applying the generation -> G9c
  - apply the generation before validating -> G10
  - accept generation zero -> G10a (after the split; before it, this
    mutation SURVIVED)
  - `Absent` erases the high-water mark -> G10c
  - a lower generation is accepted -> G10c
  - panel input requires a token the TUI cannot have -> G15

Deferred, per SS5b's split table and unchanged here: G11b (exhaustion
cancellation), G12a/G12b (both two-tick wheel EFFECTS), G6c/G7c.

Verified: `cargo fmt --check`; `cargo clippy --workspace --all-targets
-- -D warnings`; `cargo test --lib` (1959); `cargo test -p pmacs-gpu
--bins` (280); `bottom_panel_stage1_acceptance` (47),
`bottom_panel_stage2b_daemon_acceptance` (39),
`bottom_panel_stage2b_gpu_acceptance` (2); `git diff --check`. Clippy
caught two findings in the new test code after the suites were already
green, which is why it runs as its own step.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth a8a996c446
feat(panel): the wheel exemption, exhaustion, and mapped coalescing
Three rows that share a shape: each is a gate this slice owns whose
downstream EFFECT belongs to the rebased replay lane.

G10b --- ordering, and a carve-out. `panel_mapping_is_current` now takes
the event kind. Zero is refused FIRST, then coordinate-free wheels skip
the freshness comparison. The order is the row: run the carve-out first
and a sender emitting zeroed wheels faces no check at all, which is an
inbound opt-out through the exempt path. The exemption exists because a
tick changes `view_top` and so advances the key --- the next tick already
queued behind it echoes the previous generation, and without the
carve-out the panel scrolls once per frame and appears dead. It returns
before the read, so a wheel does not advance the key either; advancing
would make a wheel invalidate the press after it.

The framing's carve-out-to-the-carve-out, re-imposing the check for
CHILD-REPORTED terminal wheels where SGR carries row and column, is
replay's. Whether a wheel is forwarded is decided by the reporting mode,
and no panel pointer coordinate is consumed on this base at all.

G11a --- exhaustion fails CLOSED. `saturating_add` froze the key at the
ceiling while the mapping kept moving underneath it: the stale-gesture
hole the key exists to close, with the check still appearing to pass.
Now `checked_add`, and overflow publishes `Absent`, clears input
authority, and latches for the session.

G13a/G13b --- `PanelPointerMapped` fell through `coalesce_kind` to
`None`, so pixel-rate mapped motion was lossless and filled the bounded
outbox. Two tags of its own; tail-replacement takes the whole event, so
coordinate and generation advance together and a collapsed run can never
pair a new coordinate with a stale one. Press, release and every wheel
kind stay lossless.

Mutation results, including two that changed the design:

  - exemption before the nonzero check -> G10b(zero) alone
  - no wheel exemption -> G10b(exemption) alone
  - saturating instead of checked add -> G11a alone
  - no exhaustion latch -> G11a, but only AFTER the row was extended.
    The first version of G11a did not bite: the latch had no proven
    job, because the overflow path already returns before storing the
    ceiling snapshot, so the next read re-takes the changed arm anyway.
    Measured, the two are ALTERNATIVES --- either alone keeps the band
    down; only removing both resurrects it. The latch is kept as the
    primary because it has a job the ordering does not: `peek` now
    honours it, so the peek and the authoritative read agree that an
    exhausted session has no key rather than reporting the ceiling.
    The source comment says this, rather than the "second half" claim
    it made before the measurement.
  - mapped variants untagged / one tag for all kinds / sharing the
    legacy tags -> the mapped coalescing row alone, three times

Witness-shape note: the two G10b rows call the predicate directly, and
say why. A wheel has no dispatcher-visible effect on this base --- a
document panel focuses on `Down` only --- so asserting focus for a wheel
would prove nothing. Each row carries a press leg, which does have an
effect, to show the predicate is wired into the production arm.

Verified: `cargo fmt --check`; `cargo clippy --workspace --all-targets
-- -D warnings`; `cargo test --lib` (1959); `cargo test -p pmacs-gpu
--bins` (275); both `bottom_panel_stage2b_*` suites (39); `git diff
--check`. `composition_overhead_under_ten_percent` red once during this
work and green in isolation --- a second occurrence of a signature the
lane ledger already carries, now recorded there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 8c5ca10698
feat(panel): G5a's cancellation trigger, and the latch it needs
G5a is the one G5 row this slice owns: the mapping key advancing must
raise cancellation at the advance, not reactively when a later event is
refused. Reactive cancellation loses a race --- if the successor frame
reaches the frontend before the physical `Up`, the producer clears its
latch and the cancelling event never arrives.

Cancelling requires something to cancel, so the accepted-gesture latch
lands with it: `AcceptedPanelGesture` (button, coord, buffer, and
whether the press reached a child) in a per-frontend slot on
`SemanticRenderState`, armed from the daemon's accepted inbound arms
after every gate has passed.

Writing that code decides three things the framing's deferred rows later
assert about, so they are pinned here under SUBSTRATE names rather than
under G5c/G5d/G5g/G5p. Those IDs stay on `panel-pointer-replay` per
SS5b's split table --- each asserts something about a synthetic release
or a real drag continuation that does not exist on this base, and
claiming an ID in two branches is the merge hazard `active-work.md`
already records surviving a clean merge once.

Two consequences are recorded rather than fixed:

  - Cancellations are COUNTED, not queued. The record queue is what
    replay drains to deliver each release; landing it here would grow
    one entry per cancelled drag with nothing ever draining it. A
    saturating count is bounded and still separates a consume from a
    cancellation.
  - The other G5b transitions --- panel epoch, buffer replacement,
    same-size geometry, detach --- leave the latch armed on this base.
    Each strands a live gesture whose release can never be accepted.
    That is inert while nothing consumes the latch, and becomes a defect
    exactly when replay supplies effects, in the branch that owns the
    row. `Absent` is wired anyway, because `publish_absent_panel`
    clears input authority two lines later; leaving it out would be an
    inconsistency inside one function rather than a clean deferral.

Five mutations, each biting only its named row:

  - drop the advance trigger (reactive cancellation) -> G5a alone
  - arm on every accepted pointer event -> the arming substrate alone
  - an ordinary `Up` no longer consumes -> the arming substrate alone
  - a consume counts as a cancellation -> the arming substrate alone
  - one global latch via a shared slot -> the ownership substrate alone

Also repairs a fourth rustdoc split on this branch. Inserting
`AcceptedPanelGesture` at what read as a blank gap adopted
`SemanticRenderState`'s doc comment AND its
`#[allow(clippy::struct_excessive_bools)]`, silently un-suppressing a
lint on the struct that needed it. Same mechanism all four times; the
ledger now records the check as "look UP from the insertion point".

Verified: `cargo fmt --check`; `cargo clippy --workspace --all-targets
-- -D warnings`; `cargo test --lib` (1956 passed); the two
`bottom_panel_stage2b_*` acceptance suites (39 passed); `git diff
--check`. The full eleven-stage `--protocol` gate is reserved for slice
completion per the standing procedure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth a242038e8a
test(panel): the nine family-gate rows, and three that proved nothing
The G6/G7/G8 matrix, plus the probe correction and the corrections
review found in my first attempt at these rows. This is the coherent
checkpoint: full eleven-stage `--protocol` gate, green.

**THREE OF THESE ROWS PASSED WITHOUT PROVING THEIR CLAIM**, and each
failed differently:

  G8b/G8d had no atomicity. G8b installed a LEGACY frame and then
  switched the session to Mapped, so `mapping_generation` was `None`
  throughout --- asserting it stayed `None` after the refusal asserted
  nothing. Each direction now uses an independent state, accepts a
  CORRECT-FAMILY baseline so there is real authority to preserve, and
  primes both pointer latches. Two new mutations pin it: clearing
  authority before refusing fails G8b, discarding the retained frame
  fails G8d. The family-gate mutation touched neither.

  G8e covered one direction. An authority check that only holds one way
  is one a peer walks around by choosing which identity to forge, so a
  mapped session now also fails to borrow a legacy identity --- and
  BOTH claimed identities have real registered sessions, or a
  payload-keyed lookup fails for want of a session rather than for want
  of authority. That was why G8e's own named mutation did not bite on
  the first attempt.

  G6b measured ambient state. It pre-focused the panel and then asserted
  against `active_window_id()`, which tracks `active_frontend` too ---
  satisfiable by a frontend switch that never routed anything. Every
  routing and refusal row asserts `views[fid].active` now, with the
  document precondition stated rather than assumed.

**And the probe measured the payload rather than the band, twice over.**
Its identity tuple was `(panel_epoch, geometry_epoch, size)`, which
ordinary content, focus, cursor and generation updates all leave
unchanged --- so accepted frames went uncounted, including the
identical-frame/higher-generation case this slice requires, and a
fixture waiting for two frames would wait forever. It snapshots the
complete accepted authority now, `(presented frame, mapping_generation)`,
and keeps the raw payload kind ONLY to tell a real `Absent` from a
refusal: inferring absence from `presented() == None` turned a rejection
into "the daemon says there is no band", a different fact entirely.

Nine rows, ten mutations, each biting its own:

  G6a legacy outbound            G7a mapped outbound, live generation
  G6b legacy inbound routing     G7b mapped inbound routing
  G8a bare from v25 refused      G8c mapped from v24 refused
  G8b legacy at v25 refused, atomically
  G8d mapped at v24 refused, atomically
  G8e both forgery directions
  plus: an Unsupported session accepts NEITHER family

G6c/G7c remain replay-lane effects.

**The gate earned its keep**: it caught a real regression I would have
shipped. `one_daemon_serves_a_v21_panel_session_and_a_shipped_v20_client`
counter-offers `PROTOCOL_VERSION`, now 25, so it is a MAPPED session
whose helper drained for legacy `Present` and timed out. Third suite
whose helpers assumed one family --- daemon acceptance, the GPU probe,
now GPU acceptance --- each written when only one family existed and
each quietly deciding what "a panel arrived" means.

Four `--protocol` runs were needed. Three failed on unrelated
signatures: the composition budget twice, in different steps, and
`setsid_escapee_is_not_reaped_and_teardown_reclaims_readers` once, a
new signature. All are recorded in the lane ledger rather than
`ci-red-signatures.md`, which ends at U9 here while the unmerged replay
branch already holds a U10.

Gates: all eleven green under `env -u TMPDIR` with `--protocol`,
log 20260815T185708Z, verified by exit status.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 4ecf09bf64
fix(gpu): positive family gates, a probe that reads accepted state, and a third split doc
**`Unsupported` WAS ACCEPTING LEGACY FRAMES.** I gated the legacy arm
on `!= Mapped`, and `Unsupported` is neither --- so a session below
`PANEL_MIN_VERSION` accepted a band it never negotiated. The
`carries_panel()` check I had in mind guards
`next_geometry_declaration`, a different seam entirely. Both present
arms gate on their POSITIVE family now, which is the shape that cannot
grow this hole again when a fourth family appears.

**AND THE PROBE MEASURED THE PAYLOAD, NOT THE BAND.** It recorded panel
facts before `apply_attach_message` ruled on the message, so once one
valid frame had landed, a REJECTED frame --- wrong family, invalid,
stale generation --- still supplied the expected text while the
retained old frame supplied the rendering. The probe would report the
band showing something it does not show, which is a false positive in
the one place that exists to tell us the band is real. (The false
NEGATIVE, observing only the legacy family, was the previous commit.)

Facts now come from `state.panel.presented()` after the apply, and
`panel_frames` counts only when the retained frame's identity actually
moved: a duplicate or a refusal leaves the band exactly as it was, and
counting either would say the daemon is painting when it is not.

**Third rustdoc split in this slice**, same mechanism each time ---
`screen_size`, `peer_may_send_panel_events`, now
`send_panel_pointer`. I insert a function at what reads as a gap
between declarations, when the lines above it are the NEXT function's
documentation. The check is to look UP from the insertion point, not
just down, and I will apply it rather than keep reporting the same
correction.

Verified by exit status: clippy 0, `-p pmacs-gpu` 0 (271 passed).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 7e28eb8936
feat(gpu): one negotiated panel family, used for acceptance and production
The frontend half of bilateral gating. The nine discriminating rows
(G6a/b, G7a/b, G8a-e) and the full protocol gate are the checkpoint
after this.

**ONE ENUM, DERIVED ONCE.** `PanelFamily::{Unsupported, Legacy,
Mapped}` replaces the `panel_wire` bool, classified from
`session_protocol_version` at all four existing sites --- initial
attach and each reconnect --- and read by BOTH payload acceptance and
pointer production. Deriving those two independently is how a frontend
ends up accepting one family while producing the other: it would speak
v25 inbound and v24 outbound, and neither side could tell.

Acceptance refuses both wrong-family cases, atomically: a mapped
session rejects legacy `Present` rather than painting a band whose
cells it cannot safely invert (G8b), and a legacy session rejects
`PresentMapped` (G8d). The mapped arm also refuses generation zero and
any generation BELOW the one held --- nondecreasing, so a frame delayed
across a hide cannot roll authority backward --- while a duplicate
frame at a HIGHER generation is still accepted, because the daemon has
re-keyed the mapping and echoing the stale value would have every
gesture refused.

Production goes through one family-aware sender used by both send
sites, so they cannot drift. A mapped session with no retained
generation sends NOTHING rather than the legacy variant: falling back
is the frontend half of the bypass, and the daemon refuses it anyway.

**And the live probe observed only the legacy family.** Left alone,
mapped production could have worked end to end while the probe reported
no panel --- a false negative in the one place that exists to tell us
the band is real. It matches both now.

One thing NOT done here, deliberately: the gesture-latch reset on an
identity change is R-d, owned by `panel-pointer-replay`. I had copied
it into the mapped arm before noticing `gesture_last_content_cell` does
not exist on this branch --- it is replay-lane state. Two branches
resetting the same latch would conflict at the rebase and neither would
own the contract, so this arm installs the frame and its generation and
nothing more.

Verified by exit status: clippy 0, `-p pmacs-gpu` 0 (271 passed).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 65eef564bf
fix(panel): one negotiated version per row, a literal boundary, and a reattached doc
Three review corrections ahead of the witness rows.

**THE LEGACY CONTROLS NEGOTIATED TWO DIFFERENT VERSIONS AT ONCE.** I
moved their `SessionRegistry` to v24 and left their retained
`SemanticRenderState` on `PROTOCOL_VERSION`, so the producer shipped
`PresentMapped` to a session the daemon believed was v24. The rows
passed, which is the problem: a control that negotiates two versions
proves nothing about either. Five producers across the four rows now
take `LEGACY_PANEL_VERSION`, the same constant the registry does.

**The boundary is a LITERAL 24, not `PANEL_MAPPING_MIN_VERSION - 1`.**
G6/G14 make the family boundary absolute; arithmetic against a moving
constant would drag these rows forward on the next bump and they would
silently begin testing v25 as "legacy".

**And I split a rustdoc from its function again.** Inserting
`peer_uses_mapped_panel_family` above `peer_may_send_panel_events` left
the latter's doc block stranded, so my function inherited two
incompatible descriptions and the documented one had none. Same mistake
as `screen_size` two commits ago --- inserting an item at what looks
like a blank line between declarations, when the line above is the next
function's documentation. Each block sits directly above its own
function now.

Verified by EXIT STATUS, not by reading filtered output: `cargo test
--lib` 0 (1945 passed), clippy 0, focused suite 0 (37 passed). The
`composition_overhead_under_ten_percent` flake appeared once mid-run
and passed isolated; it is the known wall-clock budget signature, not
this diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 0fff640d15
feat(panel): inbound family gating and the mapping check
The receiving half of bilateral gating, plus the constructor fix review
found. The eight G6/G7/G8 witness rows and the GPU side are next; the
full eleven-stage `--protocol` gate runs once they discriminate.

**THE FAMILY IS DECIDED FIRST, FROM THE AUTHENTICATED SESSION.**
`peer_uses_mapped_panel_family` reads `session_state(source)`, never the
payload's `frontend_id` --- that field is untrusted on every inbound
variant, and looking negotiation up by it would let a peer claim
another session's family. The order is stated at both arms: family,
then the existing epoch ladder, then the mapping generation, then
dispatch. It cannot depend on the variant's contents, because it
decides which variant is admissible at all.

Both wrong-family cases are REFUSALS, not fallbacks. A `>= v25` session
sending the bare `PanelPointer` is dropped rather than handled under
legacy semantics --- handling it would leave the mapping hole reachable
by choosing a discriminant, which is the entire bypass. A `<= v24`
session sending `PanelPointerMapped` is dropped too, even though a peer
compiled from this crate can encode the discriminant: negotiation is a
gate, not a sender convention.

`panel_mapping_is_current` is the ladder's finest rung. `buffer_id`
catches an A->B replacement, `panel_epoch` a close/reopen,
`geometry_epoch` a declaration race, and this catches the text under
the cell changing --- a foreign edit, a fold, a reload, none of which
moves an epoch. Zero is refused outright, and the check reads through
the same accessor projection stamps with, so the two cannot drift.

**And `new()` contradicted its own contract.** It documents a
current-build peer and enables every other current capability, but I
initialised `peer_knows_mapped_panel` to `false` --- so an implicitly
current peer was sent the LEGACY family. Set true, with the doc
extended to say the assumption covers later capabilities too.

**Four daemon rows broke, and that is G8a firing.** They drive
`FrontendEvent::PanelPointer` through sessions negotiated at
`PROTOCOL_VERSION`, which is now 25 --- so the bare variant is refused,
correctly. They negotiate `LEGACY_PANEL_VERSION` now, which both fixes
them and makes them explicit legacy positive controls rather than rows
that happened to pass.

Note for the eventual rebase: this branch is based on main, where
`dispatch_semantic_panel_pointer` still takes four arguments. Threading
`mods` is R-a, owned by `panel-pointer-replay`; the mapped arm
destructures `mods` into `..` here and will pass it through when the
lanes meet.

Verified: focused suite 37/37, `cargo test --lib` 1945 green, clippy
clean.

**Amended.** The first version of this message claimed the lib suite
green when it was not: I piped `cargo test` through `tail`, so the
pipeline exited 0 and my `&&` chain committed on a failure I had not
read. The four rows above are what it was reporting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 7da2f34b94
feat(panel): outbound family selection (G6a/G7a half)
The producer half of bilateral gating. Inbound routing, the four
refusal quadrants and the positive routing controls are the next
commit; the full eleven-stage `--protocol` gate runs at that
checkpoint, once all of them discriminate.

`SemanticRenderState` gains `peer_knows_mapped_panel`, set in
`for_peer` beside the six existing capability bools --- the pattern
this codebase already uses to bake a negotiated version into the
producer. A `>= v25` peer receives `PresentMapped`; every older peer
receives legacy `Present`, unchanged.

**THE ORDERING IS LOAD-BEARING AND IS WRITTEN DOWN AT THE SITE.**
Projection first, THEN capture-and-advance the mapping key, THEN
construct the payload. A terminal projection registers the view whose
scroll anchor the key reads, so capturing earlier stamps a frame with a
key derived from an anchor that does not yet exist.

One case is deliberately not a fallback: if a `>= v25` peer has no
presentable mapping to stamp, the producer publishes `Absent` rather
than dropping to legacy `Present`. Falling back would hand a peer the
family it did not negotiate --- the bypass the gate exists to close,
arriving from the daemon's own side.

**Thirty existing rows broke, and that is the gate working.** The
fixture negotiates `PROTOCOL_VERSION`, so every panel test suddenly
received the mapped family where it matched `Present` literally. Those
rows are about the PROJECTION, not the wrapper, so the fixture helpers
are family-agnostic now and which family carries a frame is pinned by
the G6/G7/G8 rows rather than incidentally by thirty others.

Verified: focused suite 37/37, `cargo test --lib` 1945 green, clippy
clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 921989a77c
test(panel): the terminal scroll-anchor row, which I had given up on
Closes the fourth terminal gap. The row I could not drive was
reproducible; **my delta had the wrong sign** --- `scroll_view(key,
viewport, 3)` moves, `-1` does not --- and I concluded the fixture was
at fault after three attempts rather than trying the other direction.

The sequence: `frame()` registers the panel terminal view, the key is
built from the side window, the viewport is `panel_grid_size` less its
mode-line row, forty published line feeds build the history, and the
baseline is taken AFTER that history exists. Taking it after is what
makes the leg discriminating --- a constant anchor leaves the
post-history baseline unchanged, so the row fails even while
`mapping_revision` is perfectly live.

Two mutations, each failing this row alone:

  constant ANCHOR, live revision   -> the anchor is not in the key
  constant REVISION, live anchor   -> the screen is not in the key

Neither passes on the other's evidence, which is the separation the
terminal half needed: `screen.rs` proves the counter classifies events,
the domain row proves the branch is taken, and these two prove the
daemon's key actually reads both halves of what
`view_mapping_identity` returns.

The owed-witness note is removed from the ledger.

Verified: focused suite 37/37, clippy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 0bdfc9b6dd
fix(panel): the terminal mapping half --- classification, domain, publication
Three of the four terminal gaps. The fourth is recorded as owed rather
than faked; see below.

**THE STABLE CLASSIFICATION WAS INCOMPLETE.** Cursor motion still
advanced the mapping revision: `restore_cursor`, `horizontal_tab`,
`move_vertical`, `move_horizontal`, `set_col` and `set_row` all called
`changed()`. Moving the caret denotes nothing new, and a child that
merely repositions its cursor would have cancelled a drag. All six take
the display-only path now.

Worse, **rewriting the same glyph under another style advanced it**,
which is precisely the control SS5b requires to hold. `write_character`
now compares the glyph before writing --- sampled BEFORE
`clear_wide_at`, which blanks a cell that is part of a wide pair and
would otherwise make every rewrite look like a change. That ordering
was found by instrumenting the failing row, not by reading the code.

**THE SNAPSHOT CARRIED DOCUMENT-ONLY STATE FOR TERMINALS.**
`view_top`, `view_left`, wrap, content columns, fold policy and folds
describe a document projection and take no part in a terminal's, where
the child's screen decides the mapping. They live inside the `Document`
arm now; only common geometry --- buffer identity, rows, columns ---
stays outside.

**AND THE REVISION WAS NOT PUBLICATION-CONSISTENT.**
`view_mapping_identity` read the LIVE screen revision while
`projection_ref` returns the last PUBLISHED cells, so buffered output
under synchronized-output would stamp displayed cells with authority
they were never painted under --- a frontend echoing a generation
matching nothing it can see. `ScreenProjection` carries
`mapping_revision` now and the published value is what is read.

**The witnesses were separated across the seam**, which review named
exactly: `screen.rs` proved the counter, the daemon proved enum
selection, and a `view_mapping_identity` returning a constant would
have left both green. A daemon-level row now drives real events through
a panel terminal and asserts the daemon's generation moves on a new
glyph and holds across a style-only rewrite and across cursor motion.

**OWED, NOT DONE: the scroll-anchor row.** The anchor is in the key,
but three attempts failed to drive a scroll from this fixture ---
`scroll_lines` wants a viewport the projection registers on its own
schedule, and `scroll_view` with an explicit size reports no movement
after forty line feeds. Recorded in the ledger rather than faked or
quietly dropped: without it, a constant ANCHOR alongside a live
revision still passes every terminal row that exists.

The two test hooks are `#[doc(hidden)] pub`, not `#[cfg(test)]`,
because the rows needing them are integration tests and those link the
library without `cfg(test)`.

Verified: focused suite 37/37, `cargo test --lib` 1945 green, clippy
clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 42efc3618f
fix(panel): the terminal domain, an exact key, and two witnesses that were wrong
All four closure gaps in one correction. Three of them are defects in
what I committed as G1-G4; the fourth overturns a claim I made about
what could not be witnessed.

**THE KEY IS NOW EXACT, NOT PROBABILISTIC.** It was a `DefaultHasher`
digest, so authoritative equality rested on the absence of collisions
--- and a collision silently ACCEPTS a stale gesture, which is precisely
the failure the key exists to prevent. It is a `PanelMappingSnapshot`
struct compared structurally now. The emitted `mapping_generation`
stays a `u64` on the wire; only the daemon's own comparison changed.

**THE TERMINAL DOMAIN WAS ABSENT, AND THE BUFFER REVISION WAS WRONG.**
The key hashed the panel buffer's content revision for every target
kind. For a terminal that is doubly wrong: SS5b says the buffer revision
does not decide the mapping, and what does --- the screen --- was not
consulted at all. `PanelMappingContent` now splits by kind, and
terminals carry the screen's mapping revision plus the view's scroll
anchor.

That revision had to be built. `Screen::generation` cannot serve:
it advances from 39 sites including style, title, bell, tab stops and
cursor motion, none of which changes what a coordinate denotes.
`Screen` now carries `mapping_revision`, and the classification FAILS
SAFE --- `changed()` bumps both by default, and only the eleven
explicitly display-only arms call `display_only_changed()`. Anything
unclassified is treated as content, because over-cancelling a gesture
is a nuisance while under-cancelling one lets a stale coordinate reach
a child.

**"NO PRODUCTION PATH REACHES A TRANSPOSITION" WAS WRONG.** I recorded
the rows/cols product mutation as unwitnessable and kept the separate
hashing on principle. Resize plus redeclare reaches it: 4x80 -> 8x40
holds the area at 320 while swapping the dimensions, and
`last_content_cols` is not refreshed until the next render, so the two
grid fields are isolated. The row exists and the product mutation now
fails.

**G3 WAS INCOMPLETE.** It covered idle and cursor only. Focus is added
at the daemon level --- the tempting error is folding the whole frame,
which carries a `focused` flag, into the key. Styling is pinned
structurally instead: the snapshot has no style field, so there is
nothing a recolour could touch. The terminal controls live in
`screen.rs`, at the level the classification lives, with a positive
half so a revision that never advanced at all cannot pass them.

**AND MUTATION TESTING FOUND ANOTHER UNWITNESSED BRANCH.** Routing
terminal panels through the DOCUMENT arm left all thirty-five rows
green --- the `is_terminal` branch had no daemon-level witness at all.
A row now pins that the snapshot picks its domain by target kind, and
that mutation fails.

Mutations: display-only events bumping the mapping revision (the
screen-level control fails); terminal panels keyed on the buffer
revision (the domain row fails); rows*cols as an area (the
transposition row fails); plus G1-G4's original five, still biting.

Verified: focused suite 36/36, `cargo test --lib` green, clippy clean.
Full `--protocol` gate reserved for the checkpoint after bilateral
gating, per the standing procedure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 063db52555
feat(panel): the authoritative cell-mapping key (G1-G4)
The key itself, its domain, and the witnesses for both. No gating and no
replay yet.

**DERIVED FROM A FINGERPRINT, NOT BUMPED AT MUTATION SITES.**
`panel_mapping_fingerprint` hashes what actually decides which byte a
cell means --- buffer identity, grid rows and columns, `view_top`,
`view_left`, wrap mode, content columns, fold POLICY and fold CONTENT,
and the buffer's content revision --- and the generation advances
whenever that changes. This makes the changing/stable split
STRUCTURAL: an input that is hashed moves the key by construction, and
one that is not cannot. Bumping by hand at each mutation site would
have made "advances after any mapping mutation" a promise about
someone remembering.

Folds are hashed at their SOURCE, the registry's ranges, rather than
through the derived `VisibleLineMap`, whose only public summary is
`is_identity()` --- too coarse, since a fold edit that leaves the map
non-identity still changes which source line a row shows.

**ONE SEAM, READ BY BOTH SIDES.** `panel_mapping_generation` advances
if the fingerprint changed and returns the current value; projection
will stamp with it and inbound validation will compare against it, so
"what the frontend was shown" and "what the daemon checks" cannot
drift. Computed ON DEMAND, deliberately: a mutation not yet painted has
still changed the inverse, and a gesture arriving in that gap must be
refused. Deriving from the last emitted frame recreates the hole.

**Nondecreasing, and never cleared.** `Absent` yields no key to stamp
--- which is not a key of zero --- but the high-water mark survives, so
a frame delayed across a hide cannot roll authority backward. First
establishment takes 1; zero is the wire's invalid value.

**MUTATION TESTING FOUND MY WITNESSES UNDER-SPECIFIED, TWICE.**

With only the content-edit row present, dropping `view_left` from the
key stayed GREEN, and so did collapsing the grid to `rows * cols`. That
is exactly what the closure predicted --- "a key that ignores
`view_left` passes every row that only scrolls vertically" --- and it
is why G2 is enumerated per input rather than asserted in aggregate.
Six legs now, one per input, each touching only its own.

Mutations that bite: omit the content revision (3 rows), omit
`view_left`, omit wrap, omit fold policy, and include the CURSOR --- a
stable input, caught by G3.

**One mutation does NOT bite, and the row says so rather than
pretending.** Collapsing rows and columns to a product stays green,
because `last_content_cols` co-varies with a column change and the row
count co-varies with a resize: the key moves by another input either
way. Only a transposition (2x6 -> 6x2, identical product) would isolate
it, and no production path reaches one --- rows come from the band's
height, columns from the frame declaration, and nothing swaps them. The
key hashes them separately anyway; hashing a product because no test
can currently tell the difference would be choosing the weaker
construction for the suite's convenience.

G4b --- that an in-flight drag survives a selection repaint through
real replay --- stays owed by the rebased replay lane, which is the only
branch where replay exists.

Verified: focused suite 34/34, `cargo test --lib` green, clippy clean.
Per the standing procedure the eleven-stage `--protocol` gate is
reserved for the next coherent checkpoint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 758b985c35
feat(protocol): the mapped panel family --- v25 wire shapes and their pins
SS5b's first implementation commit: the two appended variants, the
version constants, and the pins that hold them in place. No gating, no
key, no replay --- those are the next commits, and the variants are
REFUSED everywhere until their gate lands.

**APPENDED AT THE TRUE END, confirmed by the discriminants.**
`PanelPointer` is 15, `TextInput` 16, `PanelPointerMapped` **17**;
`Present` 0, `Absent` 1, `PresentMapped` **2**. "Beside `Present`" would
have been adjacent insertion, which shifts every discriminant below and
silently re-interprets an older peer's bytes. `mapping_generation` is a
`u64`, last within each variant, documented invalid at zero --- the
value a default-constructed sender produces, so accepting it would let
a peer opt out of the check by sending nothing.

**THE COMPILER NAMED EVERY SEAM.** Four non-exhaustive matches:
`semantic_render`'s declaration accessor now sees through both
families, and the three routing sites REFUSE the mapped variant rather
than unwrapping it to legacy meaning. Refusal is the correct default at
an intermediate commit, not a placeholder --- until the frontend can
prove it negotiated v25 it IS a `<= v24` peer for gating purposes, and
painting first would ship a window in which the band is hit-tested with
no mapping identity at all.

**Five mutations, each biting its own rows:**

  insert `PanelPointerMapped` before `TextInput`
      -> the TextInput pin and the mapped pin. `PanelPointer`'s v23 pin
         correctly SURVIVES: its discriminant did not move, which is the
         "only the pin whose discriminant moved fails" behaviour G0a
         specifies
  insert `PresentMapped` before `Absent`
      -> the Absent pin and the mapped-frame pin
  swap `geometry_epoch` / `panel_epoch`
      -> the exact-bytes assertion, while the round-trip stays green.
         That is the blind spot G0b exists for, and it is why every
         adjacent same-typed field carries a distinct value
  bump the wire version without extending the supported set
      -> both new tripwires and 1a's v6 ladder
  move `ADVERTISED_PROTOCOL_VERSION` to 25
      -> the baseline pin

**Version fallout, enumerated rather than discovered one gate at a
time.** Four acceptance-suite tripwires (`bottom_panel_stage2b_gpu`,
`discovery_stage2` x2, `vterm_stage3`, `statusline_segments`) each say
"a wire bump must be a conscious edit here" and each worked. Rather
than fix them one run at a time I grepped the tree for version
assertions and updated all four in one pass.

Review folded five further corrections, two of which fix reasoning of
mine that was wrong:

  - I claimed reversing `frame` and `mapping_generation` "fails to
    compile" because they are different types. **False for NAMED
    variant fields** --- the initializer uses names, so reordering the
    declarations compiles and shifts postcard's positional bytes
    silently. The pin is the only thing catching that.
  - Ladder loops now track `PROTOCOL_VERSION` while TRIPWIRES stay
    literal. I had flattened both to `25`. A tripwire is literal so a
    bump is a conscious edit; a ladder must move, or the next bump
    silently stops testing the top rung. G14b is unaffected ---
    `PANEL_MAPPING_MIN_VERSION` stays literal, because there the
    arithmetic is exactly the hazard.
  - `assert!(24 < MIN)` was a compile-time tautology holding for every
    value above 24. Replaced with the literal equality plus
    `assert_ne!` against `TEXT_INPUT_MIN_VERSION`: the mapped family
    must not share v24's gate, or it is admitted on sessions that
    negotiated only `TextInput`.
  - Statusline support loop reaches `PROTOCOL_VERSION`; public protocol
    history records v25.

**CI-red observations are in the LANE LEDGER, not the registry**, and
that is deliberate: `ci-red-signatures.md` here ends at U9 while the
unmerged replay branch already added a U10, so a row from this branch
would duplicate an id or invent one blind --- which this file's own
history records going wrong, two branches' entries merging "without a
conflict, producing duplicate ids across four sites". R7 twice and the
composition budget once, fragments verified, owed to the registry by
whichever branch merges second.

Gates: all eleven green under `env -u TMPDIR` with `--protocol`,
log 20260815T103555Z. Four runs were needed; three were lost to those
two signatures, not to this diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 775a046ce4
docs(framing): fold the mapping-generation closure audit into revision 16
Close the producer, receiver, protocol-family, and gesture-lifecycle
cross-products in one pass. Separate route witnesses from replay effects,
freeze both old boundaries and new variant fields, and record the bounded
review method for the remaining chain.
2026-08-20 17:55:21 +02:00
Levi Neuwirth 137c7fc736
docs(framing): SS5b revision 16 --- the producer rule was self-defeating, and R7 recurs on a docs-only diff
Answers review of 15. Framing only. Three items reverse a rule 15
introduced, and one retracts a mutation that was not a defect.

**THE PRODUCER RULE CONTRADICTED PROACTIVE CANCELLATION.** The daemon
cancels BEFORE emitting the replacement frame, so the frontend needs
only to clear its local latch when that frame arrives, and send
nothing. Revision 15 asked it to emit a cancellation tail or retain the
latch: the tail is redundant --- the daemon would receive a release for
a gesture it has already settled, which is the duplicate release the
latch exists to prevent --- and RETAINING IS ACTIVELY HARMFUL, because
it manufactures a `Drag` under the NEW generation with no accepted
`Down`. That is the exact orphan the section exists to prevent,
produced by the rule meant to prevent it.

Ordering is what makes the simple rule safe: cancel, then emit. The
frame's arrival IS the cancellation signal; no second channel is
needed. Witnessed as `Down` -> key advances -> replacement frame ->
motion and physical `Up` produce no new drag and no duplicate release.

**THE LATCH HAD ONE TRIGGER AND NEEDED FIVE.** Cancellation runs on
every loss of gesture authority: generation advance, `Absent`, panel or
buffer identity change, geometry-epoch change EVEN AT AN UNCHANGED CELL
TOTAL, and detach. And an ordinary accepted `Up` must clear the latch,
or a later invalidation finds a gesture it believes live and
synthesises a duplicate release for a button already up --- the replay
lane's D1/D2 orphan race, arriving from the daemon's side.

**G9b's MUTATION WAS A VALID IMPLEMENTATION, NOT A DEFECT.** Keying the
dedupe by `(mapping_generation, coord)` preserves same-generation
suppression and naturally admits the first motion under a new
generation. Requiring it to fail would have forbidden a correct design.
Replaced with two real defects: compare only the cell and never key or
reset by generation (the first post-change motion is eaten), and reset
on every same-generation repaint (pixel-rate traffic returns).

**"PROJECTED CELL IDENTITY" CONTRADICTED THE STYLING CONTROL** in the
same section. The wire `Cell` derives `PartialEq` over `glyph`, STYLE
and `attachment` (`pmacs-protocol/src/cell.rs:153`), so an identity
keyed on cell equality moves on a pure recolour --- while the stable
controls rule style out. Terminal identity is now glyph and row
TOPOLOGY plus the view anchor, excluding face, style and cursor, with a
same-glyph/different-style control: the row that catches an
implementation reaching for `Cell` equality because it is right there.

P2s: zero-generation rows added in BOTH directions as independent legs
(a valid `PresentMapped` with generation zero must be rejected
atomically; a zero-generation `PanelPointerMapped` must be refused);
G7 split into outbound mapped-frame and inbound mapped-pointer legs,
since its old mutation only withheld the frame; G2's grid rows/columns
and fold-map-content/`fold_projection`-policy composites split; and
SS20 now names journey steps 5 and 8 while stating neither grade
changes --- an auditor scanning for grade movement alone would
otherwise conclude this slice touches no journey.

**AND R7 RECURRED, ON A DIFF THAT IS ENTIRELY DOCUMENTATION.** The
first `--protocol` run of this tree failed the `gpu` step on
`managed_retry_survives_transients_and_uses_the_successful_stream`,
with all three required fragments verified from the durable log
(`20260815T072601Z`). Recorded as R7's FIFTH occurrence.

It carries the strongest tree exclusion the row has had: occurrences 1
and 4 argued "unrelated lane", while this branch cannot be related at
all --- no Rust, no wire surface, no `pmacs-gpu` file. The line moved
to `attach.rs:1728` from `:1680`, which the row already treats as
occurrence-specific rather than a fragment. Isolated rerun green, and
the full gate green on the re-run (271/271 in the `gpu` step) --- which
per this file's rerun rule establishes INTERMITTENCE ONLY, though here
there is no tree change to exonerate.

What five occurrences across three flavors and five unrelated lanes now
support is that the failure is NOT LANE-CORRELATED. That is evidence
about where the cause is not. The retirement condition is unchanged.

Gates: all eleven green under `env -u TMPDIR` with `--protocol`,
log 20260815T073556Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth fa980ef1ba
docs(framing): SS5b revision 15 --- appended means LAST, and cancellation had a race
Answers review of 14. Framing only. Four of the six reverse something
14 asserted, and a green protocol gate would not have caught any of
them.

**"BESIDE `Present`/`PanelPointer`" WAS POSITIONALLY DANGEROUS.**
"Beside" reads as adjacent, and adjacent insertion shifts every
discriminant below it --- the exact hazard the appended-only rule
exists for. Appended means LAST: `PresentMapped` after `Absent`,
`PanelPointerMapped` after `TextInput`, with a diagram so the next
reader cannot re-derive it wrongly. Field order is stated exactly,
`mapping_generation` is a `u64`, ZERO IS INVALID --- it is what a
default-constructed or half-initialised sender produces, so accepting
it would let a peer opt out of the check by sending nothing --- and the
gate reads `PANEL_MAPPING_MIN_VERSION = 25` rather than a literal.

**BLANKET REFUSAL STOPPED THE WHEEL AFTER ONE TICK.** The first
effective document wheel changes `view_top`, which advances the key, so
the next already-queued tick carries the old generation and is refused:
the panel scrolls once and goes dead until the frontend observes the
new frame. Local terminal scrollback has the same shape.

The discriminator is whether the gesture USES its coordinate.
Coordinate-free gestures --- the document wheel, non-reporting terminal
scrollback --- cannot be mis-aimed by a stale mapping and are EXEMPT.
A child-reported wheel is the opposite case: SGR carries row and
column, so a stale one aims an application action at a cell the user
never pointed at, and it keeps the check. Two-tick witnesses added,
because without them a blanket-refusal implementation passes every
single-event row in the matrix.

**CANCELLATION WAS REACTIVE AND LOSES A RACE.** If the replacement
mapped frame reaches the frontend before the physical `Up`, the
producer resets `pointer_held` and SUPPRESSES THE VERY EVENT that would
have cancelled --- so the daemon is never told, the selection stays
armed, and the child keeps holding its button. It is now PROACTIVE,
triggered by the authoritative key advancing while a gesture is
accepted, and the producer must emit a cancellation tail or retain the
latch rather than clearing first.

That needs state 14 assumed and never specified: an ACCEPTED-GESTURE
LATCH recording whether the `Down` was accepted, whether it reached the
child, and the coordinate, button and encoding a release must match.
Two rules fall out and are ruled here --- a stale `Up` with no accepted
`Down` is INERT, and cancellation NEVER reclaims a controller another
frontend has since taken, because a stale gesture must not steal a live
one's terminal.

**THE EXISTING SCREEN GENERATION CANNOT BE THE TERMINAL KEY.**
`Screen::changed()` bumps from 39 call sites including `SetStyle`,
`Bell`, the tab-stop operations, cursor-only motion and `SetTitle`.
None of those change what a coordinate denotes, so keying on it would
cancel a drag every time the child recoloured a character. A dedicated
terminal mapping revision is defined over projected cell identity,
retained-row identity and the per-view scroll anchor --- with those
five events as explicit STABLE CONTROLS, so a reader who later reaches
for the convenient counter fails a test instead of shipping a cancelled
drag.

**G5'S EFFECTS ARE NOT PROVABLE ON THIS BRANCH**, and 14 claimed them.
`gesture_last_content_cell` and the document/terminal replay exist only
on `panel-pointer-replay` (`pmacs-gpu/src/main.rs:2143` there); the
same struct here is at `:2124` with no such field. The obligations are
split in a table. G5a --- that the key advancing RAISES cancellation
--- stays here on purpose: the trigger is this slice's rule, and moving
the whole row out would leave the proactive ruling with no witness in
the slice that introduces it.

P2s: mutation legs split (wrap vs gutter, terminal content vs
scrollback, G5a-c, G8a/b, G9a/b); G7 given a positive-path mutation;
G11 expanded --- exhaustion must publish `Absent`, clear input
authority, cancel any accepted gesture and LATCH, or a stale panel
stays painted and permanently inert; the v26 correction finished at the
gate and old-peer cells (`:573`); and the SS20 impact statement added
--- hardens an existing panel island, no journey grade changes, no
config, no background work.

**And the pin correction is mine to make: it EXISTS**, at
`src/protocol.rs:1975`, in the ROOT crate's test module rather than
under `pmacs-protocol/` or `tests/` --- which is exactly where I
searched. `message.rs:524` was right and the doubt was wrong; the
contrary claim is removed from both records.

Gates: all eleven green under `env -u TMPDIR` with `--protocol`,
log 20260814T180105Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth ca816a1917
docs(framing): the panel cell-mapping generation (v25) --- SS5b revision 14
Own branch, own slice, protocol-bearing, runs alone. Framing only; no
implementation. Blocks `panel-pointer-replay`, which blocks GUI arc 1b.

Answers review of revision 13. Every item below reverses or completes
something 13 got wrong.

**GATING IS REFUSAL, NOT FALLBACK.** Revision 13 said a bare
`PanelPointer` from a new peer would be "handled under the old
semantics". That is a BYPASS: it leaves the exact hole this slice
exists to close, reachable by omitting a field. A >= v25 session
sending the legacy event is REFUSED before mutation, and a >= v25
frontend REJECTS a legacy `Present` rather than painting a band it
cannot safely hit-test. Only negotiated <= v24 keeps legacy semantics;
`Absent` stays common to both families.

**ONE AUTHORITATIVE PER-FRONTEND KEY**, used by projection AND inbound
validation, advanced after any mapping mutation and BEFORE the next
inbound pointer is handled --- whether or not anything has rendered.
Comparing against the last EMITTED frame recreates the hole, because a
mutation not yet painted has still changed the inverse mapping.

**STALE TAILS TERMINATE; THEY DO NOT VANISH.** A blanket drop breaks
liveness: a refused `Up` leaves an empty document selection armed with
a stale anchor, and leaves a reporting terminal child HOLDING A BUTTON
FOREVER. Cancellation is now a ruled outcome --- producer latch reset,
daemon selection and click-chain cleanup, and the child's release
delivered at the last coordinate known good. A cancelled gesture is
explicitly not a replayed one: the release is for liveness, and no
selection or scroll effect is applied from the stale event. Stale
BEGINNINGS may still simply drop.

**THE DOMAIN WAS INCOMPLETE.** `view_left` is added, because 1b makes
horizontal scrolling real. "Cursor movement is stable" is now
CONDITIONAL: a cursor move that triggers vertical or horizontal follow
changes `view_top` or `view_left` and therefore does change the
mapping. Terminal panels are ruled explicitly --- their coordinates are
decided by the SCREEN, so output and scrollback movement change the
generation while their buffer revision does not.

**SS5b HAD NO ACCEPTANCE MATRIX.** G1-G11 now cover the foreign edit
before render, every changing and stable domain entry ROW BY ROW, a
selection repaint that must preserve the generation and let a drag
continue, mid-gesture cancellation, v24 and v25 positive controls with
both wrong-family refusals, identical cells across a generation change
still emitting, atomic retention of frame and generation on an invalid
frame, and fail-closed exhaustion. The per-entry enumeration is
deliberate: one aggregate row cannot show WHICH input moved the key,
and a key ignoring `view_left` passes every vertical-only row.

**MAPPED MOTION KEEPS ITS COALESCING TAGS.** A new variant falling
through to the lossless default would put pixel-rate `Move`/`Drag` on a
bounded queue.

**PINS ACCUMULATE.** Revision 13 said the pin "moves", which would
delete coverage of the shape it protects. `PanelPointer` is retained;
exact `TextInput` bytes are added as the previous-final
`FrontendEvent`; the complete nested `PanelFrame(Absent)` bytes are
added as the previous-final `PanelFramePayload`. Recorded honestly: I
could find NO exact-bytes pin for `PanelPointer` anywhere in the tree,
though `pmacs-protocol/src/message.rs:524` says one is in the tests.
Either my search missed it or the doc overclaims; this slice resolves
it either way, since it must add exact pins regardless.

**AND THIS SLICE OWNS THE VERSION CORRECTION.**
`docs/gui-stage1-input-framing.md` now says 1e's `OpenTarget` is
**v26**, with the reason stated at the top. An expected rebase conflict
on `gui-stage1b-pointer-scroll` is not grounds for leaving the
canonical document false --- which is what I argued last round, and it
was wrong. `ADVERTISED_PROTOCOL_VERSION` stays pinned at 20.

Gates: all ELEVEN green under `env -u TMPDIR`, with `--protocol`
(`build-crdt`, `sweep-crdt`), log 20260814T162843Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 5f2015c26f
docs(handoff): a timeout wrapper around the gate is not a result
Section 3 already forbids starting the gate from a shell that ignores
SIGINT, and already records that an ordinary tool-level background
launch is measured deliverable. It did not cover the other way a
harness convenience turns into false evidence.

The 16-stage suite outruns a ten-minute agent command cap. Wrapping it
in `timeout 580` kills sweep mid-run, and the runner records that stage
as a failure --- indistinguishable in the log from a real red. That
produced one false SS5b gate result.

Records the supported alternative, which is the tool-level background
launch the section already vouches for, and the fallback of labelled
pieces with the record saying which piece produced which result.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:32:34 +02:00
Levi Neuwirth 24e4039eb6
docs(lane): record the one rd_precondition failure, without a cause
The row rd_precondition_validates_the_whole_conformance_set failed once
in a sweep-crdt run on 2026-08-20 and passed on the two sweeps after it.
The message was not captured, so nothing here explains it --- the
occurrence is recorded and the diagnosis is not.

Also withdraws a mechanism I offered for it. I described the test as
spawning 46 concurrent stubs under load; it runs 45 stubs SEQUENTIALLY
plus one intentional nonexistent-path spawn probe, so there is no
concurrency to be pressured and 46 was a miscount. Thirty consecutive
user-run repetitions at load ~10.5 --- 1,350 stub executions --- did not
reproduce it.

Records the standing instruction that a recurrence must capture the
exact case and error before anyone theorises again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:31:13 +02:00
Levi Neuwirth f13506caf5
docs: absorb the SIGINT-guard lane --- #241 merged at f8033bc
Records what the lane closed, measured rather than argued.

A6a is closed by measurement: status 1 with no token classifies as a
boundary error, never `ignored`, green on macOS --- the platform whose
shell exits 1 for an exec failure, which is what produced the original
defect and what a status-only ABI could not distinguish.

A7 stops being "satisfied by disclosure". Both macOS flavours exercised
the helper and gate consumers across the full 45-case shared set. The
R-d consumer stays Linux-only, because its test is crdt-gated while the
macOS jobs build without crdt and Test (crdt) is ubuntu-only --- recorded
as an open gap rather than quietly closed.

Also records that the two m4_24_* rows failing locally under crdt do not
reproduce in CI, at this branch or at 72da24a: local-environment
-specific, not a code defect and not this lane's.

panel-mapping-generation is unblocked, and its sixteen-stage gate must
run in the foreground --- the condition its stage 15 always needed.

Per the standing convention this absorption does not advance any
canonical base to its own commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 15:43:46 +02:00
Levi Neuwirth f8033bc245
Merge pull request #241 from levineuwirth/gpu-probe-sigint-teardown
gate: refuse to run when SIGINT is not deliverable
2026-08-20 13:42:37 +00:00
Levi Neuwirth 5089715737
docs(tests): describe sentinel-bearing cases precisely
X3 and X4 deliberately use dedicated stderr payloads, so describe the
sentinel as belonging to the branch-discriminating cases rather than to
every conformance stub.
2026-08-20 14:46:37 +02:00
Levi Neuwirth b492426c69
test(sigint): assert the 45 inputs are DISTINCT, and stop claiming every stub carries the sentinel
Two closure gaps.

1. Both suites asserted only `cases.len() == 45`, so the exact
   45-entries-over-43-distinct-inputs regression could recur unnoticed
   --- the one where X3 collapsed into 1/E/empty and X4 into
   0/V/safe/bare, leaving two framing-specified cases silently
   unexercised. shared_cases() now asserts uniqueness over
   (status, stdout, stderr), inside the generator so no consumer can
   forget it. Verified by reverting both payloads to the sentinel: it
   fails naming X3.

2. Comments and ledger still said every stub emits the sentinel, which
   the explicit X3/X4 payloads had made false. They now say the
   BRANCH-DISCRIMINATING cases carry it while X3 and X4 deliberately
   carry their own --- X3 the canonical ignored wording with no token,
   X4 noise --- and that this is what makes them distinct inputs. The
   duplicated `self::`/`super::` explanation left over from the nesting
   fix is reduced to the correct one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 14:22:12 +02:00
Levi Neuwirth fb8a904923
test(sigint): distinct X3/X4 vectors, nesting-safe paths, and the gate I should have run
Five findings. The first was red CI that my local gate could not have
caught.

1. `crate::common` cannot resolve when gpu_invocation_acceptance.rs is
   compiled as a nested module of gpu_initial_target_acceptance.rs,
   where `crate::` is the outer test crate. Now `super::common`, which
   resolves in both modes --- verified by compiling each target
   explicitly. Clippy's `(Some(1 | 2), true)` folding applied too.

   The reason this shipped: plain `./scripts/gate` omits sweep-crdt,
   the only stage that compiles the nested target under crdt, while
   04-lib-crdt builds the lib alone. This lane gates with `--protocol`,
   and the ledger now says so.

2. X3 and X4 had stopped being the cases the framing specifies:
   stub_script() gave every case the same sentinel stderr, so X3 lacked
   the canonical ignored text and X4 was byte-identical to
   0/V/safe/bare --- 45 entries, 43 distinct inputs. Case now carries an
   explicit stderr payload; X3 emits the canonical wording with no
   token, and both consumers assert they never repeat it.

3. The capture-creation-failure row asserted exit, wording and stage
   output but not residue. It now inspects the temporary root before
   its RAII drop and requires it empty.

4. The exact-token test covered safe and error but not ignored, despite
   the ledger claiming all three. The ignored arm now asserts its exact
   stdout, driven through a SIGINT-ignoring shell.

5. The ledger's claim that the status-2 mutation is caught only by the
   dedicated row is superseded --- the sentinel matrix catches it --- and
   the self-referential "this commit" is replaced by bc7d776.

Also records two PRE-EXISTING crdt-only failures found while gating
properly (m4_24_bare_string_glob_stays_relative and
m4_24_d3_fallback_base_is_the_smallest_attachment_dir): they reproduce
in isolation and fail identically at 72da24a, so they are not this
lane's, and no cause is claimed for them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 11:50:34 +02:00
Levi Neuwirth 9321975197
docs(lane): head-exact gate evidence, and the run that was not
Full gate GREEN on the committed head 8802d6a, all 8 stages, log
20260820T072102Z-3009434.

Two provenance corrections recorded rather than smoothed over:

  - The first attempt on that same head failed 07-sweep on
    composition_overhead_under_ten_percent, a perf budget unrelated to
    this lane's surface, green in isolation and already recorded as a
    recurring signature on the panel-mapping-generation ledger. Both
    runs are kept. No cause is claimed for the first --- only that the
    second is the head-exact evidence.
  - The earlier 20260819T190930Z-2647615 run finished about thirty
    seconds BEFORE bc7d776 was committed, so it described the
    implementation tree, not a committed head. It is relabelled
    accordingly rather than left standing as gate evidence for a commit
    that did not yet exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 09:25:48 +02:00
Levi Neuwirth 8802d6a1a2
test(sigint): share the conformance vectors and assert the exact branch
Four acceptance gaps, all upheld.

1. Neither suite distinguished a validated refusal from a boundary
   error. Both exit 2 (and both produce Err in Rust), so comparing exit
   codes or is_ok() let a validator that accepts EVERY status-2 pair
   pass the whole matrix --- the precise defect A6c exists to catch.
   Every stub now emits a sentinel on stderr, and an Outcome enum
   (Safe / ValidatedIgnored / ValidatedError / Boundary) is asserted
   branch-exact: a validated verdict must surface the sentinel, a
   boundary failure must withhold it. Verified: mutating the gate to
   accept any status 2 now fails the MATRIX, where before it only
   failed a dedicated row. Each helper arm's exact stdout token is
   asserted as well.

2. The 45-case set was duplicated in both suites and could drift while
   both still reported length 45. It now lives in
   tests/common/sigint_conformance.rs and both validators consume the
   same vectors.

3. A8 was incomplete --- nothing forced capture-directory creation to
   fail. A bounded row points TMPDIR at a missing directory so
   `mktemp -d` fails, asserting boundary error 2, no stage execution and
   no residue; mutating the failure branch to fall through makes it
   fail. Temporary directories are RAII throughout, replacing the
   keep()-plus-manual-cleanup shape.

4. The R-d comment still claimed a shared helper means the consumers
   "can never disagree" and described status-only behaviour. Both were
   withdrawn by revision 13; the comment now points at the shared matrix
   as what actually keeps them in step.

36 gate rows, 16 GPU rows, clippy clean, full gate green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 09:15:47 +02:00
Levi Neuwirth bc7d776569
feat(gate,test): implement revision 13 --- the validated (status, token) pair
The helper now emits its verdict token on stdout with diagnostics on
stderr, and both consumers validate the PAIR rather than the status
alone. This closes the macOS defect CI found: a shell that cannot
execute the helper exits 1, which the status-only ABI read as
`ignored`, so a broken guard told the operator their environment
ignores SIGINT.

Gate (shell consumer):
  - guard-local capture directory, created before the gate's own
    temporary roots exist, with cleanup armed BEFORE the helper runs and
    disarmed on the safe path so the gate's later trap is undisturbed;
  - `|| sigint_status=$?` retained --- a bare invocation dies under
    `set -eu` before the status is read, which was the original bug;
  - `expected_token` selected by an explicit status case before any
    `set -u`-sensitive use, since an out-of-range status has none;
  - byte comparison via `cmp` against both permitted encodings, because
    a shell variable neither preserves NUL nor carries the child status;
  - the helper's stderr is surfaced ONLY for validated verdicts; a
    boundary failure prints the gate's own wording and withholds the
    untrusted child output;
  - every refusing branch prints status= and token=.

R-d (Rust consumer) validates the same pair from Command::output()
bytes. It needs no capture files, and its spawn-error path has no status
at all --- the boundary the shell cannot represent.

Conformance: 45 shared cases generated as a cross-product over token
class, encoding and status, run by BOTH validators so they cannot
diverge, plus Rust's X2 for 46 overall. 34 gate rows, 16 GPU rows, full
gate green.

Mutations, each biting its row: accepting any status 2 regardless of
token; surfacing child stderr on a boundary failure; emitting the token
to stderr. The first is caught by the dedicated error row rather than
the conformance set --- most of the set's boundary cases have empty
stderr, so they cannot tell which branch produced the exit 2 --- and
that limitation is recorded rather than left implicit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-19 21:14:30 +02:00
Levi Neuwirth 2a6625ddb9
docs(framing): record revision 13 approval
Revision 13 is approved at 5dece3e after closing the status-preserving
capture, guard-local cleanup, exact-byte grammar, stderr trust, complete
pair matrix and consumer-specific boundary blockers.

The replacement may now be implemented under the A1-A8 contract. PR
#241 remains unmergeable until that implementation is complete, gated,
and green on macOS.
2026-08-19 20:52:25 +02:00
Levi Neuwirth 5dece3e271
docs(framing): make revision 13's consumer algorithm total
Close the last three approval blockers in revision 13.

The shell algorithm now removes and disarms its guard-local capture on
the safe path, selects the expected token through an explicit status
case before any set-u-sensitive use, and sends every out-of-range status
straight to boundary error. The load-bearing `|| status=$?` remains in
place. A mechanical set-eu exercise covers safe, ignored, validated
error, status 126 and capture-creation failure; every path returns the
specified public status and leaves no capture residue.

The conformance accounting now distinguishes the 45 cases shared by the
shell and Rust validators from Rust's additional no-status spawn-error
case. A shell exec failure necessarily becomes a shell status, so it
cannot exercise that Rust-only input. The text also stops claiming that
Rust uses file-backed capture: only the shell needs files, while Rust
compares Command::output byte vectors directly.

No remedy implementation. PR #241 remains blocked until revision 13 is
recorded approved.
2026-08-19 20:51:07 +02:00
Levi Neuwirth a546a85476
docs(framing): revision 13 round 5 --- the spec reintroduced the shipped bug
Three blockers, all upheld.

1. The file-backed snippet dropped `|| status=$?` and invoked the helper
   bare. Under scripts/gate's `set -eu` that terminates the gate at the
   helper's non-zero exit, before the status is ever read --- which is
   the ORIGINAL shipped bug, reintroduced in the very section written to
   replace it. The load-bearing shape is restored and commented as such.

2. $tmp does not exist where the guard runs. The guard sits immediately
   after the worktree resolves and deliberately precedes the log
   directory, ambient root and GATE_TMPDIR, so it must create and own
   its capture directory --- with the cleanup trap armed BEFORE the
   helper is invoked, and a disarm on the safe path so the gate's own
   later trap setup is undisturbed. New A8 witnesses that no capture
   directory survives any path, including failure to create one:
   the guard was placed early to leave nothing behind, and a capture
   directory must not weaken that.

3. The case count was fiction. T0 + LF is a valid third encoding per
   status --- and is what the shipped helper actually emits, since it
   prints with echo --- and "a different valid token" has two
   possibilities per status, so sampling one left half the mismatches
   untested. Now enumerated: two valid encodings, six mismatched
   valid-token pairs each in both encodings, eight malformed classes,
   giving 14 per status x 3 = 42, plus four out-of-band cases = 46.
   Earlier drafts claimed twelve, then twenty-three, then thirty-four,
   each a count of a set that had not been enumerated; the document now
   says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-19 20:44:42 +02:00
Levi Neuwirth 41b8c4c517
docs(framing): revision 13 round 4 --- variable capture cannot carry this ABI
Three blockers, all upheld, and the first two say the same thing: the
capture mechanism I specified cannot implement the contract above it.

1. The sentinel idiom destroys the helper status. In
   out=$("$helper"; printf x) the last command is printf, so the
   assignment returns 0 whatever the helper did --- measured: a helper
   exiting 1 gives assignment status 0.

2. A shell variable cannot carry the byte grammar. Command substitution
   drops NUL in POSIX sh and bash --- and, measured here, zsh KEEPS it.
   So TOKEN NUL validates in one shell and not another, which is worse
   than lossy for a contract two consumers must implement identically.

   Both defects live in variable capture, so the spec now uses
   file-backed capture: redirect stdout and stderr to files, read the
   helper's own status directly, and compare bytes with `cmp` against
   generated want/want_lf files. Files preserve every byte including
   NUL; Rust compares out.stdout against TOKEN and TOKEN+LF. If a
   future consumer must use a variable, the status has to be carried
   out explicitly and the NUL divergence still bars a byte-equality
   claim --- both recorded.

3. The matrix was not the claimed cross-product: it omitted
   (1, unknown-version) and applied malformed and whitespace cases only
   at status 0, so a validator that checked tokens strictly for 0 and
   accepted arbitrary status-1 output passed all 23 rows. Replaced by a
   generated ten-token-class x three-status cross-product --- only the
   diagonal validates, the other 27 combinations are boundary errors ---
   plus four out-of-band cases: out-of-range status, spawn failure, the
   untrusted-stderr case, and stderr noise on an otherwise valid pair.
   34 cases. The stale "same twelve cases" sentence is gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-19 20:39:26 +02:00
Levi Neuwirth c3ad66f578
docs(framing): revision 13 round 3 --- untrusted stderr, exact pairs, one grammar
Four blocking issues, all upheld. The first defeats the whole design if
left standing.

1. Boundary errors trusted unvalidated stderr. A helper exiting 1 with
   NO token but the canonical "SIGINT is ignored" text would classify
   as boundary error --- correctly --- and then tell the operator their
   environment ignores SIGINT. A6 satisfied in the classification,
   violated in the message actually read. Now: a validated pair's
   stderr IS the diagnosis and is surfaced unchanged; a boundary
   failure's stderr is untrusted, and the consumer emits its own
   wording, omitting the child's or labelling it untrusted. New A6b
   witnesses exactly that case (conformance row 23), with a mutation
   for a consumer that surfaces it anyway.

2. The matrix did not prove exact-pair validation: no invalid status-2
   pair existed, and the expected column collapsed validated
   (2, :error) with boundary errors, so a validator accepting every
   status 2 passed all twelve rows. The matrix is now a 23-case
   cross-product distinguishing `error (validated)` from
   `error (boundary)`, with (2, missing), (2, :safe), (2, :ignored) and
   (2, unknown-version) all boundary. New A6c pins it.

3. Normalisation was internally inconsistent and not implementable
   identically. "Strip one newline then trim ASCII whitespace" removes
   further newlines, so TOKEN\n\n would have validated while the same
   clause demanded single-line output --- and POSIX $() strips ALL
   trailing newlines while Rust returns raw bytes, so the consumers
   could not have agreed even on a correct rule. Replaced by one byte
   grammar, stdout := TOKEN | TOKEN LF, with NO trimming, plus the
   shell sentinel idiom `out=$(helper; printf x); out=${out%x}` so the
   shell preserves what it must compare. Vectors added for extra
   newline, leading newline, surrounding spaces, CRLF and doubled
   token.

4. The ledger's old A7 assertion --- satisfied by disclosure, Linux-only,
   no non-Linux unix reachable --- contradicted its own macOS record
   twenty lines above. Marked explicitly as revision-12 history with
   the live record named.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-19 20:32:54 +02:00
Levi Neuwirth 8b8a692528
docs(framing): revision 13 round 2 --- the algorithm still implemented r12
Five blocking inconsistencies, all upheld. The first was the worst: the
document specified a validated pair and then printed an algorithm that
emits no tokens and a consumer flow that proceeds on exit 0 alone ---
accepting 0 with a missing token, the exact defect revision 13 forbids.

  1. The algorithm now emits exactly one token per arm on stdout with
     diagnostics on stderr; the consumer flow is pair-validation with
     explicit normalisation (strip one trailing newline, trim ASCII
     whitespace, require exactly one line); and the outcome table is
     keyed on pairs, with a fourth row for boundary error including
     macOS's status 1 with no token. `safe` is validated like the
     others --- a status arriving without its token did not come from
     this helper.

  2. A6a is SCOPED TO THE GATE. R-d never sees a shell status: the gate
     goes through /bin/sh, which turns an exec failure into an exit
     status, while Rust's Command returns a spawn error with no status
     at all --- conformance row 12, not row 5. And macOS CI does not
     compile R-d's test, which is crdt-gated while the macOS jobs build
     without crdt. R-d on macOS is unexercised, and the framing says so
     rather than implying coverage.

  3. A7 is restated against measurement. It cannot still say no
     non-Linux unix was tried when macOS ran and went red: five of six
     helper/gate rows pass there, one defect is named, R-d is recorded
     Linux-only, and the remaining portability claim is labelled a
     contract argument.

  4. "Both consumers use the same helper so they can never disagree" is
     withdrawn --- true when the status WAS the verdict, false once each
     consumer validates a pair independently in a different language.
     Replaced by a twelve-case conformance matrix both validators must
     agree on, including the macOS case and a normalisation case.

  5. The token-to-stderr mutation is remapped from A2 to A1/A3, with
     the reasoning recorded: with stdout empty every outcome becomes
     boundary error, which still satisfies A2 as written since A2 only
     requires "not the deadline message". A2 stays broad and A6 pins
     which diagnosis appears.

The ledger is aligned: the mechanism is established rather than
hypothesised, the "stderr prints the raw status" claim is corrected ---
the number appears only in the catch-all, and this failure took the
other branch --- and revision 12 is marked superseded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-19 20:27:43 +02:00
Levi Neuwirth 343eabd897
docs(framing): revision 13 --- a validated (status, token) pair
CI found revision 12's status-only ABI unsound on macOS. An unexecutable
helper makes macOS /bin/sh exit 1, which the ABI already reads as
`ignored`, so the gate told the operator their environment ignores
SIGINT when in fact the guard never ran. Linux returns 126 and mapped it
correctly, which is why local gating never saw it. Five of six SIGINT
rows pass on macOS; this is the sixth.

My proposed repair --- move `ignored` from 1 to 3 --- was rejected in
review, correctly: it relocates the collision rather than closing it,
since an execution failure can return any nonzero status. The
generalisation is what matters: NO EXIT STATUS CAN PROVE THE HELPER RAN.

Revision 13 therefore replaces the status-only ABI with a validated
(status, token) pair --- 0/1/2 paired with pmacs-sigint-v1:safe /
:ignored / :error, token on stdout, diagnostics on stderr. Any other
pair, including macOS's status 1 with no token, is a boundary error
mapped to 2. The public status meanings are preserved; what changes is
that a status must now be corroborated by something only the helper
could have printed.

Every refusing branch must also print the observed status and the token
state --- valid, missing or unexpected --- as diagnostic context, never
as the classifier. Revision 12 printed the number only in its catch-all,
so the macOS path had to be identified indirectly by which message text
appeared.

A4 gains four token mutations, each named against the row it must bite,
including accepting a missing token --- the shipped defect itself. A6 is
extended to cover missing, mismatched and unknown tokens in both
consumers, and a new A6a makes the macOS case a concrete obligation:
status 1 with no token must classify as boundary error, never ignored,
and the row is satisfied only when that platform is green.

Also records that A7 earned its keep: satisfied by disclosure because
the portability claim was argued rather than measured, and wrong the
first time it was measured.

No implementation. PR #241 stays blocked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-19 20:20:10 +02:00
Levi Neuwirth 70f0bc960e
test(gate): carry the gate's stderr into the boundary assertion
CI on 916007b: 12 green, 2 red, both macOS Test jobs, and exactly one
row --- gate_maps_an_unexecutable_helper_to_error_not_ignored, left
Some(1) right Some(2). The other five SIGINT rows pass on macOS.

This is the A7 portability finding the review pre-declared, and it is a
real one: the gate returned 1, meaning `ignored`, for a helper it could
not execute --- the exact conflation §7c forbids.

The cause is not established. The leading hypothesis is that the ABI's
1 is ambiguous by construction: 1 means "ignored", and 1 is also a
status shells hand back for assorted failures. On Linux an unexecutable
file yields 126 and the catch-all maps it to 2; if macOS /bin/sh
returns 1 instead, the two cases are the same number at the boundary
and no catch-all can separate them. That would call for verdicts
outside the range shells produce, which is a design change needing its
own revision --- not something to patch here.

This commit only makes the failure self-diagnosing: the assertion now
includes the gate's stderr, which prints the raw probe status it saw.
The first failure could not say which status produced it, because the
message discarded stderr.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-19 20:01:38 +02:00
Levi Neuwirth 916007b391
docs(lane): immutable checkpoint SHAs, and drop the stale "No PR"
Two ledger findings, both mine.

The lane block still said "No PR" while its own header and a new entry
recorded PR #241.

And the self-referential checkpoint wording had gone false, which is the
same trap as naming a branch's own tip: "this entry's own commit adds
the A6 rows" was true when written at 167d830 and false by d64d300, and
"the entry's own commit adds only the gate record" was 7cef9ca. Every
event now carries its IMMUTABLE sha --- implementation 3206433, A6 rows
and bounded negative path 167d830, factual corrections c9cc8dd, gate
record 7cef9ca, PR record d64d300 --- and only the branch tip stays
symbolic, which is the one pointer that has to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-19 19:46:32 +02:00
Levi Neuwirth d64d3009d8
docs(lane): record PR #241
Opened from gpu-probe-sigint-teardown into main after the quiet 8/8 gate
on c9cc8dd. Not merged; awaiting review rounds.

Docs-only, per the recording exemption that keeps gate evidence from
recursing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-19 19:42:09 +02:00
Levi Neuwirth 7cef9ca375
docs(lane): record both gate runs on c9cc8dd --- the red one included
Full gate GREEN on the committed head c9cc8dd, all 8 stages, log
20260819T160220Z-2339958, started at load 3.90.

The preceding attempt on the SAME head is kept rather than dropped. It
failed 04-lib-crdt and 07-sweep on four wall-clock rows --- the
composition budget, the summary-flatten scaling row, dired's 200ms
budget and a lean4 progress notification --- none of which touches this
lane's change. Load average was 49.6 and an unrelated
./verify_task_state.sh run was compiling under a separate toolchain at
/usr/local/rustup, having started about three minutes in and
overlapping precisely the two failing stages.

That overlap is recorded as evidence of WHEN, not proof of WHY. This
lane already retracted one confident environmental attribution, so the
red run was treated as "not valid evidence" rather than explained away,
and the green run on the same commit is what settles it. Had any of the
four failed again on a quiet machine it would have been a real finding
on this branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-19 18:07:28 +02:00
Levi Neuwirth c9cc8dd969
docs: two wrong facts --- 33 rows, and approval at 1fc0df6
Both mine, both checkable against evidence already in the repo.

The ledger said 35 gate-acceptance rows. The suite has 33. The 35 was
git_status_stage1_acceptance's result line, which sits immediately
below gate_script_acceptance's in the sweep log; I read the wrong one.
The correction names the misread so the next reader can see how a
transcription from a sweep log goes wrong.

The framing header newly attributed revision 12's approval to 7752bcb.
It was 1fc0df6 --- as the ledger says and as 7752bcb's own commit
message says in its first line. Restored.

The full gate is re-run on THIS commit rather than on the tree that
preceded it; the previous run finished twenty seconds before 167d830
was committed, so it described an uncommitted tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-19 17:22:01 +02:00
Levi Neuwirth 167d830932
test(gate): witness A6 in both consumers; bound the negative path
Three findings, all upheld.

1. A6 was witnessed only for the helper. Both consumers now have real
   -path rows.

   Gate side, driven through a stub worktree --- a temp git repo holding
   a copy of scripts/gate and a controlled helper --- so the gate's own
   code path runs against each verdict without touching the checked-in
   helper: a stub exiting 2 refuses with the ERROR wording and never
   "SIGINT is ignored"; a NON-EXECUTABLE stub maps 126 to boundary
   error 2 with its own wording. That second case is what the original
   guard got wrong twice.

   R-d side: the precondition is split into sigint_diagnosis() ->
   Result, so the message is testable rather than reachable only
   through a panic in a test that cannot run under the condition it
   describes. The new row asserts safe proceeds, ignored says so and
   says "NOT a teardown defect", error says "could not determine" and
   never "ignored", and an unrunnable helper is undecidable at the
   boundary.

2. The refusal row violated this suite's no-recursion constraint: it
   invoked the ordinary gate, so a regression of the exact `if !` bug
   would have launched eight real gate stages inside the gate suite.
   It now uses --self-test, which drives the same runner over a
   hardcoded synthetic plan, so the negative path stays bounded
   whatever the guard does. under_ignored_sigint() also takes the
   program and arguments POSITIONALLY --- `exec "$@"` --- instead of
   interpolating them into script text, which broke for any path
   containing a space or shell metacharacter, and every path here comes
   from a tempdir or CARGO_MANIFEST_DIR.

3. The portable checkpoint is recorded: implementation at 3206433,
   pushed, signed, clean, full default gate green 8/8 foreground. The
   framing header no longer says implementation "may proceed" --- it
   reports IMPLEMENTED. And docs/agent-handoff.md §3 gains the durable
   rule: never start the gate or cargo test from a shell that ignores
   SIGINT, `setsid nohup ... &` is forbidden, SIG_IGN is inherited
   across fork and survives exec, the gate refuses with no override,
   and scripts/check-sigint-deliverable answers the question directly.

35 gate-acceptance rows, 16 gpu_invocation_acceptance rows, full gate
green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-19 16:43:37 +02:00
Levi Neuwirth 32064336ee
fix(gate): the guard never fired --- two shell bugs, now covered by tests
Four findings, all upheld, and the first was a live bug I shipped.

1. R-b's non-zero handling was unreachable. scripts/gate runs under
   `set -eu`, so the bare helper invocation killed the shell at exit 1
   or 2 and neither `sigint_status=$?` nor the refusal messages ever
   ran; an unexecutable helper would have escaped as raw 126/127 rather
   than boundary error 2. Reproduced before fixing.

   The first repair was ALSO wrong, and worse: `if ! helper; then
   sigint_status=$?; fi` captures the status of the NEGATED condition,
   which is always 0, so the gate printed the ignored diagnosis and
   then ran the entire suite. The working shape is `helper ||
   sigint_status=$?` --- failure handled, so `set -e` does not fire and
   `$?` is the helper's own --- which is the idiom the helper already
   uses internally. Statuses 1 and 2 pass through unchanged; everything
   else, including 126/127, maps to 2 at the boundary and is never
   reported as "SIGINT is ignored".

   The guard also moved to immediately after the worktree resolves,
   before any log directory, ambient root or tmpdir exists, so a
   refused run leaves nothing behind.

2. The behaviour had no durable coverage, which is exactly why 27
   passing gate tests missed both bugs. Four rows added: helper safe,
   helper ignored, helper error (and never ignored), and gate refusal
   before stage 1. Ignored-SIGINT is simulated with `trap "" INT`,
   which is the real mechanism --- SIG_IGN inherited across fork and
   surviving exec --- not a stand-in. Verified to bite: mutating the
   gate back to either shipped bug fails
   gate_refuses_to_start_when_sigint_is_ignored and nothing else.

3. The ledger now records the implementation, both bugs, the four rows
   and their mutation check.

4. A7 is recorded SATISFIED BY DISCLOSURE, which is the fallback
   revision 12 allows when no non-Linux unix is reachable. The earlier
   "stays open" contradicted the approved contract and is withdrawn.
   Tried: Linux x86_64, all three outcomes, all consumers. Not tried:
   every non-Linux unix. Claimed: POSIX shell only, no /proc, no
   sigaction --- labelled a contract argument, not a measurement.

The full default gate passes all eight stages foreground; it caught a
rustfmt violation in the new test code on the first attempt, which is
the guard-and-gate arrangement working as intended.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-19 16:19:06 +02:00