diff --git a/docs/matching-cookbook.md b/docs/matching-cookbook.md index 223cbe1f56..bede2f7209 100644 --- a/docs/matching-cookbook.md +++ b/docs/matching-cookbook.md @@ -4682,3 +4682,36 @@ the 9 preserved cracks. region files THIS run created (§61's constraint, applied where I had first ignored my own rule). * **Being in a `_jr_*` TU ≠ having a table.** Only 4 of 8 wave drafts in jtbl-carved TUs actually reference a `jtbl_`; the stage correctly prepares only those. The other 4 fail for other classes. + +### §61c — The jtbl bank is INCREMENTALLY valid and CLEAN-INVALID (Phase 29, 2026-07-21) — the blocking finding + +`func_80135A4C` banks through the automated jtbl path every time: `[jtbl] carved` → `+ chunk(1)` → +`verified 1 / failed 0 BYTE-IDENTICAL`. **And it fails a clean rebuild, twice, identically:** + + incremental (harvest_verify's own gate) : BYTE-IDENTICAL + make clean && extract-all && check-all : 139 passed, 1 failed ([FAIL] ov_SC06_018) + +So the carve+isolation path yields a state that is **not reproducible from committed config + source** +— the incremental tree carries something the clean pipeline does not reconstruct (extraction order, or +asm that only exists mid-flow). This is the §42b stale-incremental false pass in its most expensive +form: the gate that authorises the bank cannot see the defect, because the gate IS the incremental +build. + +**Until that reproducibility gap is closed, NO jtbl core can be banked** — not by hand, not by the +ladder. The correct next step is to diagnose the divergence itself (diff the incremental vs clean +`build/ov_SC06_018/**` object set and the generated `.ld`/asm for the carved subseg), NOT to bank more. + +**Two design faults found on the way, both real and both fixed in `harvest_verify`:** +1. **A stranded carve poisons the overlay.** `_jtbl_prep` carved a draft the gate then REJECTED; the + carve stayed with no owner (the fn is still `INCLUDE_ASM`), and `jr_inventory`'s 1:1 ownership + assertion then refused EVERY later isolation in that overlay (`[('UNOWNED','0x801d288c')]` = + func_801299C8's table). **The assertion was RIGHT and caught it** — R32/R33 working exactly as + designed. Fix: per-function carve with snapshot-restore on gate rejection. +2. **Per-function undo is unsound in a BATCH.** Isolation REPARTITIONS shared source, so restoring one + draft's snapshot deletes region files that now host OTHER pending drafts — their stubs vanish + (`KeyError` in render). jtbl drafts must therefore be processed **one per `harvest_verify` + invocation**, or the undo must be region-aware. + +**Measured, so the next session does not re-derive it:** of 11 preserved wave cracks, exactly ONE +(`func_80135A4C`) reaches byte-identical through the carve path; the other four table-bearing ones +fail one-at-a-time too, on the PLUMBING classes (§57 self-decl et al), not on the carve. diff --git a/tools/harvest_verify.py b/tools/harvest_verify.py index 3309778be9..bfc777d4f5 100644 --- a/tools/harvest_verify.py +++ b/tools/harvest_verify.py @@ -208,13 +208,47 @@ def _reload_corpus(): baseline = {q: open(q).read() for q in _touched} -def _jtbl_prep(): - """Make every table-bearing draft's carve valid. Returns the list prepared.""" - todo = [fn for fn in items if _fn_has_jtbl(fn)] - if not todo: - return [] +def _jtbl_snapshot(): + """Text of the config files a carve/isolation may rewrite (for an exact undo).""" + out = {} + for q in (os.path.join(REPO, 'config/splat.%s.yaml' % a.binary), + os.path.join(REPO, 'config/overlays.mk')): + try: + out[q] = open(q).read() + except OSError: + pass + return out + + +def _jtbl_restore(snap, regions_before): + """Undo a carve/isolation EXACTLY: restore the config text and delete only the region files + this attempt created. Then re-extract and re-derive the stub map. + + WHY THIS EXISTS (byte-proven 2026-07-21). A carve left behind by a draft the gate REJECTED has + no owner — the function is still INCLUDE_ASM — so `jr_inventory`'s 1:1 ownership assertion + (R32/R33) then refuses EVERY later isolation in that overlay: + committed .rodata carve ownership is not 1:1 … [('UNOWNED','0x801d288c')] + 0x801d288c is func_801299C8's table: prepped, gate-rejected, carve stranded. The assertion is + CORRECT and caught it; the defect was leaving the carve behind. One failed draft poisoned the + whole overlay for the rest of the batch (4 isolate-FAILs downstream).""" + for q, txt in snap.items(): + open(q, 'w').write(txt) + for rf in set(glob.glob('src/%s/%s_jr_*.c' % (a.binary, a.binary))) - regions_before: + try: + os.remove(rf) + except OSError: + pass + _sh(['make', '--no-print-directory', 'extract', 'BINARY=%s' % a.binary]) + _reload_corpus() + + +def _jtbl_prep_one(fn): + """Prep ONE table-bearing draft's carve. Returns (ok, snapshot, regions_before).""" + if not _fn_has_jtbl(fn): + return True, None, None + snap, regions_before = _jtbl_snapshot(), set(glob.glob('src/%s/%s_jr_*.c' % (a.binary, a.binary))) done = [] - for fn in todo: + for fn in [fn]: st = _stubs.get(fn) if st is None: continue @@ -245,11 +279,13 @@ def _jtbl_prep(): last = ((r.stdout or '') + (r.stderr or '')).strip().splitlines()[-1:] or [''] print(' [jtbl] carve FAILED %s: %s' % (fn, last[0][:120])); continue done.append(fn) - if done: - _sh(['make', '--no-print-directory', 'extract', 'BINARY=%s' % a.binary]) - _reload_corpus() - print(' [jtbl] carved %d/%d table-bearing draft(s): %s' % (len(done), len(todo), ' '.join(done))) - return done + if not done: + _jtbl_restore(snap, regions_before) + return False, None, None + _sh(['make', '--no-print-directory', 'extract', 'BINARY=%s' % a.binary]) + _reload_corpus() + print(' [jtbl] carved %s' % done[0]) + return True, snap, regions_before def _stub_line(fn): @@ -289,16 +325,22 @@ def commit(fns): verified.extend(fns) -_jtbl_prepared = _jtbl_prep() - i = 0 while i < len(items): chunk = items[i:i + a.chunk] i += a.chunk + # per-chunk jtbl prep (chunk==1 is the prescribed default, so this is per-function): carve the + # table, and if the gate then REJECTS the draft, undo the carve — a stranded carve has no owner + # and poisons every later isolation in the overlay (see _jtbl_restore). + _jsnap = _jregions = None + if len(chunk) == 1: + _ok, _jsnap, _jregions = _jtbl_prep_one(chunk[0]) if attempt(chunk): commit(chunk) print(' + chunk(%d): %s' % (len(chunk), ' '.join(chunk))) elif len(chunk) == 1: + if _jsnap is not None: + _jtbl_restore(_jsnap, _jregions) # stranded-carve undo (R32 ownership stays 1:1) # ATOMIC CHUNK — do NOT bisect (Phase-28 T6). The old code fell into the loop below and # re-ran attempt([fn]) on the SAME single element against the SAME baseline: a byte-identical # DUPLICATE build. classify_fail reads _last_sha/_last_err, which the failed attempt(chunk)