fix(phase-31): wave-selector coverage + gate_lane crash-vs-empty (S52-1, S52-8)

build_wave_atlas.py:
- taken-set was a hardcoded 'abcdefghijkl' wave-letter literal -> glob('.run/wave_*_cards.json')
  (R33 derive, don't re-derive). NC: 634 -> 726 taken, strict superset, +92 cards from waves m/n
  that the literal would have silently re-issued.
- --exclude-bins defaulted to 'main' on the REFUTED link-defect rationale; default now empty and
  the help states the real reason (main cannot be gated incrementally; use tools/gate_main.py).
- new --only-bins allow-list: main waves need it, since gate_main rebuilds once per SLATE.

gate_lane.py: an unhandled gate_stage exception produced no JSON line -> v={} -> '0 banked',
indistinguishable from an honest empty gate (cost 2 cycles in P31). Now a non-zero rc or a missing
JSON line is labelled CRASH with the stderr tail, recorded in the results JSON, listed in the
summary as NEVER GATED, and exits non-zero. R39 NC both directions.
This commit is contained in:
Drew T
2026-08-15 09:31:10 -06:00
parent 683c44e15b
commit fb4c1f03a1
3 changed files with 54 additions and 11 deletions
+13
View File
@@ -57,6 +57,19 @@ R22 clean-fleet **213/213** after every banked batch · tools-health green · 0
- 2026-08-14 — **T9 COMPLETE.** `tools/warmstart.py` (the permuter/grinder FEEDER): `--from-banked` walks a banked exemplar's h_seq family's still-open members, builds remapped proven-body drafts (`symbol_map` + `aprop_autodraft.build_draft`, refusing on reloc-count mismatch), **stream-classifies member-vs-seed with ZERO compiles** (masked_diff-shaped dicts from ground-truth bytes → `residual_class.classify_streams`), enqueues ONLY permuter-shaped (bucket==permuter or LENGTH-DRIFT |Δ|≤2) as backlog near-records; `--lenmiss` ingests T8's 49-route. **Grinder patch NOT needed** (its candidates() deliberately keeps unclassified records — "unknown is not a reason to skip" — so pre-filtered enqueues flow as-is; documented in the feeder's docstring). Armed live: **49 + 10 enqueued, 120 refused** by the stream filter (the anti-92%-wasted-CPU discipline working). `family_cousins --weak-cards`: **954 units** (the 0.70–0.85 annotate-only band, never before consumed) as seeded-crack cards, ins-ranked, §168 laws embedded, model-routed **haiku 804 / v3 43 / sonnet 86 / opus 21** (cheap tiers dominate — the token-efficiency shape), 0 unresolved `.s`.
## SESSION S52 TASK LIST (2026-08-15, ultracode) — the monitorable view (no TaskCreate tool in this harness build)
- [x] **S52-1 — Preflight + wave-selector repair.** Tree clean @ `commit:2390`, no gate/grinder in flight. Fixed `build_wave_atlas.py`: (a) the already-waved set was a hardcoded `'abcdefghijkl'` wave-letter literal → now a `glob('.run/wave_*_cards.json')` derivation (R33); **NC: old 634 → new 726 taken, strict superset, +92 previously-missable cards from waves m/n**; (b) `--exclude-bins` defaulted to `main` carrying the REFUTED link-defect rationale → default now empty, help corrected to the real (gate-path) reason; (c) new `--only-bins` allow-list (main waves need it — `gate_main` rebuilds once per SLATE, so main has no per-TU gate cost).
- [x] **S52-2 — Atlas regen** (`make atlas`, $0) — the selector was stale by ~448 banks (last regen predates waves J–N).
- [x] **S52-8 — `gate_lane` crash-vs-empty fixed.** A non-zero rc or a missing JSON line is now labelled `‼ CRASH`, prints the stderr tail, records `{'CRASH':True,…}` in the results JSON, lists the never-gated groups in the summary, and exits non-zero. R39 NC both ways: crash→CRASH, honest-empty→not-CRASH.
- [~] **S52-3 — Wave O: MAIN + the UNKNOWN probe** — LAUNCHED (`wf_278b05de-bae`, 49 cards / **6,266 ins**, 2 gate groups). **The selection finding that reshaped this wave: main's agent-lane pool at ≥20 ins is nearly SPENT (21 left after 113 already waved) — main's remaining 52,714 ins are overwhelmingly UNKNOWN-lever, as are 138k fleet-wide.** So wave O is deliberately three arms: 21 main head-crack (avg 145 ins, opus) + 18 main UNKNOWN (avg 122) + 10 overlay UNKNOWN (`ov_SC04_011`, avg 103, sonnet). The UNKNOWN arms are an R37 probe of the biggest unclaimed block on the atlas — 1,239 candidates at 60–120 ins alone. Wave prompt gained §175/§176A–C (statement-order-around-a-call first; pin the interloper; **a pin cannot schedule across a call and can silently DELETE an instruction**).
- [ ] **S52-4 — Independent re-verify (R14) + `gate_main.py --apply`** (one clean rebuild per slate).
- [ ] **S52-5 — R22 clean fleet 213/213 + commit** (task + this log together).
- [ ] **S52-6 — Wave P: overlay TU-packed**, drafted concurrently with the main gate (drafting is tree-free).
- [ ] **S52-7 — Recover the ~10 conflict-dropped main drafts** (waves J/K/L; verified-correct, need cast-at-use).
- [ ] **S52-8 — Tooling debt: `gate_lane` swallows `gate_stage` stderr** (reports a crash as `0/0/0`).
- [ ] **S52-9 — Bank idioms into the cookbook + refresh this checkpoint** BEFORE any pause (memory `bank-idioms-before-checkpoint`).
## Campaign velocity ledger (T10+)
| wave | lane | cards | standalone MATCH | banked | tokens | tok/bank | lesson banked |
+21 -7
View File
@@ -20,7 +20,7 @@ MUST NOT run while a gate is in flight (R35 — corpus.stubs() misreports substi
Usage: build_wave_atlas.py <out.json> [N] [--max-bins K] [--min-ins M] [--levers a,b,c]
"""
import json, sys, collections, subprocess, argparse
import json, sys, collections, subprocess, argparse, glob
sys.path.insert(0, 'tools')
import corpus
@@ -33,25 +33,38 @@ ap.add_argument('--max-ins', type=int, default=120, help='above this the bulk la
ap.add_argument('--levers', default='head-crack,seeded-crack,redraft,len-vein,integration,family-sweep,tiny-direct',
help='agent-draftable levers; UNKNOWN/tell/jtbl/o0/cc1 need their own lanes')
ap.add_argument('--atlas', default='.run/atlas.json')
ap.add_argument('--exclude-bins', default='main',
help='comma-separated binaries to skip. main is excluded BY DEFAULT: it is blocked '
'on a link-resolution defect (a byte-correct C fn retargets a jal between game '
'code and a linked PsyQ archive object), not on matching — see CURRENT_PHASE.md')
ap.add_argument('--exclude-bins', default='',
help='comma-separated binaries to skip. NOTHING is excluded by default. '
'(History: main used to be excluded on a "link-resolution defect" — that '
'diagnosis was REFUTED 2026-08-15 by a null-draft control: the false diff '
'reproduces with ZERO drafts substituted. main simply cannot be gated '
'INCREMENTALLY, because its extract runs psyq_integrate/ld_interleave and '
'rewrites the .ld. Draft main like any binary; gate it with '
'tools/gate_main.py, never gate_lane/gate_stage.)')
ap.add_argument('--only-bins', default='',
help='comma-separated allow-list; if set, ONLY these binaries are eligible. '
'Use --only-bins main for a main wave: gate_main.py rebuilds the whole EXE '
'once per SLATE, so main has no per-TU gate cost and --max-bins can be large.')
a = ap.parse_args()
EXCLUDE = {b for b in a.exclude_bins.split(',') if b}
ONLY = {b for b in a.only_bins.split(',') if b}
busy = subprocess.run(['pgrep', '-f', 'tools/(gate_stage|dedup_propagate|gate_lane)'],
capture_output=True, text=True)
if busy.returncode == 0 and busy.stdout.strip():
sys.exit(f"REFUSING: gate in flight (pids {busy.stdout.split()}) — corpus.stubs() would misreport (R35).")
PRIORS = ['.run/wave_p31%s_cards.json' % c for c in 'abcdefghijkl']
# R32/R33: derive the already-waved set from what is ON DISK, never from a hardcoded wave-letter
# list (the literal 'a'..'l' silently missed waves m and n and would have re-issued their cards).
PRIORS = sorted(glob.glob('.run/wave_*_cards.json'))
taken = set()
for p in PRIORS:
try:
taken |= {c.get('fn') or c.get('name') for c in json.load(open(p))}
except (FileNotFoundError, json.JSONDecodeError):
except (FileNotFoundError, json.JSONDecodeError, TypeError):
pass
if not PRIORS:
print('NOTE: no prior wave card files found — nothing filtered as already-waved', file=sys.stderr)
levers = set(a.levers.split(','))
atlas = json.load(open(a.atlas))
@@ -86,6 +99,7 @@ for g in atlas['groups']:
fn, b, nins = m.get('name'), m.get('b'), m.get('nins') or 0
if not fn or not b: skipped['no-name'] += 1; continue
if b in EXCLUDE: skipped['excluded-binary'] += 1; continue
if ONLY and b not in ONLY: skipped['not-in-only-bins'] += 1; continue
if fn in taken: skipped['already-waved'] += 1; continue
if not (a.min_ins <= nins <= a.max_ins): skipped['out-of-band'] += 1; continue
if not is_open(b, fn): skipped['already-banked'] += 1; continue
+20 -4
View File
@@ -44,7 +44,7 @@ if n_g != len(cards): # R32: a silent skip is a DEFECT, not a no-op
print(f"R32 COVERAGE: {len(cards)} records -> {n_g} gateable; {len(missing)} missing drafts: {missing[:8]}", flush=True)
if n_g == 0: sys.exit("R32: nothing gateable — refusing to report 0 as a result")
print(f"gating {n_g} drafts in {len(groups)} groups", flush=True)
banked=[]; res={}
banked=[]; res={}; crashed=[]
for (b,src),items in sorted(groups.items()):
if dirty():
subprocess.run("git checkout -- src/ config/",shell=True); print(" (reverted residue)",flush=True)
@@ -57,13 +57,28 @@ for (b,src),items in sorted(groups.items()):
if sub: cmd+=["--asm-subdir",sub]
r=subprocess.run(cmd,capture_output=True,text=True) # NO outer timeout (§169 law 3)
js=[l for l in (r.stdout+r.stderr).splitlines() if l.startswith('{')]
v=json.loads(js[-1]) if js else {}
got=v.get('verified',[]); banked+=got
print(f" {b:14} {os.path.basename(src or ''):34} {len(items)}d -> {len(got)} banked", flush=True)
# A CRASH IS NOT A RESULT. Without this, an unhandled gate_stage exception (e.g.
# corpus.CorpusError) left js empty -> v={} -> "0 banked", INDISTINGUISHABLE from an honest
# empty gate. That cost two cycles in P31. Surface stderr and label the group CRASH.
if r.returncode != 0 or not js:
tail="\n".join((r.stderr or r.stdout or "").strip().splitlines()[-12:])
v={'CRASH':True,'returncode':r.returncode,'stderr_tail':tail,'verified':[]}
crashed.append(f"{b}:{src}")
print(f" {b:14} {os.path.basename(src or ''):34} {len(items)}d -> ‼ CRASH rc={r.returncode} "
f"(NOT an empty result)\n{tail}", flush=True)
else:
v=json.loads(js[-1])
got=v.get('verified',[])
print(f" {b:14} {os.path.basename(src or ''):34} {len(items)}d -> {len(got)} banked", flush=True)
banked+=v.get('verified',[])
res[f"{b}:{src}"]=v
json.dump(res,open(outp,'w'),indent=1)
if dirty(): subprocess.run("git checkout -- src/ config/",shell=True)
print(f"\nBANKED {len(banked)}: {banked}", flush=True)
if crashed:
print(f"‼ {len(crashed)} GATE GROUP(S) CRASHED and were NEVER GATED: {crashed}\n"
f" Their drafts are unjudged — re-run those groups after fixing the cause "
f"(run tools/gate_stage.py directly for the full traceback).", flush=True)
json.dump(banked,open(outp.replace('.json','_banked.json'),'w'))
# propagate per function
for fn in banked:
@@ -76,3 +91,4 @@ for fn in banked:
subprocess.run(f'git commit -q -m "feat({tag}): propagate {fn} (gate lane)"',shell=True)
print(f" prop {fn}: committed", flush=True)
print("DONE", flush=True)
if crashed: sys.exit(f"EXIT NON-ZERO: {len(crashed)} gate group(s) crashed — output above is INCOMPLETE.")