FABRIC-2.md: ACL-RWT-style 3x3 Latin square re-run on Stadium substrate (item 5.1/F.3)

Found and fixed a real bug before any campaign work could start: EXEC-DOE's
own CSV output was almost entirely lost to console interleaving with the
routine per-tick heartbeat export -- same bug class as Section L's PLOT
case. Fix: HB-OFF immediately before EXEC-DOE, HB-ON after DOE: complete.
Confirmed HB-ON-first (the reverse order) does NOT fix it -- tested
directly, row loss recurred identically.

Also found: L8-DOE/WL-HI/WL-LO (the mechanism bare_metal/README.md
describes as auto-run) don't exist anywhere in capsules/, and Makefile.
starkernel's DOE_SEED variable is declared but never referenced -- both
vestigial, matching Section K's earlier staleness finding.

Built QEMU-serial-socket injection tooling (socat) to drive EXEC-DOE
interactively after boot, since it requires live REPL input, not just
observation. Two real defects found and fixed in that tooling itself: a
log-discovery race (self-excluding the very log it needed to find,
causing two separate stuck-injector incidents, one overnight) and an
unredirected background launch that deadlocked socat on a full stdout
pipe. Both fixed by having the orchestrator pass exact log/socket paths
directly and always launching through the harness's tracked-background
mechanism.

First full campaign attempt ran all 9 cells as three ISA-blocked loops,
reusing one build per architecture -- caught mid-run: this confounds ISA
with time/session-order, invalidating the Latin square design. Discarded
(logs kept as audit artifacts, not treated as valid data) and re-run
clean: all 9 (arch, seed) cells in fully randomized order, fresh clean
rebuild before every single cell, one continuous sitting. Result:
4,320/4,320 rows captured, zero VM errors anywhere.

This validates the campaign mechanism runs cleanly and reproducibly under
the post-4.6 Stadium substrate -- satisfies item 5.1's own concern that a
green POST suite isn't evidence determinism holds post-migration. It does
NOT produce an ACL-RWT overhead number: ACL.4th is not self-activated in
this repo's default init.4th, so these 9 cells ran with ACL inactive.
Reproducing the original +0.0054%-+0.0088% measurement needs a paired
ACL-enabled/disabled run using this now-validated mechanism -- scoped,
not attempted here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-08-20 09:16:18 -04:00
co-authored by Claude Sonnet 5
parent ea5d2c7ceb
commit 79d160c1ca
37 changed files with 1180490 additions and 5686 deletions
+103
View File
@@ -1890,3 +1890,106 @@ two obstacles/workarounds, replacing their earlier "not yet visually confirmed"
This closes the last open item from SDK v1.9.0 scoping's own list. Only the version-string bump
remains, and it's a release decision, not implementation work.
## M. ACL-RWT-style 3x3 Latin square re-run on the Stadium substrate — 2026-08-19/20
Captain Bob asked to start the deferred F.3 item ("5.1 DoE re-run + ACL-RWT re-measurement")
now that item 4.6 (Section H) closed. What follows covers the mechanism work and the campaign
itself; the ACL overhead *number* is explicitly NOT produced here (see caveat at the end).
**A real, previously-unknown bug found before any campaign work could start: `EXEC-DOE`'s own
CSV output is almost entirely lost to console interleaving with the routine per-tick
`[HADES][DOE]` heartbeat export, the same bug class as Section L's framebuffer `PLOT` case.**
First pilot cell (amd64, seed 12345, `12345 30 EXEC-DOE`) ran cleanly to `DOE: complete` with
zero VM errors, but only 1 of the expected 480 `EMIT-ROW` lines survived in the log — the very
last one (`run_id=479`). Root cause identical in shape to Section L: the heartbeat CSV logger
(`doe_log.c`) and `EXEC-DOE`'s own `EMIT-ROW` prints share the same serial console, and the
heartbeat's continuous per-tick output clobbers `EXEC-DOE`'s rows almost every time. Fix:
`HB-OFF` immediately before `EXEC-DOE`, `HB-ON` once `DOE: complete` appears — confirmed this
recovers all 480/480 rows cleanly (verified repeatedly, see below). `HB-ON` before `EXEC-DOE`
(the reverse order) does **not** fix it — tested directly, heartbeat rows kept streaming
throughout and the same near-total row loss recurred, confirming `HB-OFF` (not `HB-ON`) is the
side that actually silences the per-tick export.
**A second, separate finding: `L8-DOE`/`WL-HI`/`WL-LO` (the mechanism `experiments/bare_metal/
README.md` describes as "the DoE," auto-run from `init.4th`) do not exist anywhere in
`capsules/` — confirmed by grep, zero matches, and `init.4th` calls no DoE mechanism at all.**
This matches Section K's own earlier finding but is re-confirmed here since it directly
determined what "re-run the DoE" had to mean in practice: the only live, invokable mechanism is
`doe.4th`'s `EXEC-DOE ( seed n-reps -- )`, loaded via `S" doe.4th" EXEC` (not auto-run either).
README's staleness on this point remains flagged, not fixed (same as Section K).
**Third finding: `DOE_SEED` (`Makefile.starkernel:233`, default `12345`) is declared but never
referenced anywhere else in the Makefile — vestigial, same class of staleness as `L8-DOE`.**
No build-time seed injection exists; `EXEC-DOE`'s seed must be typed at the live `ok>` prompt.
**Tooling built to drive this: QEMU serial socket injection via `socat`, since these campaigns
require typing FORTH commands into a running kernel after boot, not just observing serial
output.** `Makefile.starkernel`'s `qemu` target already exposes a Unix-domain socket per
architecture (`-chardev socket,...,path=$SOCK`); `printf '<command>\r\n' | socat -
UNIX-CONNECT:$SOCK` reliably injects a line into the live REPL. Two real defects found and
fixed in this tooling itself, both session-scoped (scratchpad scripts, not committed):
1. **Log-discovery race.** An early injector script auto-discovered "the new log file" by
snapshotting existing logs (`OLD_LOGS`) then polling for one not in that set. If the
snapshot happened *after* the target boot's own `touch $LOG` (which happens early in the
`qemu` rule, before qemu itself launches) — a real possibility any time two separate tool
calls are used to start the boot and then the injector — the target log gets permanently
self-excluded and the script spins forever. Root cause of two separate stuck-injector
incidents (one for ~15 hours, overnight; one caught within minutes on retry). **Fix:**
stopped having the injector discover its own target; the orchestrating side determines the
exact log/socket path once (via a short, explicitly-bounded wait) and passes it in directly
— no discovery, no race.
2. **Unredirected background launch.** One injector was launched via a bare `&`/`disown`
instead of the harness's own tracked-background mechanism. Its first `socat` call began
relaying the live (heartbeat-heavy, pre-`HB-OFF`) serial stream back through its own stdout;
with nothing reading that pipe, the buffer filled and `socat` blocked permanently inside a
`write()` syscall, deadlocking the whole script before it ever delivered a single command.
Every properly-tracked background launch (which redirects output to a drained file)
avoided this. **Fix:** never launch these injectors outside the tracked-background
mechanism.
**A related process-discipline lesson, not a code defect: the first attempt at the full
campaign ran all 9 cells as three strict ISA-blocked loops (amd64 x3, then aarch64 x3, then
riscv64 x3), reusing one build per architecture rather than rebuilding per cell.** Caught by
Captain Bob mid-run: this confounds ISA with time/session-order — exactly what a Latin square
is supposed to control against — and reusing one build across seeds doesn't match "every
ISA-seed pairing occupies a unique session" (the original campaign's own stated design
principle, per `bare_metal_doe_report.tex`). The 3 amd64 cells completed under the flawed
protocol were discarded (not deleted — logs remain as audit artifacts, just not treated as
part of the valid campaign).
**Final campaign, run clean in one continuous sitting 2026-08-20, randomized order, fresh
`clean` + full rebuild before every single cell:**
| Order | Arch | Seed | Rows | Errors |
|-------|---------|-------|---------|--------|
| 1 | riscv64 | 13579 | 480/480 | 0 |
| 2 | aarch64 | 12345 | 480/480 | 0 |
| 3 | amd64 | 13579 | 480/480 | 0 |
| 4 | amd64 | 12345 | 480/480 | 0 |
| 5 | riscv64 | 67890 | 480/480 | 0 |
| 6 | amd64 | 67890 | 480/480 | 0 |
| 7 | aarch64 | 67890 | 480/480 | 0 |
| 8 | aarch64 | 13579 | 480/480 | 0 |
| 9 | riscv64 | 12345 | 480/480 | 0 |
**4,320/4,320 total rows captured across all 9 cells, zero VM errors, zero `UNKNOWN WORD`
faults anywhere in any cell.** Order was randomized up front (Python `random.shuffle` over the
9 (arch, seed) pairs), not the sequential/nested order shown by architecture above. Raw serial
logs (audit artifacts, committed): `logs/20260820-0{82610,83018,83710,84147,84627,85030,85509,
90205,90903}/`. Extracted per-cell CSVs (note: `scripts/extract_doe.sh` targets a *different*,
unrelated CSV format — the routine `[HADES][DOE]` heartbeat export, not `EXEC-DOE`'s own
output — so a separate extraction was written for this data):
`experiments/bare_metal/runs/acl-rwt-20260820/`.
**What this closes, and what it explicitly does NOT close.** This satisfies item 5.1's own
concern — "a green POST suite is not evidence that determinism holds under the Stadium
migration, needs its own campaign" — the campaign mechanism itself runs cleanly, reproducibly,
and completely under the post-4.6 Stadium substrate, across all three architectures, under
proper randomized-order/fresh-build discipline. **It does NOT produce an ACL-RWT overhead
number.** `capsules/ACL.4th` is not self-activated in this repo's default `init.4th`
(`\ S" ACL.4th" EXEC`, commented out) — these 9 cells ran with ACL inactive, same as every
other boot in this document. Reproducing the original `+0.0054%+0.0088%` measurement would
need a paired run (ACL enabled vs. disabled) using this now-validated mechanism and tooling —
scoped but not attempted here.