mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
feat(draw): audit the exclude list as a PREREQUISITE — a stale one is refused
An exclude list records what the TOOLING could not do, then gets treated as a property of the FUNCTIONS. Nothing re-examined it, so every tool fix left behind a population that is now tractable and still marked impossible — invisible, because the draw filters it out before anything measures it. MEASURED one day after .run/S71_exclude.txt was written: 88 of its 107 entries were stale — 28 already banked, 14 linked PsyQ symbols that were never targets, and 46 whose blocker had since been fixed. Those 46 are 12,750 instructions of open, drawable work including main:SaveLoadRoutine (1,165), the largest function left in main. * tools/exclude_audit.py (NEW) — classifies each entry by its CURRENT blocker (BANKED / LINKED / RE-PROBE / CARVE-BLOCKED / WALL), regenerates keeping only the still-valid classes, and --assert-fresh exits 3 on staleness. * draw_waves --exclude-file — runs that audit and REFUSES to draw on a stale list, naming the counts and the regenerate command. --exclude-stale-ok still draws but prints what it ignores: skipping is possible, never silent. Also fixes the old --exclude parsing, which could not survive a '#' comment. * .run/S72_exclude.txt — the regenerated list: 19 entries (16 CARVE-BLOCKED + 3 WALL), each carrying its reason, down from 107. Verified in all three directions: stale refuses rc=1, fresh proceeds rc=0, override proceeds and announces. The parser's own report-don't-drop design caught a bug I introduced in it (comma-splitting before comment-stripping).
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# regenerated by tools/exclude_audit.py from .run/S71_exclude.txt
|
||||
# 19 still-valid of 107; 88 dropped as stale (banked / linked / blocker-since-fixed).
|
||||
# An exclude list records what the TOOLING could not do — regenerate it as
|
||||
# part of every tool fix, or it becomes a list of work you decided not to do.
|
||||
main:func_8005D9C4 # WALL: no jump table — curated compiler fact, not a tooling limit
|
||||
main:func_8005ECC0 # WALL: no jump table — curated compiler fact, not a tooling limit
|
||||
main:func_80062144 # WALL: no jump table — curated compiler fact, not a tooling limit
|
||||
ov_SC01_084:func_80182328 # CARVE-BLOCKED: ov_SC01_084_jr_8017F690 owns >1 raw jtbl span — needs a split
|
||||
ov_SC01_084:func_80182A00 # CARVE-BLOCKED: ov_SC01_084_jr_8017F690 owns >1 raw jtbl span — needs a split
|
||||
ov_SC02_005:func_80185060 # CARVE-BLOCKED: ov_SC02_005_jr_80181D30 owns >1 raw jtbl span — needs a split
|
||||
ov_SC02_005:func_80185E80 # CARVE-BLOCKED: ov_SC02_005_jr_80181D30 owns >1 raw jtbl span — needs a split
|
||||
ov_SC02_011:func_80183178 # CARVE-BLOCKED: ov_SC02_011_jr_8017AE2C owns >1 raw jtbl span — needs a split
|
||||
ov_SC02_011:func_80183630 # CARVE-BLOCKED: ov_SC02_011_jr_8017AE2C owns >1 raw jtbl span — needs a split
|
||||
ov_SC02_011:func_8018418C # CARVE-BLOCKED: ov_SC02_011_jr_8017AE2C owns >1 raw jtbl span — needs a split
|
||||
ov_SC02_011:func_80188E3C # CARVE-BLOCKED: ov_SC02_011_jr_8017AE2C owns >1 raw jtbl span — needs a split
|
||||
ov_SC03_105:func_801806F8 # CARVE-BLOCKED: ov_SC03_105_jr_8017C8D0 owns >1 raw jtbl span — needs a split
|
||||
ov_SC03_105:func_80180ABC # CARVE-BLOCKED: ov_SC03_105_jr_8017C8D0 owns >1 raw jtbl span — needs a split
|
||||
ov_SC03_105:func_80180EC0 # CARVE-BLOCKED: ov_SC03_105_jr_8017C8D0 owns >1 raw jtbl span — needs a split
|
||||
ov_SC03_105:func_801813BC # CARVE-BLOCKED: ov_SC03_105_jr_8017C8D0 owns >1 raw jtbl span — needs a split
|
||||
ov_SC03_105:func_801818E8 # CARVE-BLOCKED: ov_SC03_105_jr_8017C8D0 owns >1 raw jtbl span — needs a split
|
||||
ov_SC03_105:func_80181C84 # CARVE-BLOCKED: ov_SC03_105_jr_8017C8D0 owns >1 raw jtbl span — needs a split
|
||||
ov_SC03_105:func_8018624C # CARVE-BLOCKED: ov_SC03_105_jr_8017C8D0 owns >1 raw jtbl span — needs a split
|
||||
ov_SC03_105:func_801867D0 # CARVE-BLOCKED: ov_SC03_105_jr_8017C8D0 owns >1 raw jtbl span — needs a split
|
||||
@@ -754,6 +754,8 @@ Every script under `tools/` (plus the two report make-targets), grouped by purpo
|
||||
| | `tools/t5_targets.py` · `tools/t5_cards.py` · `tools/wave_card_fuel.py` · `tools/t5_bank.sh` | **(Phase 31 S63 T5)** The wave *draw* and the wave *bank* halves around the T4 harness: `t5_targets.py --wave .run/t5x --n 48 [--residue .run/t5w] [--main-only]` draws a routed slate (open per `corpus.stubs`, ledger `.run/t5/drawn.json` keyed (binary,fn), ≤120 ins → sonnet / >120 → opus, residue → opus once, stratified by band, ≤4 per binary, one fn NAME per wave — the harness keys packs/drafts by bare name, so `claude_wave_packs`/`wave_judge` now REFUSE duplicates, R43/R48); then **`t5_cards.py --wave .run/t5x`** BUILDS the target's OWN card fuel (`tu_ref` = in-TU banked neighbours §194-E, `decl_prior` = fleet signatures §196) from `wave_card_fuel.py` — those helpers extracted VERBATIM out of `build_wave_atlas.py` (which parses argv at import and so was unusable as a library) so both consumers share ONE oracle (R33); measured 88% tu_ref / 98% decl_prior on a fresh K-class slate, against 0% correct fuel before the fix. Then `claude_wave_packs.py --cards <wave>/cards.json` → Workflow `tools/workflows/claude_wave_draft.js` `args={wave, targets}` (one agent per target, `model=target.arm`) → `tools/t5_bank.sh .run/t5x sonnet opus` (refuses if a lane/gate runs or src/ config/ is dirty; `wave_judge --union`; then a RECOVERY pass — every un-banked draft through `fix_tu_ret_decls`, which retypes a destination TU's `extern void` decl on a value-returning function and re-gates (wave t5a: 2/2 banked, both byte-exact bodies refused for that decl alone); then the R22 clean fleet sweep parsed `N passed, 0 failed of N`; commit `— N banked`). |
|
||||
| | `tools/jtbl_rodata_pads.py --derive` | **(Phase 31 S62 T3a, cookbook §303)** Module path of the §8e pads filter: the Makefile runs `--derive $(BINARY) --tu <tu>` for every `md_*` object **and for `main` (P31 S72)** — jump-table pads derived at build time from the retail island + the emission stream (trailing pads `0t1`, table-aware, const data passes through). No stored spec; an anchor miss fails the build with the offset. **S72:** `--derive` now serves main too — `_file0_vram` returns the code segment's `vram - start` (the PS-X EXE's 0x800 header), which makes both `raw[a - vram]` and `vram + <yaml offset>` correct for the EXE and leaves flat overlays byte-identical; `_splat_yaml` resolves main to `splat.us.exe.yaml`. |
|
||||
| | **main's TU layout (P31 S72)** | main's game code is no longer one file. `src/800.c` was split at the jtbl-span TU boundaries into **`src/800.c`** (vram 0x800123F0-0x8002B0B4, owns `.rodata` span A 0x80072A38-0x80072C70), **`src/800_b.c`** (0x8002B0B4-0x80035270, span B 0x80072E44-0x80073140) and **`src/800_c.c`** (0x80035270-0x8003A444, span C 0x800732A0-0x8007344C), plus **`src/800_shared.h`** for the 19 typedefs + 2 includes that cross. Reason: one code object contributes exactly ONE contiguous `.rodata` run, so each jump-table span needs its own object or every switch function in it double-emits its table (cookbook §426/§431). main's island is now a 7-piece sandwich and `ld_interleave` runs with `--order`, not `--front/--tail`. **Any tool that reads "main's source" must glob `corpus.src_files('main')`, never hardcode `src/800.c`** — `reconcile_slate` did, and saw 133 of 187 typedefs after the split. |
|
||||
| | `tools/split_indicator.py` | **(P31 S72, cookbook §426/§431)** Which code subsegs MUST be split before their switch functions can bank: a code object contributes exactly ONE contiguous `.rodata` run, so a subseg owning raw jump tables in ≥2 non-adjacent island spans makes every switch function outside the one carveable span unbankable at any effort. **Decidable from the raw image at 0% matched** — no attempt needed. `--self-test` fires on main's pre-S72 island, stays silent on main today, and does not over-fire on a one-span subseg; linked PsyQ subsegs are excluded on principle. Runs in `make tools-health` (informational while 4 known violations exist — flip to a hard gate once they are split). First fleet run: 209/213 OK, 4 flagged holding 16 open fns / 3,613 ins. |
|
||||
| | `tools/exclude_audit.py` | **(P31 S72)** Classifies every wave-exclude entry by its CURRENT blocker — `BANKED` / `LINKED` / `RE-PROBE` (blocker since fixed) / `CARVE-BLOCKED` / `WALL` — and regenerates the list keeping only the still-valid classes. **`draw_waves --exclude-file` runs it as a PREREQUISITE and refuses a stale list** (`--exclude-stale-ok` overrides, loudly). Measured on `.run/S71_exclude.txt` one day after it was written: 88 of 107 entries stale, of which 46 were open drawable functions totalling 12,750 instructions — `main:SaveLoadRoutine` among them. |
|
||||
| | `tools/main_diff_locate.py` | **(P31 S72, cookbook §426/§427)** Turns a RED main gate — whose whole output is two SHA1s — into a NAMED list of divergent symbols: byte-diffs `build/us/SLUS_007.26` against `extracted/retail/SLUS_007.26`, coalesces the differing bytes into runs, and attributes them PER BYTE to symbols via `build/us/SLUS_007.26.map` (file-offset mapping DERIVED from each output section's `load address`, not the 0x800 header constant). `--focus <fn>` prints the routing verdict: **BODY REJECT** (divergence confined to the function) vs **PLUMBING REJECT** (the function is byte-identical, everything differs elsewhere → the §376/§378 chain) vs **MIXED**. `--self-test <addr>` is the negative control: flips one byte at a known address and asserts the containing symbol is named, plus the identical-pair-reports-zero direction. `gate_main` calls it automatically and preserves the red image + map under `.run/gate_main_fail/` BEFORE the R40 baseline control rebuilds over them. |
|
||||
| | `tools/blocker_probe.py` | **(Phase 29 SESSION-16, cookbook §65)** WHY a byte-correct draft fails the whole-binary gate. Read-only; **two oracles** (R34): STATIC (`cdecl.parse` + `cdecl.compatible` — cc1's own acceptance question, never text equality) beside the REAL cc1 (via `rtu_match`), leading with the DISAGREEMENT table. Classes `self_decl_hdr`/`self_decl_tu`/`callee_decl`/`data_decl`/`local_type`, each mapped to a blast-radius tier (T0 draft-only / T1 binary-local / T2 fleet-shared). Blockers STACK, so a function's tier is the MAX over them. 36 drafts in ~9 s. Replaced+deleted `.run/diag_plumbing.py`. |
|
||||
| | `tools/symcheck.py` | **(Phase 29 SESSION-18, cookbook §67a)** The pre-gate SYMBOL-SET guard: diffs the symbols a draft's object references (reloc records) against the target `.s`'s `%hi`/`%lo`/`jal` set. Catches the class **every masked oracle is structurally blind to** — `match_one`/`masked_diff` compare relocation-MASKED words (object-vs-`.s` mode is symbol-agnostic by construction) and `rtu_match` **compiles without linking**, so a draft that invents an extern no symbol table defines reads MATCH in both and can never bank (the SESSION-17 `func_801463A0` `_s`-alias trap). Negative-control-proven: on a draft with one renamed data extern, `match_one` reports the SAME 14 mismatched as the correct draft while `symcheck` exits 1 naming both the MISSING and the INVENTED symbol. `--c` (compiles via `match_one`, so the triple can never drift) or `--obj`. A cheap necessary condition, NOT a match oracle — still finish on the byte-gate (G3/P9). |
|
||||
|
||||
@@ -33,6 +33,18 @@ python3 tools/draw_waves.py --only-main --prefix .run/<name>m_ --waves 1 --per-w
|
||||
|
||||
* **`--prefix` IS A RELATIVE PATH.** `--prefix s67o` writes `./s67o1/` into the repo root, not
|
||||
`.run/`. Always pass `.run/<name>_`.
|
||||
* **THE EXCLUDE LIST IS AUDITED BEFORE THE DRAW, AND A STALE ONE IS REFUSED (P31 S72).** Pass it as
|
||||
`--exclude-file .run/<name>_exclude.txt`; `draw_waves` classifies every entry against the CURRENT
|
||||
tooling and **exits non-zero** if any is stale, naming the count and the regenerate command.
|
||||
`--exclude-stale-ok` still draws, but prints what it is ignoring — skipping is possible, never
|
||||
silent. Regenerate with `tools/exclude_audit.py <old> --write <new>`.
|
||||
|
||||
> **Measured the day after `.run/S71_exclude.txt` was written: 88 of its 107 entries were stale**
|
||||
> — 28 already banked, 14 linked PsyQ symbols that were never targets, and **46 whose blocker had
|
||||
> since been fixed**. Those 46 are **12,750 instructions of open, drawable work**, including
|
||||
> `main:SaveLoadRoutine` (1,165), the largest function left in main. A list that filters them out
|
||||
> costs more than it saves. Current list: **`.run/S72_exclude.txt` (19 entries)**.
|
||||
|
||||
* **The exclude list is not optional.** It carries two populations that no model can bank:
|
||||
* **96 jtbl functions whose carve plan `build_carve` REFUSES** (non-contiguous same-subseg
|
||||
`.rodata`). Cookbook §322. Before the S67 probe fix these all read "carveable".
|
||||
|
||||
+38
-1
@@ -137,12 +137,49 @@ def main():
|
||||
ap.add_argument('--only-main', action='store_true', help='draw ONLY main (the main lane; implies --main)')
|
||||
ap.add_argument('--ledger', default=LEDGER)
|
||||
ap.add_argument('--exclude', default='', help='comma-separated binary:fn to skip')
|
||||
ap.add_argument('--exclude-file', help='file of binary:fn to skip (# comments allowed); '
|
||||
'audited for freshness before the draw')
|
||||
ap.add_argument('--exclude-stale-ok', action='store_true',
|
||||
help='draw anyway against a STALE exclude list (prints what it ignores)')
|
||||
ap.add_argument('--dry', action='store_true')
|
||||
a = ap.parse_args()
|
||||
|
||||
ledger = json.load(open(a.ledger)) if os.path.exists(a.ledger) else {}
|
||||
drawn = {tuple(k.split(':', 1)) for k in ledger}
|
||||
skip = {tuple(x.split(':', 1)) for x in a.exclude.split(',') if ':' in x}
|
||||
# THE EXCLUDE LIST IS A PREREQUISITE, NOT A PARAMETER (P31 S72).
|
||||
# An exclude list records what the TOOLING could not do at the moment it was written, and is
|
||||
# then treated as a property of the FUNCTIONS. Nothing re-examined it, so every tool fix left
|
||||
# behind a population that is now tractable and still marked impossible -- invisible, because
|
||||
# the draw filters it out before anything measures it. Measured the day after `.run/
|
||||
# S71_exclude.txt` was written: 88 of its 107 entries were stale (28 already banked, 14 linked
|
||||
# PsyQ symbols that were never targets, 46 whose blocker had since been fixed) -- including
|
||||
# SaveLoadRoutine, the single largest function left in main.
|
||||
# So the draw AUDITS whatever exclusions it is handed and REFUSES on staleness. Skipping is
|
||||
# still possible (--exclude-stale-ok) but can no longer be silent.
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import exclude_audit as EA
|
||||
ex_rows, ex_bad = EA.parse(text=a.exclude)
|
||||
if a.exclude_file:
|
||||
r2, b2 = EA.parse(path=a.exclude_file)
|
||||
ex_rows += r2; ex_bad += b2
|
||||
if ex_bad:
|
||||
sys.exit(f"draw_waves: {len(ex_bad)} unparseable exclude entr(ies) — refusing rather than "
|
||||
f"under-excluding (R43): {ex_bad[:4]}")
|
||||
if ex_rows:
|
||||
classified, _ = EA.classify(rows=ex_rows)
|
||||
stale = [(b, f, k) for b, f, k, _ in classified if k in EA.STALE]
|
||||
if stale:
|
||||
by = collections.Counter(k for _, _, k in stale)
|
||||
msg = (f"\n*** EXCLUDE LIST IS STALE — {len(stale)} of {len(classified)} entries no "
|
||||
f"longer describe reality ({dict(by)}).\n"
|
||||
f" e.g. " + ", ".join(f"{b}:{f}" for b, f, _ in stale[:6]) + "\n"
|
||||
f" Regenerate: tools/exclude_audit.py <list> --write <new>\n"
|
||||
f" Drawing against it silently filters out work that is now doable.")
|
||||
if not a.exclude_stale_ok:
|
||||
sys.exit(msg + "\n (--exclude-stale-ok to draw anyway)")
|
||||
print(msg + "\n --exclude-stale-ok given: proceeding, and ignoring those entries.")
|
||||
ex_rows = [(b, f) for b, f, k, _ in classified if k not in EA.STALE]
|
||||
skip = set(ex_rows)
|
||||
|
||||
bins = sorted(os.path.basename(p) for p in glob.glob('src/*') if os.path.isdir(p))
|
||||
if a.only_main:
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
"""exclude_audit.py -- classify every entry of a wave exclude list by its CURRENT blocker, and
|
||||
regenerate the list so it never silently becomes "work we have decided not to do".
|
||||
|
||||
THE STANDING RULE THIS ENFORCES. An exclude list records **what the TOOLING could not do at the
|
||||
moment it was written**, and is treated thereafter as a property of the FUNCTIONS. Nothing in the
|
||||
pipeline re-examines it, so every tool improvement leaves behind a population that is now tractable
|
||||
and still marked impossible -- invisible, because the draw filters it out before anything measures
|
||||
it. S71 re-probed 68 such entries and found 17 newly carveable, banked 9, four of them in 57 seconds.
|
||||
|
||||
MEASURED ON `.run/S71_exclude.txt` AT P31 S72 (107 entries, written the day before):
|
||||
|
||||
28 BANKED -- already matched; the entry is dead
|
||||
58 RE-PROBE/LINKED-- the blocker that put them here is GONE, or they were never targets
|
||||
16 CARVE-BLOCKED -- still real: their subseg owns >1 raw jtbl span (split_indicator)
|
||||
5 WALL -- a curated per-function compiler fact
|
||||
|
||||
i.e. **74 of 107 entries were wrong**, one day after the list was written.
|
||||
|
||||
THE CLASSES, and why each is decided the way it is:
|
||||
* `BANKED` -- the `INCLUDE_ASM` stub is gone. The source is the oracle, not any ledger.
|
||||
* `LINKED` -- the function lives in a PsyQ LINKED subseg. It is not a matching target at
|
||||
all and `draw_waves` already refuses those, so the entry is pure noise.
|
||||
* `CARVE-BLOCKED` -- it has a jump table and its subseg still owns raw tables in >= 2 non-adjacent
|
||||
spans (`split_indicator`), so it genuinely cannot bank yet. KEEP.
|
||||
* `RE-PROBE` -- it has a jump table and its subseg is now CLEAN. Whatever refused it has since
|
||||
been fixed. **DROP, loudly** -- this is the population the rule exists for.
|
||||
* `WALL` -- no jump table, so the jtbl machinery is not why it was excluded. A curated
|
||||
per-function compiler fact (§188 epilogue, §332 `la`-in-delay-slot). KEEP,
|
||||
because it cannot be re-derived here -- but it should carry its refutation
|
||||
list so a later idea can be checked against it cheaply.
|
||||
|
||||
tools/exclude_audit.py .run/S71_exclude.txt # report
|
||||
tools/exclude_audit.py .run/S71_exclude.txt --write OUT # regenerate (KEEP classes only)
|
||||
tools/exclude_audit.py .run/S71_exclude.txt --assert-fresh # exit 3 if stale (the draw prereq)
|
||||
"""
|
||||
import argparse, collections, functools, os, re, sys
|
||||
|
||||
print = functools.partial(print, flush=True)
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, os.path.join(REPO, 'tools'))
|
||||
|
||||
KEEP = ('CARVE-BLOCKED', 'WALL')
|
||||
STALE = ('BANKED', 'LINKED', 'RE-PROBE')
|
||||
|
||||
|
||||
def parse(path=None, text=None):
|
||||
"""-> (rows, unparseable). Accepts a FILE or a comma/space-separated string, because
|
||||
`draw_waves --exclude` takes the latter and the freshness prerequisite must cover both."""
|
||||
src = open(path, errors='replace').read() if path else (text or '')
|
||||
rows, bad = [], []
|
||||
# STRIP COMMENTS PER LINE **BEFORE** SPLITTING ON COMMAS, never after. The regenerated list
|
||||
# carries a `# CLASS: reason` note on every entry and prose in its header, and those contain
|
||||
# commas -- splitting first turned `# ...compiler fact, not a tooling limit` into a second
|
||||
# "entry" reading `not a tooling limit`. Caught only because this function REPORTS what it
|
||||
# cannot parse instead of dropping it (R32); a silent parser would have quietly under-excluded.
|
||||
for line in src.splitlines():
|
||||
body = line.split('#', 1)[0]
|
||||
for chunk in body.split(','):
|
||||
s = chunk.strip()
|
||||
if not s:
|
||||
continue
|
||||
if ':' not in s:
|
||||
bad.append(s); continue
|
||||
b, _, f = s.partition(':')
|
||||
rows.append((b.strip(), f.strip()))
|
||||
return rows, bad
|
||||
|
||||
|
||||
def classify(path=None, rows=None):
|
||||
"""-> ([(binary, fn, klass, note)], unparseable). Unparseable lines are reported, not dropped."""
|
||||
import corpus, progress, split_indicator as SI
|
||||
if rows is None:
|
||||
rows, bad = parse(path=path)
|
||||
else:
|
||||
bad = []
|
||||
|
||||
by = collections.defaultdict(list)
|
||||
for b, f in rows:
|
||||
by[b].append(f)
|
||||
|
||||
blocked, linked_of, stubs_of = {}, {}, {}
|
||||
for b in by:
|
||||
try:
|
||||
_st, lines = SI.check(b)
|
||||
blocked[b] = {m.group(1) for l in lines
|
||||
for m in [re.search(r'subseg `([^`]+)`', l)] if m}
|
||||
except Exception:
|
||||
blocked[b] = set()
|
||||
try:
|
||||
progress.set_binary(b); linked_of[b] = set(progress.linked_subsegs())
|
||||
except Exception:
|
||||
linked_of[b] = set()
|
||||
try:
|
||||
stubs_of[b] = {x.symbol: x for x in corpus.stubs(b).values()}
|
||||
except Exception:
|
||||
stubs_of[b] = None
|
||||
|
||||
out = []
|
||||
for b, f in rows:
|
||||
stubs = stubs_of.get(b)
|
||||
if stubs is None:
|
||||
out.append((b, f, 'WALL', 'binary not readable — kept, unverified')); continue
|
||||
st = stubs.get(f)
|
||||
if st is None:
|
||||
out.append((b, f, 'BANKED', 'no INCLUDE_ASM stub in the source')); continue
|
||||
sub = os.path.basename(os.path.dirname(st.asm_path))
|
||||
if sub in linked_of.get(b, ()):
|
||||
out.append((b, f, 'LINKED', f'subseg {sub} is linked PsyQ — never a target')); continue
|
||||
txt = open(st.asm_path, errors='replace').read()
|
||||
# §401: a jtbl function jumps through a register that is NOT $ra; `jr $ra` ends every function
|
||||
if re.search(r'\bjr\s+\$(?!ra\b)\w+', txt):
|
||||
if sub in blocked.get(b, ()):
|
||||
out.append((b, f, 'CARVE-BLOCKED', f'{sub} owns >1 raw jtbl span — needs a split'))
|
||||
else:
|
||||
out.append((b, f, 'RE-PROBE', f'{sub} is clean now — the blocker is gone'))
|
||||
else:
|
||||
out.append((b, f, 'WALL', 'no jump table — curated compiler fact, not a tooling limit'))
|
||||
return out, bad
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('path')
|
||||
ap.add_argument('--write', metavar='OUT', help='regenerate, keeping only still-valid entries')
|
||||
ap.add_argument('--assert-fresh', action='store_true',
|
||||
help='exit 3 if any entry is stale (the draw-time prerequisite)')
|
||||
a = ap.parse_args()
|
||||
os.chdir(REPO)
|
||||
rows, bad = classify(a.path)
|
||||
cnt = collections.Counter(k for _, _, k, _ in rows)
|
||||
print(f"{a.path}: {len(rows)} entries")
|
||||
for k, v in cnt.most_common():
|
||||
print(f" {v:4d} {k}{' (STALE)' if k in STALE else ''}")
|
||||
if bad:
|
||||
print(f" !! {len(bad)} unparseable line(s), kept out of the count: {bad[:3]}")
|
||||
stale = [r for r in rows if r[2] in STALE]
|
||||
for k in STALE:
|
||||
ex = [f"{b}:{f}" for b, f, kk, _ in rows if kk == k]
|
||||
if ex:
|
||||
print(f"\n {k} ({len(ex)}): " + ", ".join(ex[:8]) + (" …" if len(ex) > 8 else ""))
|
||||
|
||||
if a.write:
|
||||
keep = [r for r in rows if r[2] in KEEP]
|
||||
with open(a.write, 'w') as fh:
|
||||
fh.write(f"# regenerated by tools/exclude_audit.py from {a.path}\n"
|
||||
f"# {len(keep)} still-valid of {len(rows)}; "
|
||||
f"{len(stale)} dropped as stale (banked / linked / blocker-since-fixed).\n"
|
||||
f"# An exclude list records what the TOOLING could not do — regenerate it as\n"
|
||||
f"# part of every tool fix, or it becomes a list of work you decided not to do.\n")
|
||||
for b, f, k, note in keep:
|
||||
fh.write(f"{b}:{f} # {k}: {note}\n")
|
||||
print(f"\nwrote {a.write}: {len(keep)} kept, {len(stale)} dropped")
|
||||
|
||||
if a.assert_fresh and stale:
|
||||
print(f"\n*** STALE — {len(stale)} of {len(rows)} entries no longer describe reality. "
|
||||
f"Regenerate with:\n tools/exclude_audit.py {a.path} --write <new>\n"
|
||||
f"Drawing against this list would silently filter out work that is now doable.")
|
||||
return 3
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user