Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,43 @@ fixes the only ones with no hit are the ones that are DATA (a hackathon name, a
persona, an `aria-label` template, organiser-authored form fields). Run that
before a journey rather than paying a full run per red.

## Ways a HANDOVER was confidently wrong

Distinct from the section below, and it cost more. Those are tests that agreed
with a broken product; these are briefs that told an agent something false with
enough confidence that it could have been taken on trust. Five in one week, all
mine, all caught only because the brief said "verify this before acting on it"
and the agent did:

- **A 44-second Nix floor that did not exist.** Handed over as fact and already
propagated into five files as the justification for design decisions.
Measured: 4.6-5.0 s steady state, and clean-vs-dirty is not the variable. The
44 s had been measured during a frontend crash loop that was fixed hours
earlier. A wrong number stated once became load-bearing in four other files
within a day.
- **git-lfs framed as the fix for that floor.** It makes `git status` truthful,
which is worth having; it does not change the timing at all. The agent A/B'd
it before touching the Dockerfile and said so.
- **A vacuity guard removed on a wrong theory.** "A no-op preview lists nothing"
— except the table renders every planned row including unchanged ones.
Deleting the wait made the two assertions after it pass instantly against a
page that never rendered. I introduced two silent-green assertions while
explaining why I was right.
- **"The devcontainer can drive k3d."** It mounts no Docker socket and has no
docker CLI. The agent found out in its first minute and rewrote that half of
the brief.
- **A stale consequence stated as current.** "A backend outage restarts every
frontend pod" was true before the landing page learned to catch its own gRPC
failures. The cost is real (the probe issues up to five calls per pod every 15
s) but the consequence had changed.

What made the difference every time was a brief that said **verify this rather
than transcribe it**, and named what would count as disproof. The failure mode
is not a lie — it is a true-once observation restated after its context moved,
which is exactly the shape nobody re-checks. Two habits follow: attribute a
number to the run that produced it, and when handing over a diagnosis, hand over
the measurement that would falsify it.

## Ways a test reported green while proving nothing

The most expensive category of bug here, because nothing turns red. All of these
Expand Down
8 changes: 5 additions & 3 deletions .claude/skills/hackathon-e2e/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ node_modules/
.artifacts/
test-results/
playwright-report/
# build-quality-report.mjs stages the report beside itself and renames it into
# place, so this only exists while a build is in flight — or after one was
# killed mid-write, which is exactly when it must not read as a tracked change.
# build-quality-report.mjs and embed-run-report.mjs stage their output beside
# itself and rename it into place, so these only exist while a build is in
# flight — or after one was killed mid-write, which is exactly when they must
# not read as a tracked change.
.quality-report.html.tmp-*
.recipe-player.html.tmp-*
43 changes: 42 additions & 1 deletion .claude/skills/hackathon-e2e/mutations/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,47 @@ function apply(mut) {
})
}

/**
* Put one backup back, retrying the 9p refusal — and never by COPYING.
*
* `fs.copyFileSync` on this repo's bind mount intermittently answers
* `EPERM: operation not permitted, copyfile` with nothing holding either file:
* the same refusal container trap 5 documents for a rename, and the same one
* `writeChecked` in build-quality-report.mjs already retries around. Here it was
* far worse than a failed restore, because **Node removes the destination when a
* copy fails after opening it** — so the file being restored was DELETED, the
* journal still claimed the mutation was applied, and every later entry aborted
* on `assertCleanTree`. Observed three times in one afternoon (2026-08-15), at
* entries 1, 7 and 9 of three separate runs; each run lost every entry after it.
*
* Two changes, for two different halves of that:
* - read-then-write instead of copyfile. A write can leave the destination
* wrong, but it cannot unlink it, so the worst case stops being "the source
* file is gone" — which is the only outcome nothing downstream can undo.
* - retry the EPERM, because it is transient (the very next restore of the
* same file succeeded, every time).
* The bytes are read back and compared, because a restore this tool did not
* verify is exactly what "restoration is verified, not assumed" is about.
*/
function restoreFile(backup, abs) {
const bytes = fs.readFileSync(backup)
const sleep = (ms) =>
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
for (let attempt = 1; ; attempt++) {
try {
fs.writeFileSync(abs, bytes)
break
} catch (e) {
if (attempt >= 5 || e.code !== "EPERM") throw e
sleep(250)
}
}
if (!fs.readFileSync(abs).equals(bytes))
throw new Error(
`restore: ${abs} does not match its backup after being written back`,
)
}

/**
* Undo everything the journal records, NEWEST FIRST.
*
Expand All @@ -225,7 +266,7 @@ function restore({ quiet = false } = {}) {
spawnSync("git", ["checkout", "--", e.file], { cwd: ROOT, env: ENV })
continue
}
fs.copyFileSync(e.backup, path.join(ROOT, e.file))
restoreFile(e.backup, path.join(ROOT, e.file))
if (!quiet) console.log(C.dim(` restored ${e.file} (${e.id})`))
}
writeJournal([])
Expand Down
55 changes: 14 additions & 41 deletions .claude/skills/hackathon-e2e/quality-report.html
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@

<header class="topbar">
<div class="brand">Hackagon <span>· e2e quality report</span></div>
<div class="topmeta">sketch/06-08-26 @ ea66ff97 · 2026-08-15</div>
<div class="topmeta">chore/loose-ends @ abb7f921 · 2026-08-15</div>
<nav>
<a class="btn" href="recipe-player.html">▶ Recipe player</a>
<button class="btn" id="btnTheme" type="button" aria-pressed="false">
Expand Down Expand Up @@ -425,68 +425,43 @@ <h2>Mutation results — can these tests go red?</h2>
</article>
<article class="tile"><h3>Verification run</h3>
<p class="val"><span class="num" data-claim="mut.verifyRows" data-value="38">38</span><a class="src" href="#src-D" title="Source D">D</a><span class="unit">entries judged</span></p>
<p class="chips"><span class="chip info"><span class="glyph" aria-hidden="true">•</span>recorded&nbsp;<span class="num" data-claim="mut.verifyDay" data-value="2026-08-13">2026-08-13</span></span></p>
<p class="chips"><span class="chip info"><span class="glyph" aria-hidden="true">•</span>recorded&nbsp;<span class="num" data-claim="mut.verifyDay" data-value="2026-08-15">2026-08-15</span></span></p>
<p class="note">Verdicts below. <code>NO REDS</code> is not a curiosity to note and move past — it
fails the run.</p>
</article>
</div>

<div class="wrap"><table>
<caption>Verdicts as recorded in <code>mutations/.state/verify.json</code><a class="src" href="#src-D" title="Source D">D</a> on
<span class="num" data-claim="mut.verifyDate" data-value="2026-08-13">2026-08-13</span><a class="src" href="#src-D" title="Source D">D</a>.</caption>
<span class="num" data-claim="mut.verifyDate" data-value="2026-08-15">2026-08-15</span><a class="src" href="#src-D" title="Source D">D</a>.</caption>
<thead><tr><th scope="col">Verdict</th><th scope="col" class="numcell">Entries</th><th scope="col">What it means</th></tr></thead>
<tbody>
<tr>
<td><span class="chip good"><span class="glyph" aria-hidden="true">✓</span>EXACT</span></td>
<td class="numcell"><span class="num" data-claim="mut.verdict.EXACT" data-value="23">23</span><a class="src" href="#src-D" title="Source D">D</a></td>
<td class="numcell"><span class="num" data-claim="mut.verdict.EXACT" data-value="37">37</span><a class="src" href="#src-D" title="Source D">D</a></td>
<td>exactly the declared tests went red — the property is provably catchable</td>
</tr>
<tr>
<td><span class="chip warn"><span class="glyph" aria-hidden="true">!</span>MISMATCH</span></td>
<td class="numcell"><span class="num" data-claim="mut.verdict.MISMATCH" data-value="3">3</span><a class="src" href="#src-D" title="Source D">D</a></td>
<td class="numcell"><span class="num" data-claim="mut.verdict.MISMATCH" data-value="0">0</span><a class="src" href="#src-D" title="Source D">D</a></td>
<td>the declared set failed plus something else; every extra here is the one declared-flaky spec</td>
</tr>
<tr>
<td><span class="chip crit"><span class="glyph" aria-hidden="true">✗</span>GAP</span></td>
<td class="numcell"><span class="num" data-claim="mut.verdict.GAP" data-value="11">11</span><a class="src" href="#src-D" title="Source D">D</a></td>
<td class="numcell"><span class="num" data-claim="mut.verdict.GAP" data-value="1">1</span><a class="src" href="#src-D" title="Source D">D</a></td>
<td>nothing went red — no test in the fast tier holds this property</td>
</tr>
<tr>
<td><span class="chip warn"><span class="glyph" aria-hidden="true">!</span>GAP CLOSED</span></td>
<td class="numcell"><span class="num" data-claim="mut.verdict.GAPCLOSED" data-value="1">1</span><a class="src" href="#src-D" title="Source D">D</a></td>
<td class="numcell"><span class="num" data-claim="mut.verdict.GAPCLOSED" data-value="0">0</span><a class="src" href="#src-D" title="Source D">D</a></td>
<td>declared a gap, but a red appeared — promote it to a real entry</td>
</tr>
</tbody>
</table></div>

<div class="callout">
<h3>The verdicts above judge an older manifest than the one below</h3>
<p>That run recorded <span class="num" data-claim="mut.verdict.GAP2" data-value="11">11</span><a class="src" href="#src-D" title="Source D">D</a>
<code>GAP</code> verdicts. The manifest declares <span class="num" data-claim="mut.gaps5" data-value="1">1</span><a class="src" href="#src-C" title="Source C">C</a> gap
today<a class="src" href="#src-C" title="Source C">C</a> — Go specs written after the run closed the rest, and <strong>nothing above has
been re-judged</strong>. Everything from here down is derived from the manifest and is current;
the table above is a snapshot of <span class="num" data-claim="mut.verifyDay2" data-value="2026-08-13">2026-08-13</span><a class="src" href="#src-D" title="Source D">D</a>.</p>
</div>


<div class="callout">
<h3>All <span class="num" data-claim="mut.mismatch.rows" data-value="3">3</span><a class="src" href="#src-D" title="Source D">D</a> MISMATCH verdicts have the same single
unexpected red</h3>
<p><code>service::Capacity &gt; never oversells the last place under simultaneous joins</code> — it fails roughly one run in five under in-memory
SQLite, has nothing to do with any mutation, and is declared in the runner's
<code>KNOWN_FLAKY</code> with that reason. Every ignored red is printed with its excuse, because a
list of failures that don't count is exactly the shape that could hide a real one.</p>
<p><span class="num" data-claim="mut.mismatch.superseded" data-value="1">1</span><a class="src" href="#src-D" title="Source D">D</a> of the
<span class="num" data-claim="mut.mismatch.rows2" data-value="3">3</span><a class="src" href="#src-D" title="Source D">D</a> would be judged differently today:
<code>capacity.oversell-by-one</code> now lists that spec in its
OWN <code>expectReds</code>, where the excuse cannot reach it — a flaky test can still be a genuine
witness, and this one hammers concurrent joins against a cap, which is precisely what the mutation
breaks. The recorded run counted it as an unexpected extra, so the file and the manifest disagree,
and the timestamps do not explain it: the manifest is newer
than the run (<span class="num" data-claim="mut.manifestTime" data-value="2026-08-14 16:34:51Z">2026-08-14 16:34:51Z</span><a class="src" href="#src-C" title="Source C">C</a> vs
<span class="num" data-claim="mut.verifyTime" data-value="2026-08-13 21:53:17Z">2026-08-13 21:53:17Z</span><a class="src" href="#src-D" title="Source D">D</a>).
<strong>The table above is the last recorded judgement, not a live one</strong> — re-run the
manifest to refresh it.</p>
</div>


<div class="wrap"><table>
<caption>Gaps cluster. Grouping is by the id prefix, so this is derived from the manifest rather
Expand Down Expand Up @@ -661,9 +636,7 @@ <h3><code>just check::test -c backend</code> is red on one flaky spec <span clas
<code>components/backend/internal/audit/audit_suite_test.go</code> (present) and <code>components/backend/internal/storage/storage_suite_test.go</code> (present) — both added in <code><span class="num" data-claim="open.bootstrapCommit" data-value="13331242e">13331242e</span><a class="src" href="#src-I" title="Source I">I</a></code>,
an ancestor of this commit.</p>
<p><code>.claude/CLAUDE.md</code><a class="src" href="#src-F" title="Source F">F</a> now records the command as red on a single
SPEC — <code>service::Capacity &gt; never oversells the last place under simultaneous joins</code>, the same one the mutation runner
declares flaky and excuses in <span class="num" data-claim="mut.mismatch.rows3" data-value="3">3</span><a class="src" href="#src-D" title="Source D">D</a>
of the verdicts above<a class="src" href="#src-D" title="Source D">D</a>. That is a test-side race, and CI runs this command, so it is a red CI run whenever it
SPEC. That is a test-side race, and CI runs this command, so it is a red CI run whenever it
lands — not a runner quirk to route around.</p>
</div>

Expand Down Expand Up @@ -793,8 +766,8 @@ <h2>Sources</h2>
<tr id="src-D">
<th scope="row" class="srckey">D</th>
<td class="mono">.claude/skills/hackathon-e2e/mutations/.state/verify.json</td>
<td class="numcell small mono">23.4 KiB</td>
<td class="small mono">2026-08-13</td>
<td class="numcell small mono">21.5 KiB</td>
<td class="small mono">2026-08-15</td>
<td class="small">The verdict the mutation runner recorded for all <span class="num" data-claim="src.verifyRows" data-value="38">38</span> entries, with the reds each one actually produced.</td>
</tr>
<tr id="src-E">
Expand All @@ -807,7 +780,7 @@ <h2>Sources</h2>
<tr id="src-F">
<th scope="row" class="srckey">F</th>
<td class="mono">.claude/CLAUDE.md</td>
<td class="numcell small mono">91.7 KiB</td>
<td class="numcell small mono">93.9 KiB</td>
<td class="small mono">2026-08-15</td>
<td class="small">The suite-results table (the only source for smoke, mobile, openreplay, backend and frontend-unit numbers) and the known-broken prose.</td>
</tr>
Expand Down Expand Up @@ -838,7 +811,7 @@ <h2>Sources</h2>

<footer>
<p>Generated by <code>.claude/skills/hackathon-e2e/scripts/build-quality-report.mjs</code> from
sketch/06-08-26 @ ea66ff97 (2026-08-15). No wall-clock is embedded, so two builds over one
chore/loose-ends @ abb7f921 (2026-08-15). No wall-clock is embedded, so two builds over one
tree are byte-identical. This is the status report; the animated replay of the recipe is
<a href="recipe-player.html">recipe-player.html</a>.</p>
</footer>
Expand Down
Loading
Loading