Files
BFM-decomp/tools/wave_snapshot.py
T
Drew T 0d05d91293 docs(phase-30 S45 p7): F1 confirmed live (cookbook 156) + the cheap-tier size cliff (157) + wave_snapshot
- cookbook 156: a FAILED draft can poison the fleet. gate_stage's arity pre-pass writes
  the shared engine_core.h before the gate; a rejected draft's caller-signature edit
  survived and broke 141/213 binaries. Byte-gate held (fail-closed). The trap: a broken
  tree makes every later gate report 'near' -- two batches of verdicts were void, not
  evidence. Standing practice: GATE_NO_ARITY=1, assert 'git status --porcelain
  src/shared config' empty after every batch, recover by revert+replay (deterministic).
- cookbook 157: the cheap-tier size cliff, measured over two controlled waves.
  Haiku 4-27 ins 86% (~44k tok/match); >=50 ins 20% (~177k, 4x worse). The documented
  '<=50' band was optimistic. Agent honesty 63/63 claims true across 100 drafters.
- tools/wave_snapshot.py: immutable sha1-manifested per-wave .s copy, so a running wave
  can no longer block R22's 'make clean'. Coverage-asserting (exit 2 on a missing target),
  negative-control proven.
- docs/concurrency-design.md (Fable5): the lane contract, the false-bank correctness
  argument, and the finding that a worktree verify certifies the COMMIT -- strictly
  stronger than our main-tree R22, which also compiles untracked strays.
- checkpoint p7.
2026-08-07 18:04:26 -06:00

95 lines
4.0 KiB
Python

#!/usr/bin/env python3
"""wave_snapshot.py — give a crack-wave its OWN immutable copy of the .s files it needs.
WHY (the blocker this removes):
A wave's drafter agents read `asm/<bin>/nonmatchings/<bin>/<fn>.s` and iterate with
`match_one`. R22's clean-fleet verify starts with `make clean`, which DELETES `asm/`.
So a running wave blocked the authoritative byte-check, and the whole pipeline
(draft -> gate -> bank -> R22) had to run strictly serially.
A wave does not need the LIVE asm tree — it needs the handful of .s files for its own
targets, and those are immutable for the life of the wave (splat regenerates them
byte-identically from committed config + symbols; H3/R22). Snapshotting them decouples
drafting from every tree-mutating step: waves, gates and R22 can then overlap.
WHAT IT GUARANTEES (and what it does not):
- The snapshot is a byte-for-byte copy, recorded with a sha1 manifest, so a draft is
always verified against exactly the bytes the wave was scoped against.
- It does NOT weaken the byte-gate. `match_one` remains a CANDIDATE filter (§52b);
the whole-binary `harvest_verify` on the live tree is still the sole arbiter (G3/P9).
A stale snapshot can therefore only cause a wasted draft, never a false bank.
- R32: fails loud if any requested target has no .s, rather than silently shipping a
short wave (a wave that quietly drops targets looks identical to a low close-rate).
USAGE
python3 tools/wave_snapshot.py --targets .run/s45p5/args_wave4.json --out .run/wave4
-> .run/wave4/asm/<bin>/nonmatchings/<bin>/<fn>.s + .run/wave4/MANIFEST.sha1
then point the wave prompt's --asm-subdir at .run/wave4/asm/<bin>/nonmatchings/<bin>
"""
import argparse
import hashlib
import json
import os
import shutil
import sys
def sha1(p):
h = hashlib.sha1()
with open(p, 'rb') as fh:
for b in iter(lambda: fh.read(65536), b''):
h.update(b)
return h.hexdigest()
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--targets', required=True,
help='JSON list of {name, source} (the wave args file)')
ap.add_argument('--out', required=True, help='snapshot dir, e.g. .run/wave4')
ap.add_argument('--asm-root', default='asm')
ap.add_argument('--allow-missing', action='store_true',
help='downgrade the R32 coverage assertion to a warning')
a = ap.parse_args()
targets = json.load(open(a.targets))
if isinstance(targets, dict):
targets = targets.get('targets', [])
copied, missing = [], []
for t in targets:
fn, src = t['name'], t['source']
rel = os.path.join(src, 'nonmatchings', src, fn + '.s')
s = os.path.join(a.asm_root, rel)
if not os.path.exists(s):
missing.append(fn)
continue
d = os.path.join(a.out, 'asm', rel)
os.makedirs(os.path.dirname(d), exist_ok=True)
shutil.copy2(s, d)
copied.append((rel, sha1(d)))
os.makedirs(a.out, exist_ok=True)
with open(os.path.join(a.out, 'MANIFEST.sha1'), 'w') as fh:
for rel, h in sorted(copied):
fh.write(f'{h} {rel}\n')
print(f'snapshot -> {a.out}/asm ({len(copied)} .s files, manifest written)')
# R32: a silent skip is a defect. A short wave must be loud, not inferred later.
if missing:
msg = (f'{len(missing)} target(s) have NO .s under {a.asm_root}/: '
+ ' '.join(missing[:8]) + (' …' if len(missing) > 8 else ''))
if a.allow_missing:
print('WARNING (coverage): ' + msg)
else:
print('COVERAGE ASSERTION FAILED: ' + msg, file=sys.stderr)
print(' These would be silently absent from the wave. Fix the target list, '
'or pass --allow-missing to proceed deliberately.', file=sys.stderr)
return 2
print(f' point the wave at: {a.out}/asm/<bin>/nonmatchings/<bin>')
return 0
if __name__ == '__main__':
sys.exit(main())