diff --git a/docs/matching-cookbook.md b/docs/matching-cookbook.md index 56e8a2f09..f989d6987 100644 --- a/docs/matching-cookbook.md +++ b/docs/matching-cookbook.md @@ -4462,6 +4462,12 @@ diverged. **the moment it banks**. Fix: `git add -u src/` (every modified tracked file under src/), which also retires the `src/ov_*/*.c` filename glob that once omitted 4 R22-verified banks from a commit because a family's members do not all live in the same-named split. + **COMPLEMENTARY HOLE (2026-07-22): `git add -u` misses the NEW files an ISOLATION creates.** A jtbl + sweep cuts a fresh region file per sibling (`src//_jr_.c`), which is UNTRACKED — so + `-u` commits the modified TU and drops the file holding the banked body, i.e. a tree that cannot + clean-rebuild. For any carve/isolation bank use **`git add -A src/ config/`**. `jtbl_family_bank` + already refuses to sweep on an uncommitted `config/`+`src/` (its per-sibling revert restores from + HEAD), and that guard is what caught this — a fail-loud precondition doing exactly its job. 2. **The `_xform` ladder dirs accumulate.** `-cn/-cast/-rc/-uni` are reused across runs and the transform tools only write the drafts they are handed, so every stale draft from every previous run survives and is re-submitted to the byte-gate. Measured: the grinder submitted **1** draft, the gate diff --git a/src/ov_SC06_018/ov_SC06_018_jr_8017C24C.c b/src/ov_SC06_018/ov_SC06_018_jr_8017C24C.c index 98162983c..e3622d66d 100644 --- a/src/ov_SC06_018/ov_SC06_018_jr_8017C24C.c +++ b/src/ov_SC06_018/ov_SC06_018_jr_8017C24C.c @@ -4290,7 +4290,185 @@ INCLUDE_ASM("asm/ov_SC06_018/nonmatchings/ov_SC06_018_jr_8017C24C", func_8018EDB INCLUDE_ASM("asm/ov_SC06_018/nonmatchings/ov_SC06_018_jr_8017C24C", func_8018F060); -INCLUDE_ASM("asm/ov_SC06_018/nonmatchings/ov_SC06_018_jr_8017C24C", func_8018F694); +// @class: other +// @stuck: none — MATCH (478 ins, relocation-masked byte-identical) + +#define gte_ldv0(r0) __asm__ volatile ( \ + "lwc2 $0, 0( %0 );" \ + "lwc2 $1, 4( %0 )" \ + : \ + : "r"( r0 ) ) + +#define gte_rtps() __asm__ volatile ("nop;nop;rtps") + +#define gte_stsxy(r0) __asm__ volatile ( \ + "swc2 $14, 0( %0 )" \ + : \ + : "r"( r0 ) \ + : "memory" ) + +#define gte_stszotz(r0) __asm__ volatile ( \ + "mfc2 $12, $19;" \ + "nop;" \ + "sra $12, $12, 2;" \ + "sw $12, 0( %0 )" \ + : \ + : "r"( r0 ) \ + : "$12", "memory" ) + +#define gte_stflg(r0) __asm__ volatile ( \ + "cfc2 $12, $31;" \ + "nop;" \ + "sw $12, 0( %0 )" \ + : \ + : "r"( r0 ) \ + : "$12", "memory" ) + +typedef struct { u16 vx, vy, vz, pad; } UVEC_8018F694; +typedef struct { u32 addr : 24; u8 len; } PTag_8018F694; +typedef struct { + u32 addr : 24; + u8 len; + u8 r0, g0, b0, code; + s16 x0, y0, x1, y1; +} LineF2_8018F694; +typedef struct { + u32 addr : 24; + u8 len; + u32 code0; +} TPage_8018F694; +typedef struct { + u8 r, g, b, pad0; + u8 dr, dg, db, pad1; + s16 vx, vy, vz, tm; +} Blip_8018F694; + +void func_8018F694(void) +{ + extern void func_8004914C(void *); + extern void func_800491AC(void *); + extern void *func_80010A08(s32); + extern u8 D_800AF648; + extern u8 D_800A6610[]; + extern short D_800B9A02; + extern Blip_8018F694 D_801D57F0[]; + + UVEC_8018F694 sxy; + UVEC_8018F694 p0; + UVEC_8018F694 p1; + long flag; + long otz; + s32 i; + PTag_8018F694 *ot; + PTag_8018F694 *otp; + Blip_8018F694 *bp; + s32 t, c, d, z, x, n; + + ot = (PTag_8018F694 *)&D_800A6610[(*(u16 *)&D_800B9A02) << 14]; + func_8004914C(&D_800AF648); + func_800491AC(&D_800AF648); + bp = D_801D57F0; + +#define EMIT_8018F694() { \ + LineF2_8018F694 *pk = (LineF2_8018F694 *)func_80010A08(0x10); \ + pk->len = 3; \ + pk->code = 0x42; \ + pk->r0 = bp->r; \ + pk->g0 = bp->g; \ + pk->b0 = bp->b; \ + pk->x0 = p0.vx; \ + pk->y0 = p0.vy; \ + pk->x1 = p1.vx; \ + pk->y1 = p1.vy; \ + pk->addr = otp->addr; \ + otp->addr = (u32) pk; } + + for (i = 0; i < 32; i++, bp++) { + t = bp->tm; + __asm__ __volatile__ ("" : "=r" (t) : "0" (t)); + if (t != 0) { + gte_ldv0(&bp->vx); + gte_rtps(); + gte_stsxy((long *) &sxy); + gte_stflg(&flag); + gte_stszotz(&otz); + if ((u16) (sxy.vx + 0x200) < 0x401 && + (u16) (sxy.vy + 0x180) < 0x301 && + (u32) (otz - 0x10) < 0x1000) { + z = otz - 0x10; + c = t >> 6; + d = (t * 3) >> 8; + otp = (PTag_8018F694 *) ((z << 2) + (s32) ot); + + p0.vx = sxy.vx - c; + p0.vy = sxy.vy - c; + p1.vx = p0.vx + d; + p1.vy = p0.vy; + EMIT_8018F694(); + + p1.vx = p0.vx; + p1.vy = p0.vy + d; + EMIT_8018F694(); + + p0.vx = sxy.vx + c; + p1.vx = p0.vx - d; + p1.vy = p0.vy; + EMIT_8018F694(); + + p1.vx = p0.vx; + p1.vy = p0.vy + d; + EMIT_8018F694(); + + p0.vy = sxy.vy + c; + p1.vx = p0.vx - d; + p1.vy = p0.vy; + EMIT_8018F694(); + + p1.vx = p0.vx; + p1.vy = p0.vy - d; + EMIT_8018F694(); + + p0.vx = sxy.vx - c; + p1.vx = p0.vx + d; + p1.vy = p0.vy; + EMIT_8018F694(); + + p1.vx = p0.vx; + p1.vy = p0.vy - d; + EMIT_8018F694(); + + { + TPage_8018F694 *tp = (TPage_8018F694 *) func_80010A08(8); + tp->len = 1; + tp->code0 = 0xE100002A; + tp->addr = otp->addr; + otp->addr = (u32) tp; + } + + n = bp->r; + x = n - bp->dr; + n = x; + if (x < 0) { n = 0; } + bp->r = n; + n = bp->g; + x = n - bp->dg; + n = x; + if (x < 0) { n = 0; } + bp->g = n; + n = bp->b; + x = n - bp->db; + n = x; + if (x < 0) { n = 0; } + bp->b = n; + x = n | (bp->r | bp->g); + if ((u8) x == 0) { + bp->tm = 0; + } + } + } + } +} + INCLUDE_ASM("asm/ov_SC06_018/nonmatchings/ov_SC06_018_jr_8017C24C", func_8018FE0C); diff --git a/tools/harvest_verify.py b/tools/harvest_verify.py index bfc777d4f..f20dd1755 100644 --- a/tools/harvest_verify.py +++ b/tools/harvest_verify.py @@ -97,13 +97,23 @@ def classify_fail(got_sha): return 'SKIP' if got_sha is not None: return 'DIFF' - m = _PLUMBING.search(_last_err) - if m: - # pull the offending line for the log (e.g. "redefinition of 's16'") - for ln in _last_err.splitlines(): - if m.re.search(ln): - return 'PLUMBING: ' + ln.strip()[:90] - return 'PLUMBING: ' + m.group(0).lower() + # §58 RED-HERRING GUARD (2026-07-22). The build log is full of BENIGN WARNINGS — chiefly + # `warning: conflicting types for built-in function 'memcpy'`, which fires from an unrelated + # TU position on essentially every overlay build. The old code searched the whole stderr and + # returned the FIRST match, so that warning won: an 11-draft re-probe returned the SAME + # 'PLUMBING: …conflicting types for built-in fu' label for 8 of 8 failures, across four + # genuinely different causes. A label that is identical for every input carries no + # information, and it is worse than none — the cookbook had to record "the gate label is + # useless here, splice individually and read real cc1 stderr" as a manual step. Classify on + # NON-warning lines only, and when nothing but warnings matched, surface the real error + # instead of guessing (R32: report the gap, do not paper over it). + lines = [ln for ln in _last_err.splitlines() if 'warning:' not in ln] + for ln in lines: + if _PLUMBING.search(ln): + return 'PLUMBING: ' + ln.strip()[:90] + errs = [ln for ln in lines if re.search(r'\berror\b|\bError \d', ln)] + if errs: + return 'CC1-FAIL: ' + errs[-1].strip()[:90] return 'CC1-FAIL' @@ -209,7 +219,22 @@ def _reload_corpus(): def _jtbl_snapshot(): - """Text of the config files a carve/isolation may rewrite (for an exact undo).""" + """Text of EVERY file a carve/isolation may rewrite — config AND the binary's sources. + + SOURCES ARE NOT OPTIONAL (byte-witnessed 2026-07-22). `jr_isolate_all` repartitions a code + object by writing region 0 back over the ORIGINAL `src//.c` (truncated to just that + region) and emitting the rest as new `_jr_.c` files. An undo that restores only config/ + and deletes the new region files therefore leaves the original TU PERMANENTLY TRUNCATED — + its stubs are gone, and nothing regenerates them (splat does not rewrite a committed + overlay .c). Measured on an 11-draft re-probe: live stubs fell 419 -> 414 -> 406 -> 395 as + successive rejected drafts each ate a TU, ending in `undefined reference to func_80191C50`. + + Worse, it is INVISIBLE to the gate that caused it: the incremental build keeps linking the + stale objects (§42b), so `make build` stays green while a CLEAN rebuild fails. That is the + mechanism behind the "139/140, twice" reading that became the §61c blocker — the tree was + being eaten by the very undo meant to protect it. + + So snapshot the whole source set, and undo by RESTORE, never by an inverse transform (§61).""" out = {} for q in (os.path.join(REPO, 'config/splat.%s.yaml' % a.binary), os.path.join(REPO, 'config/overlays.mk')): @@ -217,10 +242,15 @@ def _jtbl_snapshot(): out[q] = open(q).read() except OSError: pass + for q in glob.glob(os.path.join(REPO, 'src/%s/*.c' % a.binary)): + try: + out[q] = open(q).read() + except OSError: + pass return out -def _jtbl_restore(snap, regions_before): +def _jtbl_restore(snap): """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. @@ -233,7 +263,11 @@ def _jtbl_restore(snap, regions_before): 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: + # Remove every source file the attempt CREATED. Derived from the snapshot (which holds the + # exact pre-attempt file set), not from the `_jr_*` name shape — an isolation may emit a + # region whose name that glob does not predict, and a leftover .c is picked up by the OBJS + # glob at the next parse (R33: derive from the recorded state, do not re-guess it). + for rf in set(glob.glob(os.path.join(REPO, 'src/%s/*.c' % a.binary))) - set(snap): try: os.remove(rf) except OSError: @@ -243,10 +277,10 @@ def _jtbl_restore(snap, regions_before): def _jtbl_prep_one(fn): - """Prep ONE table-bearing draft's carve. Returns (ok, snapshot, regions_before).""" + """Prep ONE table-bearing draft's carve. Returns (ok, snapshot).""" 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))) + return True, None + snap = _jtbl_snapshot() done = [] for fn in [fn]: st = _stubs.get(fn) @@ -280,12 +314,12 @@ def _jtbl_prep_one(fn): print(' [jtbl] carve FAILED %s: %s' % (fn, last[0][:120])); continue done.append(fn) if not done: - _jtbl_restore(snap, regions_before) - return False, None, None + _jtbl_restore(snap) + return False, None _sh(['make', '--no-print-directory', 'extract', 'BINARY=%s' % a.binary]) _reload_corpus() print(' [jtbl] carved %s' % done[0]) - return True, snap, regions_before + return True, snap def _stub_line(fn): @@ -332,15 +366,15 @@ while i < len(items): # 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 + _jsnap = None if len(chunk) == 1: - _ok, _jsnap, _jregions = _jtbl_prep_one(chunk[0]) + _ok, _jsnap = _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) + _jtbl_restore(_jsnap) # stranded-carve + truncated-TU undo (R32/§61) # 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)