Skip to content

Harness honesty: an empty recv after RST, not a quiet-window expiry - #16

Merged
HackTuah merged 19 commits into
mainfrom
slice/006-harness-honesty
Sep 8, 2026
Merged

HackTuah merged 19 commits into
mainfrom
slice/006-harness-honesty

Conversation

@HackTuah

@HackTuah HackTuah commented Sep 8, 2026

Copy link
Copy Markdown
Member

Slice 006. Fixes the CI flake that made every "gate green" in this repository a sample rather than a measurement. lib/ is untouchedgit diff origin/main..HEAD -- lib/ is empty.

The mechanism was not what the PLAN hypothesised

The PLAN guessed a partial buffer from the 700 ms quiet window. Measured, that is refuted: the buffer comes back empty, and drain/2 never entered a timeout branch at all — the suite finished in 3.5 s, well inside the 10 s first-byte window.

The real cause is an empty recv after an RST: a response segment already written by the server, destroyed by the reset the server's own close emits. No read logic can recover it. That is why slice 003's 700 → 10_000 + 700 split lowered the rate and left the mechanism — it tuned a clock that was never involved.

The fix is a distinction, not a threshold

  • Protocol-complete read. The exchange reads until expect complete responses are parsed from content-length, or the connection ends — never on elapsed silence.
  • Repeat only :econnreset. A FIN and an RST are different statements. Only an abort can destroy a response already written, so only an abort is repeated; a clean close carrying no answer raises immediately. Measured, not argued: 250 exchanges under 32-way load were every one :econnreset, zero clean closes, 19 losing the response.
  • @attempts stays at 5. Raising it to 7 removes the loaded residual and is the threshold class this PLAN rejects. This repository already bought one threshold fix for this test and it came back.

Results

== green-rate-mc8-idle-round2:   0 run(s) of 100 exited non-zero ==
== green-rate-mc64-idle-round2:  0 run(s) of 100 exited non-zero ==
== green-rate-mc8-load32-round2: 1 run(s) of  40 exited non-zero ==

Idle: 0 of 200 across both CI profiles. Loaded: 1 of 40 under 32 busy loops on 32 cores — and that one is the bound working, raising as a harness fault that names itself rather than asserting something false about the server:

** (RuntimeError) 5 exchange(s) in a row were aborted before a measurement existed. The last
   ended (:econnreset) after 0 complete response(s) out of 1, having read 0 byte(s): ""

The old code turned that identical condition into assert bytes =~ "HTTP/1.1 403" — a sentence about the server, for bytes the server did send.

Criterion 3, discharged on the clean loaded table

Forty scored suites: every row equal to slice 003's record, zero variance, and both recorded survivors still survivingM2never 0|0|0|0, Mc2 0|0|0|0. A known survivor still reporting as a survivor under the conditions that produced the false kill is the only check that tests the instrument against a value known by other means rather than against itself.

Me1 | 164 tests, 1 failure on all four passes — the header_values/2 UTF-8 guard is pinned by a mutant that moves.

Also in this slice

tools/mutate.sh and tools/mutants/ are now in the repository — slice 003's record cited $S/mut.sh, a scratchpad path no reader could resolve. It refuses a dirty target (proven red-then-green), holds a lock, and prints exchanges repeated: N per row so a defect the harness absorbs can no longer be invisible.

Two things the record says it cannot show

  • Round 2's two lanes are two written passes by one reader, not two readers. Each verdict says so at its head.
  • The provenance notes on the four *-round2 archives destroyed the evidence they cite. All four now share one birth equal to their mtime — prepending each note rewrote its file and reset the timestamp offered as proof. Corrected by appending; the uncertainty is recorded rather than argued away.

Verification

tools/signoff.sh verify slices/006-harness-honestyVERIFY_EXIT=0, round 2 both approve on the current review tree.

== beam_mcp gate ==
  format                     pass
  compile                    pass
  test                       pass
  credo                      pass
  optional deps              pass
  docs                       pass
  reuse                      pass (286 tracked; 80 in scope, 78 headered + 2 sidecar; excluded 204 archive + 2 licence text)
  licence files              pass
Gate OK.  GATE_EXIT=0

164 tests, 0 failures. Push-event CI run 34234715748 green on f0350d3 — criterion 4, demonstrated in CI at the profile where it was caught.

@HackTuah
HackTuah force-pushed the slice/006-harness-honesty branch from 9a4a756 to 24370ce Compare September 8, 2026 02:40
@HackTuah HackTuah changed the title Plan slice 006 — harness honesty: a gate run must be a measurement, not a sample Harness honesty: an empty recv after RST, not a quiet-window expiry Sep 8, 2026
SCR-289. The number is 006 and it runs FIRST: slice 005 already started, so
005 is taken, and under the numbering rule a number is identity assigned when a
slice starts while order is separate. Number is not rank; that separation is
the rule working rather than an exception to it.

Why it goes ahead of 005: a test defect reaching CI means every "gate green,
every step line reading pass" in this repository is currently a SAMPLE rather
than a measurement, and that sentence is the evidence behind every slice record
here. 005's acceptance criterion 2 is mutation-scored, so it cannot be recorded
honestly until this is true again.

READ FROM THE CODE RATHER THAN FROM THE ISSUE. drain/2 terminates on a
wall-clock silence, not on a protocol event: once any byte has arrived, a gap
over @quiet_ms 700 is read as "the server is done", so under load the function
returns a PARTIAL buffer and reports :open, and every assertion downstream is
made against bytes that had not finished arriving.

This is already a repair of a previous instance of itself. The comment above
those attributes records that a single 700 ms window made the 9 MB case flaky
and presented as a SURVIVING mutant scoring as KILLED. That repair split one
window into two and made the first generous -- it lowered the rate and left the
mechanism, which is why the same test failed again in CI at max_cases: 8. A
threshold change cannot fix a race, and the repository has already bought the
threshold fix once.

Three observations, one mechanism, and the error always has the same direction
-- it ADDS a failure, which is the direction that makes a table read all-killed
and nobody investigate:

    003 round 3     dev, max_cases 64     Mc2, a survivor, scored KILLED
    003 re-score    40 back-to-back       M13rev read 2 (rec 1), Mr2 4 (rec 3)
    PR #15 push     CI, max_cases 8       test FAIL on a commit with NO code

The red is a RATE, not a transcript, measured at both max_cases profiles and
with N derived from the observed rate rather than chosen. The fix removes the
wall-clock window rather than retuning it, and the rejected alternatives are
named so they are not revisited: raising @quiet_ms, --max-cases 1, and
isolation with a settle interval each lower the rate on one profile and leave
the mechanism.

The load-bearing acceptance criterion is that a KNOWN SURVIVOR still reports as
a survivor under the conditions that produced the false kill, because this
slice measures its own success with the instrument it is repairing, and that
criterion checks the instrument against a value known by other means rather
than against itself.

Also in scope: commit the mutation harness under tools/. Slice 003's record
cites $S/mut.sh <name>, a scratchpad path no future reader can resolve, and a
command that cannot be re-run is not a record.

Not in scope: re-scoring slice 003, re-running PR #15's red check, and lib/.

No shipping code, no behaviour change.

    reuse pass (234 tracked; 59 in scope, 57 headered + 2 sidecar)
    Gate OK.  GATE_EXIT=0

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…rdict an empty buffer

SCR-289. The PLAN's diagnosis was a hypothesis and it is REFUTED. It said
drain/2 terminates on wall-clock silence and so returns a PARTIAL buffer under
load. Measured, the failing read returns an EMPTY buffer and never takes a
timeout branch at all -- which is also why slice 003's repair, splitting one
700 ms window into 10_000 + 700, lowered the rate and left the mechanism. You
cannot retune your way out of a branch you are not in.

RED FIRST, AND IT IS A RATE. mix test on the unmodified tree, 100 consecutive
runs at each of CI's two profiles on an idle 32-core machine:

    --max-cases  8    2 of 100 exited non-zero
    --max-cases 64    1 of 100 exited non-zero

All three are the same test, and the local run prints what the CI transcript
did not -- the argument:

    code: assert String.contains?(bytes, "HTTP/1.1 403")
    arguments:
        # 1
        ""

WHERE THE BYTES GO. The 9 MB refusal, 40 runs per variant, reporting
{bytes received, recv reason, send result, saw the 403?}:

    A  send everything, then read      29x {297, :closed, :ok, true}
                                       11x {0, :closed, :ok, false}
    B  A, plus show_econnreset: true    8x {0, :econnreset, :ok, false}
    C  passive recv while writing      40x {297, :closed, :ok, true}
    D  C, plus show_econnreset: true    0 losses in 40

The server refuses on the headers, writes a 297-byte 403 and closes with ~9 MB
unread, so Linux aborts the connection with an RST rather than a FIN. The
response is LOST, not late, and show_econnreset only renames the error.

WHICH SIDE LOSES IT. A probe whose authorize callback messages the test process
separates "the server never answered" from "the answer never arrived". 60 runs
under 32 busy loops: authorize ran 60 of 60, and 3 of those refusals never
reached the client. The write shape does not govern it either -- 120 runs in
one send lost 0, 120 in 64 KB chunks lost 5, same machine.

So there are two faults wearing one symptom.

1. A VERDICT REPORTED OVER A BUFFER NOBODY READ. drain/2 answered
   {acc, :closed} and {acc, :open} over whatever acc held, including nothing.
   collect/3 now reads until `expect` COMPLETE responses have been parsed --
   status line, headers, content-length body -- because every response this
   adapter sends declares a length. A timeout is no longer a verdict; it
   raises. :open versus :closed survives and is asked only after the responses
   are complete: TCP delivers a FIN in order behind the bytes before it, so
   that window races the server's close syscall and not the response.

2. A RESPONSE SEGMENT DESTROYED IN TRANSIT. active: true moves received bytes
   into this process's MAILBOX before a failing gen_tcp:send can destroy the
   port, which the passive draft could not do under CPU starvation (6 of 40
   loaded suites still lost them). The residue is a lost segment and no read
   logic can recover it, so an exchange that produced no measurement is
   REPEATED on a fresh connection, bounded at 5, announced on stderr. The
   predicate is protocol-determined -- the connection ended with fewer than
   `expect` complete responses -- and decided before any assertion runs.
   Reporting a destroyed exchange as {"", :closed} was the old defect;
   reporting it as a failed assert on the 403 is the same lie with the sign
   flipped, a sentence about the server for bytes the server did send.

NO TIMING CONSTANT WAS RAISED. @hold_ms is still 700; @read_ms 10_000 is a
backstop that raises rather than a window that answers.

drain/2 was pinned by nothing, because it was the instrument. Two tests now
drive the read from a raw scripted listener, and both were mutated to prove
they carry information: restoring the silence-terminated read kills 2, and
restoring the empty-buffer verdict kills 1. That is why the suite is 162 tests
where the records say 160.

GREEN: 0 of 100 at --max-cases 8, 0 of 100 at --max-cases 64, and 0 of 40 at
--max-cases 8 under 32 busy loops, where six exchanges were destroyed, repeated
and none became a failure.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
… paths

SCR-289. Slice 003's record cites its scoring instrument as `$S/mut.sh <name>`,
where $S is a scratchpad directory on one developer's machine. Nobody reading
that record can re-run it, which makes a nine-row table a claim rather than a
measurement. CONVENTIONS.md states the rule for archives -- written by a
command that fetches it, or it does not exist -- and it is the same rule one
step earlier: an instrument that cannot be re-run is not evidence either.

tools/mutate.sh and tools/mutants/*.py are that harness, with three changes.

THE PATHS COME FROM GIT, NOT FROM A WORKTREE NAME. The script cds to
`git rev-parse --show-toplevel`, and each mutant is handed the file to mutate
as argv[1] instead of hard-coding /home/aylac/.../003-release-0-3-1/lib/...
in ten separate scripts.

THE PRISTINE COPY IS TAKEN FROM THE TREE IN FRONT OF YOU. The scratchpad
version diffed against an http.ex.pristine copied by hand some time earlier,
and carried a second copy named .prerebase because the first had gone stale.
A stale pristine makes the printed diff describe a tree nobody is running.
Here it is made at the start of every invocation and restored by an EXIT trap,
so an interrupted run cannot leave a mutated file behind. `mutate.sh check`
applies all ten and restores, proving both halves without running a suite.

IT KEEPS THE FAILING TEST NAMES, NOT ONLY THE COUNT. "160 tests, 1 failure"
reads identically whether the failure is the mutant's or the harness's, and
that ambiguity is exactly what let slice 003 round 3 write down a false
KILLED. `score` now prints the name of every test that failed beneath each
row, so a corrupted score is legible instead of plausible.

What it refuses is unchanged and is the point: a mutant whose anchor no longer
matches, or that leaves the file byte-identical, is reported NOT-APPLIED and
scored as nothing. The suite is never run over an unmutated file and called a
score.

Ten mutants move across, including Me1, which slice 003 added after its
round-3 table and which the rebased run scored at 160 tests, 1 failure.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…ced on demand

SCR-289. The record for the two commits before it, written at the round
boundary rather than at the end.

Every count is quoted from a command's output and every archive is written by
the command that produced it. slices/006-harness-honesty/logs holds the two
red rates at both of CI's max_cases profiles, the three mechanism probes, the
two intermediate drafts that refused the fix before it, the harness-anchor
mutations, the mutation set scored under load before and after, and five
consecutive gate runs.

THE FINDING THAT MATTERS IS NOT THE FIX. Slice 003 round 3 recorded a
SURVIVING mutant scoring as KILLED, and recorded that it could not be
reproduced on demand. It can:

    tools/mutate.sh score, 4 passes per mutant, 32 busy loops on 32 cores,
    with the harness as it was --

    M2never  recorded 0 failures, a SURVIVOR   read  1 | 1 | 1 | 0
    Mc2      recorded 0 failures, a SURVIVOR   read  0 | 0 | 0 | 0

Sixteen of forty scored suites read one failure more than the mutant deserves,
and the row corrupted is a recorded survivor. With the harness as it ships,
the same command over the same forty suites returns every row equal to slice
003's table, with zero variance, and both survivors survive.

Slice 003's table is NOT re-scored and its records are NOT rewritten. That
round found a false KILLED with the tools it had and said so; this makes the
finding reproducible, which strengthens the record rather than replacing it.

Also recorded, because it is the kind of thing that quietly becomes a claim:
the local rate is 2 in 100 and CI's is 2 in 15. The disagreement is reported
rather than smoothed. Everything measured here says the defect is governed by
whether the client process is scheduled in time, and a rate that rises when
CPU is scarce is consistent with that -- but consistent is not demonstrated,
and what closes it is the CI run at the end of this slice, not the paragraph.

PR #15's and PR #17's red push runs were not re-run. lib/ is untouched.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…lve nowhere

Two small record defects, both of the class this slice exists to fix: a
statement that was true when written and is false now, and a citation a reader
cannot follow.

HANDOFF.md said "160 tests, 0 failures". This slice's two anchors took it to
162, measured:

    162 tests, 0 failures

The README rule says the document moves in the slice that changes the thing,
so leaving a false count that this branch created was not an option.

And two bare `logs/probe-d-bandit-drain*.txt` references in the test file's
moduledoc resolved against no directory -- they are slice 003's logs and read
correctly only from inside that slice. Same unresolvable-path class as the
`$S/mut.sh` in slice 003's record that this slice moved into tools/. A citation
that cannot be followed is not a citation.

    Gate OK.  GATE_EXIT=0

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…lying about its own proof

Round 1 read tree e2f8a08 at commit 90e4e99. Both
lanes pinned the same tree. Verdicts are recorded before the fixes, so the round
is a record of what was found rather than of what survived.

Lane m (mechanism) and lane h (record and instrument): CHANGES REQUIRED.

Five blocking findings, and NONE of them is in what the read logic does.

B1, found INDEPENDENTLY BY BOTH LANES, and it is the sharpest. FINDINGS' anchor
table prints "162 tests, 2 failures / 1 failure / 0 failures". Its cited
archive, logs/mutation-harness-anchors.txt, says "11 tests" on all three lines:
that run was `mix test <one file>`. The failure counts and the conclusion are
right and the population was typed up to the whole-suite number -- in the table
whose job is to show the new anchors carry information, inside the slice whose
thesis is that an ambiguous count is how a false record gets written. This is
the rule this repository exists to defend, broken in the proof of the fix for
breaking it.

B2, lane m. The repeat is a CONTAINED ANCHOR and can launder a real defect. The
"never answers" test interpolates #{@attempts} into its own expected regex, so
the constant sits on both sides and cannot disagree with itself: @attempts 5->1,
which deletes the repeat outright, still gives 162 tests, 0 failures. Lane m
then built a listener that stays silent on four connections and answers on the
fifth, and the suite PASSED with four "produced no measurement" lines on stderr.
The predicate cannot separate a lost segment from an intermittently silent
server, and FINDINGS' "nothing is masked" is true only of a deterministic
silence. Worse, an absorbed defect leaves no trace where it would matter:
`grep -c 'produced no measurement'` is 0 in the scored mutation run and 166 in
the rate log, because mutate.sh's suite() keeps only the count line.

B3, lane h. Commit 90e4e99 swept unresolvable log citations out of
http_bandit_test.exs and left five behind -- lines 118, 136, 156, 166 and 179,
every one of them added by THIS slice. The two it fixed were slice 003's. That
is CONVENTIONS.md's "a grep finds what you already thought of", inside the
commit that names the class.

B4, lane h. The CI table is the premise of the whole slice -- it is why 006 runs
ahead of 005 -- and it has no archive. The run ids, the seeds, "two failures in
the fifteen most recent push runs" and "main has three green runs in that
window" appear nowhere but in FINDINGS itself. The local rates beside them are
archived properly and reconcile exactly, which makes the gap sharper rather than
smaller.

B5, lane h. PLAN section 4 criterion 4 (demonstrated in CI) is undischarged in
the record and absent from "What is NOT done here", and criterion 6 (gate green
repeatedly) is discharged in the tree but has no section: gate-round1.txt and
red-gate-credo-nesting.txt are the only two logs the record never cites.

Non-blocking and carried into round 2: settle/2 still answers :open on a 700 ms
timeout, so "no timeout decides a verdict" is true of @read_ms only -- narrowed,
not eliminated, and the file says so; two archives whose internal banner names a
different run because the FILE was renamed after the fact; three archives citing
scratchpad scripts nobody can re-run; "probe-write-shape rules the write shape
out" overstating its own 0/120 against 5/120; and mutate.sh holding no lock, so
two runs in one worktree collide, which lane h saw bite live.

Both lanes verified independently and by command that lib/ is untouched, that
slice 003 is not rewritten, that the seven rate archives and both mutation
tables reconcile row by row, and that the REUSE step really can see the new
tools/ files -- lane h removed a header and got `reuse FAIL` naming
tools/mutants/Mr2.py, 69 headered down to 68.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…count the repeats

Three changes to tools/mutate.sh, and the probes it and the rate archives were
produced by, which until now lived in a scratchpad nobody else could reach.

THE DIRTY-TARGET GUARD. The header used to claim "an interrupted run cannot
leave a mutated file behind". No trap catches SIGKILL, which is what a task stop
sends, so the claim was false for the one interruption a runaway scoring loop is
most likely to receive. The consequence is worse than a leftover file: pristine
is copied from the WORKING file, so the next invocation would adopt that
leftover mutant as its baseline, every restore afterwards would restore the
defect, and every diff would print clean because the mutant is now the
reference. start() therefore refuses when TARGET has uncommitted changes.
It is scoped to TARGET rather than to the tree -- a slice scoring mutants while
its own test file and records are uncommitted stays allowed -- and
MUTATE_ALLOW_DIRTY_TARGET=1 is the documented override.

The guard is demonstrated red then green in
slices/006-harness-honesty/logs/probe-mutate-dirty-target.txt: a leftover Mr2 is
planted, the next run prints REFUSING and SCORE_EXIT=1, and after
git checkout -- lib/beam_mcp/transport/http.ex the same command gives
"Mc2 | 164 tests, 0 failures TEST_EXIT=0" and SCORE_EXIT=0.

THE LOCK. The script rewrites TARGET in place, so two runs in one worktree score
each other's mutations and neither result means anything. Round 1 saw exactly
that happen live. The lock is a DIRECTORY because mkdir is atomic everywhere
this runs and needs no flock, which bash 3.2 on stock macOS does not ship; it
lives under the git directory so it is never tracked and can never enter the
REUSE step's population. restore() rmdirs it alongside the pristine restore.

THE REPEAT-COUNT LINE. suite() kept only the count line and the failing test
names, so a scored archive held no trace of an exchange the Bandit harness had
repeated -- grep -c over the scored archive returned 0 while the rate archive
held 166. A defect the repeat absorbed was invisible in exactly the artefact a
verdict is read from. A row that needed repeats now says "exchanges repeated: N".

The probes move into tools/probes/ for the reason round 1 gave: three archives
cited scripts that existed only in a scratchpad, which is a citation nobody
holding this repository can follow. Each carries its SPDX header, so the gate's
REUSE step, whose population is git ls-files, sees them.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
Round 1's B2. The repeat could launder a real defect: lane m built a server
silent on four connections and answering on the fifth, and the suite passed. Its
anchor was contained -- @attempts 5 -> 1, deleting the repeat entirely, still
gave a green suite, because the expected text interpolated #{@attempts} and so
the mutation moved both sides of the comparison together.

Closed by a distinction rather than a threshold. A FIN and an RST are different
statements about what happened to a response already written: only an abort can
destroy one in flight, so only an abort is repeated. A clean close carrying no
answer is a fact about the server and raises immediately.

That rests on measurement, not on argument: 250 exchanges against the real
listener under 32-way load were every one :econnreset, zero clean closes, 19 of
them losing the response.

The bound is a literal now, so @attempts sits on one side of the comparison
only. Both previously invisible anchors move at full-suite scale:

    H4  @attempts 5 -> 1, the repeat DELETED     164 tests, 2 failures
    H5  clean close repeatable again             164 tests, 1 failure

@attempts stays at 5. Raising it to 7 removes the loaded residual and is the
threshold class this slice rejects; the repository already bought one threshold
fix for this test and it came back.

    164 tests, 0 failures

Signed-off-by: Ayla Croft <aylacroft@proton.me>
… ate its own evidence

FINDINGS gains a Round 2 section. Round 1 is left exactly as written; every
correction here is appended.

    164 tests, 0 failures
    == green-rate-mc8-idle-round2:   0 run(s) of 100 exited non-zero ==
    == green-rate-mc64-idle-round2:  0 run(s) of 100 exited non-zero ==
    == green-rate-mc8-load32-round2: 1 run(s) of  40 exited non-zero ==

0 of 200 at both CI profiles. The loaded residual is the bound working rather
than a regression -- it raises as a harness fault naming itself, where the old
code turned the identical condition into an assertion about bytes nobody
received.

Criterion 3 discharged on the clean loaded table: forty scored suites, every row
equal to slice 003's record, zero variance, and both recorded survivors still
surviving -- M2never 0|0|0|0 and Mc2 0|0|0|0. That is the only check that tests
the instrument against a value known by other means rather than against itself.

TWO CORRECTIONS, BOTH APPENDED.

Round 1 said the local rate shows "what the CI transcript did not -- the
argument". False. logs/ci-evidence.txt carries it in both red runs:

    arguments:

             # 1
             ""

The abridgement was in the summary this slice was handed. A claim about what an
upstream transcript did not contain, made without fetching it, is a typed count
wearing different clothes.

And the four *-round2 archives carry provenance notes arguing they are the clean
re-run because their filesystem births form a serial chain. THAT EVIDENCE IS
GONE, AND ADDING THE NOTES IS WHAT DESTROYED IT:

    birth 13:36:56  mtime 13:36:56    x4, identical to the millisecond

Prepending each note rewrote its file and reset the timestamp the note offers as
proof. The archive cited as evidence was consumed by the act of citing it --
this slice's own subject, one level up. What survives is the driver's own date:
lines, which establish one serial run and cannot rule out interference in its
opening minutes; there is no later quiet-machine table to cite instead, since
mutation-harness-anchors-round2.txt is EARLIER at 12:06:42Z. Recorded as
unresolved rather than argued away.

B1 stands as found: Round 1's table quotes 162 against an archive reading 11.
Left there as the finding. Every Round 2 table reads 164, quoted from its run.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
Two written passes by one reader, not two readers, and each verdict says so at
its head rather than in a footnote -- independence is the entire content of a
second lane, and this round does not have it.

Lane m read the mechanism: the FIN/RST distinction is implemented as a
distinction rather than a threshold, a clean close with no answer raises without
repeating, and both round-1 blind spots move at full-suite scale. lib/ untouched.

Lane h read the record and found this round's sharpest thing, which is not in
the code: the provenance notes destroyed the evidence they cite. All four
*-round2 archives now share one birth equal to their mtime -- prepending each
note rewrote its file and reset the timestamp offered as proof. Corrected by
appending, and recorded as unresolved rather than argued away.

Both approve.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
tools/signoff.sh record for lanes m and h, recorded before any later edit so
each names the tree its verdict is about rather than a tree that moved after.

This is the mechanism slice 004 built and the reason it exists: this repository
has shipped 16 verdicts bound to no tree and 2 trees carrying no verdict.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
@HackTuah
HackTuah force-pushed the slice/006-harness-honesty branch from f0350d3 to ad72c1d Compare September 8, 2026 14:25
signoff.sh refused after the rebase onto PR #18: the tree the lanes approved is
not the tree that ships. The refusal is the mechanism working -- slice 001b's
failure was sixteen verdicts bound to no tree, and this is the guard against
approving a tree that moved afterwards.

Re-recording without looking would be the dishonesty the guard exists to stop,
so the delta was read in full first:

    CHANGELOG.md | 4 ----
    HANDOFF.md   | 7 -------
    2 files changed, 11 deletions(-)

Eleven deleted lines in two documentation files -- the stale Known-gaps bullets
that said the invalid-UTF-8 defect was unfixed while the same section listed it
fixed. No code, no test, no archive, and nothing the suite or the harness reads:
CHANGELOG.md is referenced by tests only as a filename in mix.exs files:, never
by content.

Both verdicts carry the delta and say they are unchanged by it.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…a stale tree

Signed-off-by: Ayla Croft <aylacroft@proton.me>
Signed-off-by: Ayla Croft <aylacroft@proton.me>
Signed-off-by: Ayla Croft <aylacroft@proton.me>
…the gate

I committed it with a blanket `git add -A`. It carries no SPDX header, so the
widened REUSE step correctly failed:

    reuse FAIL (293 tracked; 83 in scope, 80 headered + 2 sidecar; ...)
        no SPDX-License-Identifier in the file or in a <path>.license sidecar:
          slices/006-harness-honesty/signoff/verify.txt

A header is the wrong fix: signoff.sh rewrites this file on every `verify`, so
a header would be dropped on the next run and tracking it would make that run
refuse on its own output. Its bytes restate the .signoff records committed
beside it, which are the record.

Ignored rather than headered, with the reason in .gitignore.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
Signed-off-by: Ayla Croft <aylacroft@proton.me>
Signed-off-by: Ayla Croft <aylacroft@proton.me>
Signed-off-by: Ayla Croft <aylacroft@proton.me>
@HackTuah
HackTuah merged commit 8410065 into main Sep 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant