phase-36: T6 CLOSE — both yield lines measured (permuter 5 of 16 exemplars / 665 of 2,131 bodies; recipes 134 of 134 in 6.0 min), 664 sites gone (34,091 -> 33,427), R22 218/218 at every step; the S99 checkpoint written for T7 (which starts only on Drew's direct approval) with the parallelised rung-R sweep as the drawable work meanwhile

This commit is contained in:
Drew T
2026-09-09 13:02:51 -06:00
parent 1c23550540
commit aef1159488
14 changed files with 1186 additions and 46 deletions
@@ -2,7 +2,7 @@
> **Generated by `tools/cookbook_index.py` — do not hand-edit** (R33). Regenerate after adding a cookbook section.
>
> `docs/matching-cookbook.md` is ~716 KB / 1170 sections. Grepping it blind is how three P30 wave-1 agents each "discovered" an idiom that was already written down. **Start here, then read the section.** A section appears under every symptom it addresses.
> `docs/matching-cookbook.md` is ~716 KB / 1171 sections. Grepping it blind is how three P30 wave-1 agents each "discovered" an idiom that was already written down. **Start here, then read the section.** A section appears under every symptom it addresses.
**How to use:** name what you SEE in the diff (a stolen delay slot, an extra `la`, a swapped register pair, a `conflicting types` error), find that symptom below, read those sections first. If nothing fits, THEN grind — and add a section when you win.
@@ -1461,7 +1461,7 @@
- **§479** — ★★★ — WHERE THE PERMUTER ACTUALLY PAYS: A MEASURED YIELD CURVE (P31 S77, 8 candidates) <sub>L35811</sub>
- **§501** — ★★★ — A LEVER THAT MEASURES WORSE MAY BE A CASCADE: READ THE `.loop` DUMP FOR THE DESIRABILITY FLIP BEFORE DISCARDING IT (P32 T4b, `main:func_800391D4`, a pinned wall banked by a Fable agent) <sub>L37151</sub>
### (unbucketed — title matched no symptom vocabulary) (337)
### (unbucketed — title matched no symptom vocabulary) (338)
- **§3-How** — to use this <sub>L30</sub>
- **§1** — Idiom catalog (asm pattern → C that produces it) <sub>L39</sub>
@@ -1800,6 +1800,7 @@
- **§452** — ★★★ — NOT EVERY VERBATIM BODY IS UNDECOMPILED WORK, AND §448'S HEADLINE OVERSTATED IT (P31 S75; 10-function burst, 0 banks, and the negative result is the finding) <sub>L35104</sub>
- **§462** — FOUR LEVERS FROM `main:func_80024054` (91 ins, 74/53/32 → 4) <sub>L35644</sub>
- **§480** — 🔴 — A STATIC BLOCKER CLASS THAT THE REAL PIPELINE ALREADY REMOVES IS A PHANTOM <sub>L35890</sub>
- **§454** — Rung D and rung R: taking a lever off a body that still has to compile to the same bytes (Phase 36 T6) <sub>L37604</sub>
## All sections, in order
@@ -2974,6 +2975,7 @@
- **§500** — ★★★ — THE T3 WAVE HARVEST (P32 T3, 2026-09-05): 31 one-agent-per-function drafters → 20 MATCH / 9 NEAR / 2 FAIL, every closer, two NEW named gcc mechanisms, and the wave-process defects <sub>L36793</sub>
- **§501** — ★★★ — A LEVER THAT MEASURES WORSE MAY BE A CASCADE: READ THE `.loop` DUMP FOR THE DESIRABILITY FLIP BEFORE DISCARDING IT (P32 T4b, `main:func_800391D4`, a pinned wall banked by a Fable agent) <sub>L37151</sub>
- **§453** — ★★★ — ONE SOURCE PER UNIQUE FUNCTION: THE INCLUDE-AT-SITE SHARE, ITS GATE, AND THE FOUR DEFECTS THAT SHAPED IT (P35 S94–S96) <sub>L37555</sub>
- **§454** — Rung D and rung R: taking a lever off a body that still has to compile to the same bytes (Phase 36 T6) <sub>L37604</sub>
---
@@ -4156,3 +4158,4 @@ Notes routinely quote that as a section id. This table resolves it. Grep bait: `
| L36793 | §500 | ★★★ — THE T3 WAVE HARVEST (P32 T3, 2026-09-05): 31 one-agent-per-function drafters → 20 MA |
| L37151 | §501 | ★★★ — A LEVER THAT MEASURES WORSE MAY BE A CASCADE: READ THE `.loop` DUMP FOR THE DESIRABI |
| L37555 | §453 | ★★★ — ONE SOURCE PER UNIQUE FUNCTION: THE INCLUDE-AT-SITE SHARE, ITS GATE, AND THE FOUR DE |
| L37604 | §454 | Rung D and rung R: taking a lever off a body that still has to compile to the same bytes ( |
@@ -37600,3 +37600,70 @@ green. R34 in one sentence: build the oracle that can disagree, then measure the
the classifier became includes (ov_SC03_015's 219 single-site macros; the never-extended members of five under-listed groups). REAL
360,744 → 350,533 with EMPTY up the same: empty-bodied shared functions were folded into REAL under the macro form. Instruction-weighted
metrics unchanged. A number generated from a form the tooling cannot see is a number, not a count.
## §454 — Rung D and rung R: taking a lever off a body that still has to compile to the same bytes (Phase 36 T6)
The residue after the mechanical de-lever campaign (rungs A–C) is 12,970 bodies in 1,804 text classes, each with at least one site the
oracle called NEEDED. Two instruments work on it: **rung D**, decomp-permuter seeded with the LEVER-FREE body, and **rung R**, the
cookbook's byte-neutral shape recipes applied mechanically. What follows is what S99 measured, including the part where the measurement
was wrong.
**A DISASSEMBLY LISTING IS NOT A TARGET.** Two campaigns returned "0 of 16 exemplars" with a straight face. Both were the harness. The
permuter's `target.o` had been assembled from `verbatim_target_s.py --gas`, a listing regenerated from the ROM image — and assembling a
disassembly is a second toolchain with its own answers:
* objdump prints the PSEUDO-instruction `move rX,rY` for `addu rX,rY,$zero` (0x…21). GAS assembles `move` as `or` (0x…25). In one
234-instruction function that is **24 wrong words**, and nothing said so.
* a listing's `%hi`/`%lo` pairs come back RESOLVED — `lui $a1,0x801f` / `addiu $a1,$a1,-11600`, no relocation — while every compiled
candidate carries `R_MIPS_HI16`/`LO16` against a symbol, and the masked scorer compares reloc operands.
Result: the scorer reported **28 for a body that is byte-identical**, so score 0 was unreachable and every NO-MATCH was its own.
The control that names this in one line: **seed the search with the body the tree already matches and read the base score.** It must be
0. (`delever_permute.py --positive-control`.) Run it before believing any campaign's yield — a 0-yield run cannot otherwise be told from
a broken scorer (R40).
The fix is to stop assembling a listing: **the target is the tree's own body compiled by the build's own tail** into a one-function
object, so it carries the candidates' relocations by construction. The ROM listing stays the INDEPENDENT oracle that proves it —
`match_one` must call that body a MATCH before the search starts — which is what keeps the arrangement from being circular (R34/R56).
`verbatim_target_s.py --gas` now verifies itself too (assemble → disassemble → compare word by word → `.word`-patch or REFUSE); it is
the file pasted into decomp.me, and it was wrong for every function containing a `move`.
**THE PROFILE FOLLOWS THE REGISTER, NOT THE SITE KIND.** A needed pin on a CALLEE-SAVED register ($16–$23) is an allocation-order
residual and wants the regalloc profile. A pin on a CALLER-SAVED one ($2/$3/$4–$7) is not. `func_80163EC8` pins `$2`, and its residual
is `lw v1,68(s1); li v0,-33; and v0,v1,v0` against `lw v0,68(s1); li v1,-33; and v0,v0,v1` — the operand ORDER of one `&`. The regalloc
profile weights `perm_commutative` 2.0; the cse profile weights it 40.0. Steering by "it was a pin, so regalloc" spends the whole budget
20× away from the only lever that closes it.
**WHAT RUNG D ACTUALLY FINDS, AND WHY RUNG R THEN GETS IT FOR FREE.** Every win is a small, nameable source shape:
| function | start (mismatched ins) | time | the whole semantic change |
|---|---|---|---|
| `func_80135D20` | 12 | 24 s | `flag = 0;` → `do { flag = 0; } while (0);` (`perm_ins_block`, RC-5 scope) |
| `func_80163EC8` | 8 | 314 s | `uVar5 = *(s32 *)(p+0x44); … uVar5 & ~0x20` → the temp inlined at its use (`perm_expand_expr`, §501-R's S2 kill) |
The permuter's winner is machine-reprinted by pycparser (extra parens, `;` statements, its own brace style), so banking it verbatim
trades a lever for a readability regression — which is exactly what this phase exists to prevent. Each win is therefore turned into a
mechanical recipe that produces the SAME bytes from a one-line diff in the real source. Rung R's generators, in trial order (targeted
first, blanket last), each one compile:
* **R2** the formerly-pinned declarations permuted among their own lines · **R4** one of them moved through the whole declaration run
(§76/§501-R: the allocation order is the bank) · **R3** an initializer split off its declaration, the assignment placed after the
WHOLE run (C89 forbids a declaration after a statement — placing it after the last PINNED declaration compiles as K&R parameters and
fails in a way that reads like a compiler bug)
* **R5** the operand order of one commutative operator (the caller-saved lever above; needs no pinned declaration)
* **R6** a local assigned once and read once, inlined at its use with its dead declaration removed
* **R7** one statement wrapped in a block — `{ stmt; }` tried before `do { stmt; } while (0);`, because the readable spelling should win
when both hold
**THE CLASS, NOT THE BODY.** A win on an exemplar is worth its whole text class (the head of the draw is 80 classes of 134 copies), but a
ledger REPLAY cannot deliver it: the ledger replays a SITE SET, and a reshaped body is not one. `delever.py --propagate TU FN` does it by
address remap — the two old bodies are identical modulo `func_`/`D_` tokens (that IS the class hash), so their tokens correspond one for
one and the map they define is applied to the new body, each sibling judged on its own objects. `--apply-body` records the before/after
body text in its ledger row because after the write it exists nowhere else.
**THE SHAPE OF THE POPULATION (first 6 of 16 exemplars, all ov_SC04_011, 134-copy classes).** Two closed, four did not, and the split is
the starting distance, not the site count: the closers started 8 and 12 mismatched instructions from the target; the four that did not
started at 70, 99, 105 and 131 — every one a body where removing a hand-placed `instruction` lever changed the instruction COUNT and
shifted everything after it (`mine=103 ins, target=106`). They still improved a long way (131→35, 105→18, 99→37, 70→24), so they are
seeds for a longer run or for T7's agents, not walls. `--max-start N` triages by that number instead of spending a search on it.
@@ -916,3 +916,33 @@ each a different action. (3) **A known-true control per join:** the registry rep
multi-batch tool's hardest bug (stale line numbers + a restore that wipes the previous batch) into a non-event; the driver commits
between batches. (5) **The census's per-instance forms are the registry check's C2d for free** (one cached scan, 36 s) — derive, don't
re-parse (R33).
## P36 S99 (2026-09-09) — the search harness whose target was a disassembly, and the three cheap checks that would have caught it
(1) **Before believing any search yield, seed the search with a body that already MATCHES and read the base score. It must be 0.**
Two rung-D campaigns returned "0 of 16 exemplars" and both were the instrument: the permuter's `target.o` had been assembled from a
regenerated disassembly listing, and assembling a disassembly is a second toolchain — objdump prints the pseudo-instruction `move` for
`addu rX,rY,$zero` and GAS assembles `move` as `or` (24 wrong words in one 234-instruction function), while a listing's `%hi`/`%lo`
pairs come back resolved, with no relocation, against candidates that all carry one. The scorer read 28 for a byte-identical body, so
score 0 was unreachable. The control is one command and ~30 seconds (`delever_permute.py --positive-control`); without it, "the residue
is hard" and "the scorer is broken" are the same observation. R40, and the first place this project has needed it against a SCORER
rather than a model.
(2) **The target of a byte search should be produced by the same toolchain as the candidates, with an independent oracle to prove it
— not by a second toolchain that has to be argued with.** The fix was to compile the tree's own body into a one-function object (the
candidates' relocations by construction) and require `match_one` against the ROM listing to call it a MATCH before the search starts.
Faster to build than the listing-fixing it replaced, and it cannot drift.
(3) **A generated artifact that nothing ever consumed is a claim.** `verbatim_target_s.py --gas` had been proven once, through
decomp.me, on a function with no `move` in it — so it was wrong for a large fraction of the fleet and nobody knew (R98/DK-81 in a second
place). It now assembles, disassembles and compares itself word by word against the image, `.word`-patches what does not reproduce, and
REFUSES what still disagrees.
(4) **Steer a directed search by what the residual IS, not by what produced it.** The weight profile was chosen from the site kind
("a pin → regalloc"); a pin on a CALLER-saved register ($2/$3/$4–$7) is an operand-order residual, and the regalloc profile weights
`perm_commutative` 2.0 where the cse profile weights it 40.0. One reading of one diff moved 8 of 16 exemplars to the right profile.
(5) **Turn each search win into a mechanical recipe the same day.** Rung D's two wins were a statement wrapped in a block and a
single-set temp inlined at its use — both one-line source changes that a recipe generator reproduces in ONE compile, and that keep the
source readable, where banking the permuter's own pycparser-reprinted body would trade a lever for a readability regression in a phase
whose whole purpose is readability.
+4 -3
View File
@@ -8,8 +8,8 @@
> its platform SDK need the marked adaptation. The last table lists the tools that are project-only in code (their *shape* is a task;
> their code does not transfer). *TODO(platform): the MIPS and PlayStation SDK hard-codes are the ones another platform replaces first.*
>
> **Coverage:** 301 tool files in scope (submodules, vendored and downloaded code excluded), of which 301 live rows
> below; per phase: P1 2 · P2 26 · P3 17 · P4 9 · P5 26 · P6 51 · P7 20 · P8 86 · P9 27 · P10 21 · PROJECT-ONLY 16. Superseded tools appear only as pointers to their successor (30 pointer rows); one-offs are omitted. Table rows in all: 331 (the installer checks its copy against this figure).
> **Coverage:** 302 tool files in scope (submodules, vendored and downloaded code excluded), of which 302 live rows
> below; per phase: P1 2 · P2 26 · P3 17 · P4 9 · P5 26 · P6 51 · P7 20 · P8 86 · P9 27 · P10 22 · PROJECT-ONLY 16. Superseded tools appear only as pointers to their successor (30 pointer rows); one-offs are omitted. Table rows in all: 332 (the installer checks its copy against this figure).
## P1 — extraction + manifest
@@ -335,6 +335,7 @@
| `uniquify_type.py` | give each conflicting camp of a same-named type its own name | Gives each conflicting camp of a same-named type its own name so every camp becomes liftable | repo src layout |
| `verbatim_check.py` | guard that every inline-assembly body still reproduces its target bytes | Regression guard that every inline-assembly body still reproduces its target bytes | repo src layout |
| `delever_oracle.py` | judge one translation-unit edit by the bytes of its object in under a second | The Phase-36 fast byte oracle: every object's exact build command captured once via make -n -W (the Makefile's own recipe, pad stage and -O0 overrides included), a candidate compiled in place with -o/-MF redirected to scratch and compared with the fleet run's object; --calibrate proves 100 % equality untouched, twin == primary and a positive control before any verdict is trusted | the Makefile's object rules, the twin rule |
| `lever_progress.py` | keep the lever count as a series a chart and a story can be drawn from | The Phase-36 lever series: a milestone row appended per census snapshot (its totals by class with the tree's HEAD, since a census is a moment that cannot be recovered later) plus the campaign table derived from the de-lever ledger on every render — per batch the rungs used, the bodies first judged, the sites removed and rewritten, and, scored as a transition against what that body's previous row left, the sites a later rung closed and the bodies it made lever-free; renders both into docs/levers.md's generated block and --check refuses a series that is not this tree's | the census json, the delever ledger |
| `share_census.py` | measure duplicate function bodies across the fleet and assert one source per unique function | The census of byte-identical function classes across every binary with their source forms and verdicts, plus the S1 invariant check with its exception ledger and a fixture self-test | repo paths, the registry and signature schemas |
| `ghidra_apply_symbols.sh` | mirror the curated symbol file into the analysis database with a real save | Mirrors the curated symbol file into the analysis program headlessly, with a real save | repo symbol path, project name |
| `frozen.py` | refuse, from one place, the command line of a tool the project has frozen | The one refusal a FROZEN tool prints from its main(): the tool, the successor and why; imports never exit (libraries stay usable) | nothing |
@@ -343,7 +344,7 @@
| `share_body_cycle.sh` | run the share-body batch cycle unattended: batch, verify by exit code, log, commit, periodic clean fleet check | The Phase-35 T5 driver for bucket new: per batch share_body --apply --bucket new --batches 1 --label new<k>, the gated N/N line read, the phase log entry appended, the bank committed the moment it is green, the clean fleet run every N batches — stops on the first red | repo paths, the phase-log format, the fleet count from config/check.*.sha |
| `delever_permute.py` | search the compiler's own shape space for a lever-free body that keeps the bytes | The Phase-36 rung D: one exemplar per residue text class from the delever ledger (largest class first — a match banks every copy), each prepared as a single-function translation unit (delever's rung-A rewrite of every removable site, every other definition reduced to a prototype, shared-header includes to their prototypes, INCLUDE_ASM and file-scope asm dropped, the build's own CPPFLAGS through cpp -P) against a target regenerated from the ROM image in both the assemblable and the splat form; every attempt calibrates itself first (the LEVERED body must be MATCH, or the harness is not measuring that function) and records the lever-free body's starting distance; decomp-permuter through permuter_ils with the weight profile chosen from the NEEDED kinds; a score-0 winner is banked only through delever --apply-body and the GTE re-fold; scratch, winners and the outcome ledger keyed by alias+fn | the ledger, the permuter harness, the target regenerator |
| `share_body.py` | share one byte-identical function class across its binaries through an include-at-site header, gated per binary | The Phase-35 successor of dedup_propagate + dedup_extend for the include-at-site form: exemplar by majority text, the header written once, every private copy replaced by the include at its position, per-binary byte gate with object comparison, bisect on red, the registry appended or extended by text, the exception ledger on refusal; --plan / --apply --bucket extend|new | repo paths, the registry and signature schemas |
| `delever.py` | take register pins, asm statements, volatile and register levers out of matched C, byte-gated per site | The Phase-36 de-lever engine and campaign tool: positional rewrites per lever class (pin -> plain declaration, barrier/keep-alive deleted, a launder deleted or turned into the assignment it is, a hand-placed instruction -> its C, the zero-register variable -> 0, a macro-carried site deleted/valued/refused by its macro's shape, volatile/register dropped); per body replay -> rung A strip-all -> rung B greedy through delever_oracle; the file is the write unit and its final compile through every recipe the proof; --plan/--apply batches (TUs parallel, exemplar files first; headers serial, includers parallel), the ledger keyed by tu+fn+addr with the body's nhash before/after, !FAKE markers on class A/B survivors, --redraw for bodies refused by an older tool, --restore from inflight.json, --scrub for orphan markers, --apply-body for T6/T7, --selftest with a stub oracle, --probe (T2) | the census site records, the oracle |
| `delever.py` | take register pins, asm statements, volatile and register levers out of matched C, byte-gated per site | The Phase-36 de-lever engine and campaign tool: positional rewrites per lever class (pin -> plain declaration, barrier/keep-alive deleted, a launder deleted or turned into the assignment it is, a hand-placed instruction -> its C, the zero-register variable -> 0, a macro-carried site deleted/valued/refused by its macro's shape, volatile/register dropped); per body replay -> rung A strip-all -> rung B greedy through delever_oracle; the file is the write unit and its final compile through every recipe the proof; --plan/--apply batches (TUs parallel, exemplar files first; headers serial, includers parallel), the ledger keyed by tu+fn+addr with the body's nhash before/after, !FAKE markers on class A/B survivors, --redraw for bodies refused by an older tool, --restore from inflight.json, --scrub for orphan markers, --apply-body for T6/T7, --recipes (rung R: the byte-neutral shape recipes — the formerly-pinned declarations permuted, one moved through the declaration run, an initializer split — each judged by the oracle, with an identity control on both the splice and the oracle), --selftest with a stub oracle, --probe (T2) | the census site records, the oracle |
| `verbatim_to_stub.py` | turn an inline-assembly body back into a stub the toolchain can reach | Turns an inline-assembly body back into an include-assembly stub | repo src/asm layout |
## PROJECT-ONLY — project-only in code (the shape is a task; the code does not transfer)
+566 -1
View File
@@ -1254,6 +1254,512 @@ def scrub(a):
return 0 if not bad else 1
# ----------------------------------------------------------------------------------------------------------------------
# --recipes (rung R): the cookbook's byte-neutral shape recipes, tried mechanically on a RESIDUE body
# ----------------------------------------------------------------------------------------------------------------------
CTRL_KW = re.compile(r"^\s*(?:if|for|while|do|switch|else|return|goto|case|default|break|continue)\b")
def pin_names(sites):
"""the variable each pin site declares, in source order (the zero-register pins excluded: their declaration is deleted)."""
out = []
for s in sites:
if s["kind"] != "pin" or s.get("zero"):
continue
mm = re.search(r"\b([A-Za-z_]\w*)\s*(?:\[[^\]]*\])?\s*(?:__asm__|__asm|asm)\s*\(", s.get("text", ""))
if mm:
out.append(mm.group(1))
return out
def is_decl_line(masked_line):
"""a whole-statement DECLARATION on one line: `<type> <name>[\\[n\\]][ = init];`. The type and the name must be separated
(by space or `*`) — without that, `ret = f();` parses as the declaration `re t = …` and an initializer split lands its
assignment after the first statement, where C89 forbids the declarations that follow it."""
s = masked_line.strip()
return bool(s.endswith(";") and not CTRL_KW.match(s) and "(" not in s.split("=")[0]
and re.match(r"^[A-Za-z_][\w \t]*[\s*]\s*\*?\s*[A-Za-z_]\w*\s*(?:\[[^\]]*\])*\s*(?:=|;)", s))
def decl_run_end(text, d):
"""the 0-based index of the LAST line of the declaration run that opens fn's body — a C89 declaration may not follow a
statement, so an initializer split must put its assignment after the WHOLE run, not after the last pinned declaration."""
lines = text.split("\n")
last = d["line"] - 1
for i in range(d["line"], d["end"] - 1):
s = sc.mask_text(lines[i]).strip()
if not s or s.startswith("/*") or s.startswith("//") or s in ("{", "}"):
continue
if is_decl_line(sc.mask_text(lines[i])):
last = i
continue
break
return last
def decl_lines(text, tu, fn, names):
"""[(line index, text)] for the lines of fn's body that DECLARE one of `names` — one name per line, the line a whole
statement. The lever-free text is re-scanned for them (a stripped pin can delete its line, so the census's numbers have
moved); a name whose declaration is not found alone on one line makes the body ineligible (None)."""
recs = sc.scan_text(text, tu, shared_defs=None)
d = next((r for r in recs if r["form"] == "def" and r["name"] == fn), None)
if d is None:
return None
lines = text.split("\n")
found = {}
for i in range(d["line"], d["end"] - 1): # inside the body, never the header line
raw_line = lines[i]
s = sc.mask_text(raw_line).strip()
if not is_decl_line(s):
continue
hits = [n for n in names if re.search(r"(?<![\w])%s\b" % re.escape(n), s.split("=")[0])]
if len(hits) != 1 or hits[0] in found:
continue
found[hits[0]] = (i, raw_line)
if len(found) != len(set(names)):
return None
return sorted(found.values())
COMMUTATIVE = "&|^+*"
def top_level_ops(expr):
"""positions of the BINARY commutative operators at paren/bracket depth 0 in `expr` (masked text). An operator is binary
when the previous non-space character ends an operand (identifier, digit, `)`, `]`); that also excludes a unary `*`/`&`
and a cast's `*`, which in any case sits inside parentheses."""
out, depth = [], 0
for i, c in enumerate(expr):
if c in "([":
depth += 1
elif c in ")]":
depth -= 1
elif depth == 0 and c in COMMUTATIVE:
if i + 1 < len(expr) and expr[i + 1] in "&|=+*": # && || &= += *= **
continue
if i and expr[i - 1] in "&|=+*<>!-/%":
continue
prev = expr[:i].rstrip()
if prev and (prev[-1].isalnum() or prev[-1] in "_)]"):
out.append(i)
return out
def commutative_swaps(text, tu, fn, d_):
"""[(description, candidate text)] — each line of fn's body that carries exactly ONE top-level commutative operator,
with its two operands swapped. THE lever for a caller-saved ($2/$3) residual: S99 read `and v0,v1,v0` against the
target's `and v0,v0,v1` on func_80163EC8, which is the operand order of one `&` in the source and nothing else."""
lines = text.split("\n")
out = []
for i in range(d_["line"], d_["end"] - 1):
raw_line, s = lines[i], sc.mask_text(lines[i])
body = s.strip()
if not body.endswith(";") or body.startswith("#"):
continue
# only an assignment's RHS or a `return` expression — an `if (…) stmt;` line would need the condition parsed out of
# the statement after it, and a wrong split is a candidate that cannot compile (wasted, and noisy in the ledger)
asg = re.search(r"(?<![=!<>+\-*/%&|^~])=(?!=)", s)
ret = re.match(r"^\s*return\b", s)
start = asg.end() if asg else (ret.end() if ret else -1)
if start < 0 or start >= len(s):
continue
expr_end = s.rstrip().rfind(";")
expr = s[start:expr_end]
ops = top_level_ops(expr)
if len(ops) != 1:
continue
o = start + ops[0]
left, right = raw_line[start:o], raw_line[o + 1:expr_end]
if not left.strip() or not right.strip():
continue
cand = list(lines)
cand[i] = raw_line[:start] + " " + right.strip() + " " + raw_line[o] + " " + left.strip() + raw_line[expr_end:]
out.append((f"swap {raw_line[o]} @{i + 1}", "\n".join(cand)))
return out
IDENT = re.compile(r"(?<![\w.])([A-Za-z_]\w*)(?![\w])")
def inline_single_set_temps(text, tu, fn, d_):
"""[(description, candidate text)] — a local assigned ONCE and read ONCE, inlined at its use and its now-dead
declaration removed. §501-R's S2 kill: a fresh single-set local gets a birthing boost in gcc 2.7.2's allocator, so
creating or removing one moves the allocation — and this is the move rung D found first (S99, func_80163EC8:
`uVar5 = *(s32 *)(psVar6 + 0x44); … = uVar5 & ~0x20;` became `… = *(s32 *)(psVar6 + 0x44) & ~0x20;`). Doing it here
keeps the SOURCE readable: the permuter's own winner is machine-reprinted, and this phase is about readability."""
lines = text.split("\n")
lo, hi = d_["line"], d_["end"] - 1
masked = [sc.mask_text(l) for l in lines]
occ = collections.defaultdict(list)
for i in range(lo, hi):
for m in IDENT.finditer(masked[i]):
occ[m.group(1)].append((i, m.start(), m.end()))
out = []
ASG = r"^(?:[A-Za-z_][\w \t]*[\s*]\s*\*?\s*)?%s\s*=(?!=)\s*(.+);\s*$"
for v, places in occ.items():
asgs, decls, uses = [], [], []
for p in places:
s = masked[p[0]].strip()
if re.match(ASG % re.escape(v), s):
asgs.append(p)
elif is_decl_line(s) and "=" not in s.split(";")[0]:
decls.append(p)
else:
uses.append(p)
if not asgs or not uses or len(decls) > 1:
continue
# ONE ASSIGNMENT AT A TIME, not one per variable. The single-set case is the easy half; the lever rung D actually
# found is narrower: `uVar5` is assigned in TWO branches of func_80163EC8, and the winning move inlined ONE of them.
# An assignment is inlinable when its value is read exactly once before the variable is written again — the classic
# def-with-one-use — so the assignment can go and the read can carry the expression.
order = sorted(places)
for ai, _, _ in asgs:
after = [p for p in order if p[0] > ai]
reads = [p for p in after if p not in [(x, y, z) for x, y, z in asgs] and not is_decl_line(masked[p[0]].strip())]
if not reads:
continue
ui, u0, u1 = reads[0]
nxt = [p for p in after if p[0] > ui]
if nxt and (nxt[0][0], nxt[0][1], nxt[0][2]) not in [(x, y, z) for x, y, z in asgs]:
continue # read again before it is rewritten: the assignment is not dead
mm = re.match(ASG % re.escape(v), masked[ai].strip())
if not mm:
continue
expr = lines[ai].strip()[mm.start(1):mm.end(1)]
if not expr.strip():
continue
cand = list(lines)
cand[ui] = lines[ui][:u0] + f"({expr})" + lines[ui][u1:]
cand[ai] = None # the assignment goes (with its declaration when they are one)
if decls and len(asgs) == 1: # the separate `T v;` is dead only when nothing else writes v
cand[decls[0][0]] = None
out.append((f"inline {v} @{ai + 1}", "\n".join(l for l in cand if l is not None)))
return out
def block_wraps(text, tu, fn, d_):
"""[(description, candidate text)] — one statement wrapped in a block. §501-R's RC-5 scope lever: a block changes the
statement's basic-block structure and with it the allocno live range, which is how rung D closed func_80135D20 in 24 s
(`flag = 0;` -> `do { flag = 0; } while (0);` and nothing else). The plain block is tried FIRST because it is the
readable spelling; the do-while is gcc's stronger form and is only reached when the plain one does not hold."""
lines = text.split("\n")
out = []
for i in range(d_["line"], d_["end"] - 1):
s = sc.mask_text(lines[i]).strip()
if not s.endswith(";") or CTRL_KW.match(s) or is_decl_line(s) or s.startswith("#") or "{" in s or "}" in s:
continue
raw_line = lines[i]
indent = raw_line[:len(raw_line) - len(raw_line.lstrip())]
stmt = raw_line.strip()
for tag, spelling in (("block", f"{indent}{{ {stmt} }}"), ("do-while", f"{indent}do {{ {stmt} }} while (0);")):
cand = list(lines)
cand[i] = spelling
out.append((f"{tag} @{i + 1}", "\n".join(cand)))
return out
def recipe_candidates(text, tu, fn, names, limit=24, rng=None, cap=40, blocks=True):
"""[(recipe, description, candidate text)] — the byte-neutral shape recipes of the cookbook, mechanically.
R2 (§76/§501-R, the allocation ORDER is the bank): the formerly-pinned declarations permuted among their own lines.
R4: one of them moved through the whole declaration run. R3 (§17a/§501-P): an initializer split off its declaration.
R5 (§137/§501-R, the caller-saved class): the operand order of one commutative operator — the ONLY recipe here that
needs no pinned declaration, so a residue of barriers and launders still has candidates."""
recs_ = sc.scan_text(text, tu, shared_defs=None)
d_ = next((r for r in recs_ if r["form"] == "def" and r["name"] == fn), None)
if d_ is None:
return []
dls = decl_lines(text, tu, fn, names) if names else None
lines = text.split("\n")
out = []
rng = rng or random.Random(0)
idx = [i for i, _ in dls] if dls else []
body = [t for _, t in dls] if dls else []
perms = []
if not dls:
perms = []
elif len(body) <= 4:
import itertools
perms = [p for p in itertools.permutations(range(len(body)))][1:] # the identity is the current text
else:
seen = set()
while len(perms) < limit and len(seen) < limit * 4:
p = tuple(rng.sample(range(len(body)), len(body)))
seen.add(p)
if p != tuple(range(len(body))) and p not in perms:
perms.append(p)
for p in perms[:limit]:
ls_ = list(lines)
for slot, src in zip(idx, p):
ls_[slot] = body[src]
out.append(("R2", "decl-order " + ",".join(str(x) for x in p), "\n".join(ls_)))
# R4 (§76/§501-R again, one variable at a time): a formerly-pinned declaration moved to every other slot of the body's
# declaration run — the allocno creation order the pin used to override. R2 permutes the pinned declarations among
# THEIR OWN slots; this reaches the orders that involve the untouched declarations too, at one compile each.
last = decl_run_end(text, d_)
run_idx = [i for i in range(d_["line"], last + 1) if is_decl_line(sc.mask_text(lines[i]))]
if dls and len(run_idx) > 1:
run_txt = [lines[i] for i in run_idx]
for i, t in dls:
if i not in run_idx:
continue
src = run_idx.index(i)
name = next((n for n in names if re.search(r"(?<![\w])%s\b" % re.escape(n), t.split("=")[0])), "?")
for dst in range(len(run_txt)):
if dst == src:
continue
seq = list(run_txt)
seq.insert(dst, seq.pop(src))
ls_ = list(lines)
for slot, txt in zip(run_idx, seq):
ls_[slot] = txt
out.append(("R4", f"decl-move {name} {src}->{dst}", "\n".join(ls_)))
for k, (i, t) in enumerate(dls or []):
head, _, init = t.partition("=")
if not init.strip().endswith(";") or "==" in t:
continue
name = [n for n in names if re.search(r"(?<![\w])%s\b" % re.escape(n), head)]
if len(name) != 1:
continue
ls_ = list(lines)
ls_[i] = head.rstrip() + ";"
indent = t[:len(t) - len(t.lstrip())]
ls_.insert(last + 1, f"{indent}{name[0]} ={init.rstrip()}")
out.append(("R3", f"init-split {name[0]}", "\n".join(ls_)))
for desc, cand in commutative_swaps(text, tu, fn, d_):
out.append(("R5", desc, cand))
for desc, cand in inline_single_set_temps(text, tu, fn, d_):
out.append(("R6", desc, cand))
if blocks: # last: one candidate per statement, so the targeted recipes go first
for desc, cand in block_wraps(text, tu, fn, d_):
out.append(("R7", desc, cand))
seen, uniq = {text}, [] # never judge the seed twice, nor one candidate twice (R37)
for rec, desc, cand in out:
if cand in seen:
continue
seen.add(cand)
uniq.append((rec, desc, cand))
return uniq[:cap]
def recipes(a):
"""--recipes: rung R over the RESIDUE bodies. The seed is the body's LEVER-FREE text; the first candidate whose object is
IDENTICAL replaces it and its markers are scrubbed with its levers. Scope, stated (R41): only a body with at least one
formerly-PINNED declaration has candidates — the recipes are declaration-order and initializer-split levers — so a residue
of barriers/launders/keep-alives alone is not drawable here and stays for rung D and T7. A hit banks the EXEMPLAR body;
its copies are a separate step (their text must be remapped, not replayed: the ledger replays a SITE SET, and a reshaped
body is not one)."""
ensure_census(a.jobs)
clean, dirty = src_clean()
if not clean and not a.dirty_ok:
sys.exit(f"delever --recipes: src/ is dirty (commit or --restore first):\n{dirty[:400]}")
ok, why = oracle.calibration_current()
if not ok:
sys.exit(f"delever --recipes: calibration not current ({why})")
by_src = oracle.recipes_by_src(oracle.load_recipes()["recipes"])
inc = includers()
sites_by = collections.defaultdict(list)
for s in load_sites():
if s.get("fn"):
sites_by[(s["tu"], s["fn"])].append(s)
cur = {}
for r in load_ledger():
if r.get("tu") and r.get("fn"):
cur[(r["tu"], r["fn"])] = r
def recs_for(tu):
return [r for t_ in inc.get(tu, []) for r in by_src.get(t_, [])] if tu.endswith(".h") else by_src.get(tu, [])
def judge(tu, cand):
"""judge one candidate, and be killable: the oracle writes the candidate into the tree to compile it, so a SIGTERM
between the write and the restore leaves a candidate in `src/`. The original text goes into inflight.json first, the
same file `--restore` reads (P35's rule: a tool restores from its OWN snapshot, never `git checkout`) — S99 killed a
run mid-judge and found exactly that leftover."""
path = REPO / tu
raw, st = path.read_text(errors="surrogateescape"), path.stat()
RUN.mkdir(parents=True, exist_ok=True)
INFLIGHT.write_text(json.dumps({tu: raw}))
try:
return oracle.judge_all(recs_for(tu), cand, tag="rec", write_path=(tu if tu.endswith(".h") else None))
finally:
restore_file(path, raw, st)
INFLIGHT.unlink(missing_ok=True)
def lever_free(tu, raw, fn):
m, ls = same_len_mask(raw), line_starts(raw)
edits = []
for s in sites_by[(tu, fn)]:
if (s["cls"], s["kind"]) not in REMOVABLE:
if s["kind"] in DEFERRED_KINDS:
raise Refuse("asm-body")
continue
edits += site_edits(raw, m, ls, s)
return apply_edits(raw, edits) if edits else raw
todo = [(k, r) for k, r in cur.items() if r.get("verdict") == "RESIDUE" and k[1] != FILE_SCOPE_FN]
if a.only:
todo = [(k, r) for k, r in todo if any(o in (k[0], k[1]) or o in k[0] for o in a.only)]
csize = collections.Counter(r.get("nhash_after") or r.get("nhash_before") for _, r in todo)
todo.sort(key=lambda x: (len([s for s in x[1].get("sites", []) if s.get("verdict") == "NEEDED"]),
-csize[x[1].get("nhash_after") or x[1].get("nhash_before")], x[0]))
todo = todo[:a.limit] if a.limit else todo
# THE CONTROL, before any verdict is believed (R39), on the first bodies drawn and on BOTH instruments:
# the splice machinery — the IDENTITY permutation through the same code must reproduce its input text exactly
# (a text assertion, no compile: if the line surgery is not byte-neutral, every "DIFFERS" below is its own);
# the oracle — the file exactly as the tree has it must still judge IDENTICAL (its baseline is live, R56).
bad = []
for (tu, fn), r in todo[:a.control]:
raw = (REPO / tu).read_text(errors="surrogateescape")
names = pin_names(sites_by[(tu, fn)])
dls = decl_lines(raw, tu, fn, names) if names else None
if dls:
lines = raw.split("\n")
ident = list(lines)
for slot, src in zip([i for i, _ in dls], range(len(dls))):
ident[slot] = [t for _, t in dls][src]
if "\n".join(ident) != raw:
bad.append((tu, fn, "SPLICE", "the identity permutation did not reproduce the text"))
continue
v, dt, err = judge(tu, raw)
if v != "IDENTICAL":
bad.append((tu, fn, v, err[:80]))
print(f"delever --recipes: control {a.control - len(bad)}/{min(a.control, len(todo))} — the identity splice reproduces "
f"its text and the untouched file its object", flush=True)
if bad or not todo:
for b in bad[:5]:
print(f" CONTROL FAILED {b[0]}:{b[1]} -> {b[2]} {b[3]}")
sys.exit("delever --recipes: the control did not pass — no verdict from this run is usable (R39)")
print(f"delever --recipes: {len(todo)} RESIDUE bodies", flush=True)
rows, won, tried, compiles = [], 0, 0, 0
t0 = time.time()
for n_body, ((tu, fn), r) in enumerate(todo, 1):
path = REPO / tu
raw = path.read_text(errors="surrogateescape")
names = pin_names(sites_by[(tu, fn)]) # may be empty: R5 needs no pinned declaration
try:
free = lever_free(tu, raw, fn)
except Refuse:
continue
cands = recipe_candidates(free, tu, fn, names, cap=a.cap)
if not cands:
continue
tried += 1
hit = None
for rec, desc, cand in cands:
v, dt, err = judge(tu, cand)
compiles += len(recs_for(tu))
if v == "IDENTICAL":
hit = (rec, desc, cand)
break
if n_body % 10 == 0 or hit: # R55: a lane that runs unattended leaves evidence
print(f" [{n_body}/{len(todo)}] {won} closed · {compiles} compiles · {(time.time() - t0) / 60:.1f} min",
flush=True)
row = dict(ts=time.strftime("%Y-%m-%d %H:%M:%S"), label=a.label, rung="R", calib=dict(head=oracle.head(), stamp=oracle.config_stamp()),
tu=tu, fn=fn, addr=fn_addr(fn, tu), aliases=r.get("aliases"), header=tu.endswith(".h"),
nhash_before=r.get("nhash_after"), nhash_after=None, candidates=len(cands), pins=len(names),
verdict=("LEVER-FREE" if hit else "RESIDUE"), recipe=(hit[0] if hit else None), how=(hit[1] if hit else None),
sites=([] if hit else r.get("sites", [])))
if hit:
path.write_text(hit[2], errors="surrogateescape")
# the body's own markers are now orphans (its levers are gone); every OTHER body's marker is still honest, so the
# scrub is scoped to this body's line span — a file-wide scrub would leave the census with UNMARKED sites elsewhere
recs_ = sc.scan_text(hit[2], tu, shared_defs=None)
d_ = next((x for x in recs_ if x["form"] == "def" and x["name"] == fn), None)
span = range(d_["line"], d_["end"] + 1) if d_ else range(0)
scrub = scrub_edits(hit[2], [i + 1 for i, l in enumerate(hit[2].split("\n")) if FAKE in l and i + 1 in span])
if scrub:
cand2 = apply_edits(hit[2], scrub)
v2, _, _ = judge(tu, cand2)
if v2 == "IDENTICAL":
path.write_text(cand2, errors="surrogateescape")
walk = lc.walk_file(path.read_text(errors="surrogateescape"), tu, tu.endswith(".h"))
row["nhash_after"] = next((x["nhash"] for x in walk["defs"] if x["name"] == fn), None)
won += 1
print(f" {tu}:{fn} — {hit[0]} {hit[1]} IDENTICAL ({len(names)} pin(s) gone)", flush=True)
rows.append(row)
ledger_append(rows)
needed = sum(len([s for s in r.get("sites", []) if s.get("verdict") == "NEEDED"]) for _, r in todo)
print(f"recipes: {won} of {tried} bodies closed lever-free ({needed} NEEDED sites in the {len(todo)} drawn), "
f"{compiles} compiles in {(time.time() - t0) / 60:.1f} min")
return 0
def remap_body(ex_before, ex_after, sib_before):
"""the exemplar's reshaped body, with its `func_/D_` addresses replaced by the sibling's — or (None, why).
A text class is "identical modulo addresses" (that IS the nhash), so the two old bodies' address tokens correspond
one for one in order; the map they define is applied to the new body. This is what turns one crack into a whole
class: the 134-copy classes are the reason the draw is ordered by copies. A ledger REPLAY cannot do it — the ledger
replays a SITE SET, and a reshaped body is not one."""
a, b = lc.NORM_SYM.findall(ex_before), lc.NORM_SYM.findall(sib_before)
if len(a) != len(b):
return None, f"{len(a)} address tokens in the exemplar, {len(b)} in the sibling"
m = {}
for x, y in zip(a, b):
if m.setdefault(x, y) != y:
return None, f"`{x}` maps to both `{m[x]}` and `{y}` — not one class"
return lc.NORM_SYM.sub(lambda mm: m.get(mm.group(0), mm.group(0)), ex_after), None
def propagate(a):
"""--propagate TU FN: the body TU:FN was reshaped and banked; give every RESIDUE sibling of its class the same
shape, with its own addresses, and judge each on its own objects."""
tu, fn = a.propagate
rows = load_ledger()
chain = [r for r in rows if r.get("tu") == tu and r.get("fn") == fn
and r.get("verdict") == "LEVER-FREE" and r.get("after_text")]
if not chain:
sys.exit(f"delever --propagate: no banked reshape of {tu}:{fn} in the ledger (its row must carry after_text)")
# THE CLASS is what the body looked like when the campaign found it, so the key and the "before" text come from the
# FIRST bank in this body's chain; the text to spread is the LAST one (a body reshaped, then tidied, has two rows, and
# taking the last row's before-hash would look for siblings of a text only this body ever had).
src_row = dict(chain[-1], nhash_before=chain[0]["nhash_before"], before_text=chain[0]["before_text"])
key = src_row["nhash_before"]
cur = {}
for r in rows:
if r.get("tu") and r.get("fn"):
cur[(r["tu"], r["fn"])] = r
sibs = [k for k, r in cur.items() if k != (tu, fn) and r.get("verdict") == "RESIDUE"
and (r.get("nhash_after") or r.get("nhash_before")) == key]
if a.only:
sibs = [k for k in sibs if any(o in k for o in a.only)]
sibs = sibs[:a.limit] if a.limit else sibs
print(f"delever --propagate: {tu}:{fn} -> {len(sibs)} sibling(s) of class {key[:12]}", flush=True)
if not sibs:
return 1 # R68: an empty work list is a refusal, not a success
ok = bad = 0
for stu, sfn in sibs:
path = REPO / stu
raw = path.read_text(errors="surrogateescape")
d = next((r for r in sc.scan_text(raw, stu, shared_defs=None) if r["form"] == "def" and r["name"] == sfn), None)
if d is None:
print(f" {stu}:{sfn}: not defined there — SKIPPED", flush=True)
bad += 1
continue
ls = line_starts(raw)
sib_before = raw[ls[d["line"] - 1]:ls[d["end"]]]
if lc.norm_hash(sc.mask_text(sib_before)) != key:
print(f" {stu}:{sfn}: its text is not this class any more — SKIPPED", flush=True)
bad += 1
continue
body, why = remap_body(src_row["before_text"], src_row["after_text"], sib_before)
if body is None:
print(f" {stu}:{sfn}: {why} — SKIPPED", flush=True)
bad += 1
continue
f = RUN / "propagate" / f"{stu.replace('/', '_')}__{sfn}.c"
f.parent.mkdir(parents=True, exist_ok=True)
f.write_text(body, errors="surrogateescape")
r = subprocess.run([sys.executable, str(REPO / "tools/delever.py"), "--apply-body", stu, sfn, str(f),
"--label", a.label, "--rung", src_row.get("rung") or "R", "--dirty-ok"],
cwd=REPO, capture_output=True, text=True)
line = ((r.stdout or r.stderr).strip().splitlines() or [""])[-1]
print(f" {line[:200]}", flush=True)
ok += r.returncode == 0
bad += r.returncode != 0
print(f"delever --propagate: {ok} of {len(sibs)} sibling(s) banked, {bad} refused")
return 0 if ok else 1
def status():
rows = load_ledger()
done, ex = ledger_index(rows)
@@ -1318,7 +1824,10 @@ def apply_body(a):
nhash_before=nh_before, nhash_after=nh_after, source=src, verdict=("LEVER-FREE" if v == "IDENTICAL" else f"BODY-{v}"),
sites=[dict(ord=i, kind=s["kind"], cls=s["cls"], detail=s["detail"], via=s.get("via", ""), line=s["line"], verdict="NEEDED",
why="left by the author", oracle="") for i, s in enumerate(levers)],
compiles=len(recs_), seconds=round(dt, 3), objects=[r["obj"] for r in recs_])
compiles=len(recs_), seconds=round(dt, 3), objects=[r["obj"] for r in recs_],
# the body AS IT WAS: --propagate needs it to map this class's addresses onto a sibling's, and after the
# write it exists nowhere else (the tree has moved on and the ledger is the record)
before_text=before, after_text=new)
if v == "IDENTICAL":
path.write_text(cand, errors="surrogateescape")
ledger_append([row])
@@ -1569,6 +2078,47 @@ def selftest():
plan_, total_, _ = make_plan(fake_bodies, rows, False, 10, None)
if total_ != 1:
fail("a copy with a judged text but no row of its own must still be drawn")
# rung R's candidate generators, on a fixture whose every answer is known by hand
RFIX = ("void rfix(int p)\n{\n int a = p;\n int b;\n int c;\n"
" if (a == b) a = b & 3;\n c = a + 1;\n *(int *)(p + 4) = c;\n}\n")
rd = dict(line=1, end=9)
for line, want in [("s32 d = param_1;", True), ("ret = f();", False), ("u8 *p;", True), ("d = param_1;", False),
("return x;", False), ("extern s32 D_1[];", True)]:
if is_decl_line(line) != want:
fail(f"is_decl_line({line!r}) != {want}")
if top_level_ops("a & b") != [2] or top_level_ops("a && b") or top_level_ops("(s32 *)p") \
or top_level_ops("*(s32 *)(p + 4) & 0xFF") != [16]:
fail("top_level_ops: a binary commutative operator at depth 0, and nothing else")
sw = commutative_swaps(RFIX, "src/x.c", "rfix", rd)
if [s for s, _ in sw] != ["swap & @6", "swap + @7"]:
fail(f"commutative_swaps found {[s for s, _ in sw]}")
if "a = 3 & b;" not in sw[0][1] or "c = 1 + a;" not in sw[1][1]:
fail("commutative_swaps must split at the assignment, never at `==`")
R6FIX = ("void r6(int p)\n{\n int v;\n int w;\n v = *(int *)(p + 4);\n"
" *(int *)(p + 4) = v & ~0x20;\n w = 3;\n *(int *)(p + 8) = w;\n}\n")
inl = inline_single_set_temps(R6FIX, "src/x.c", "r6", dict(line=1, end=9))
if [d for d, _ in inl] != ["inline v @5", "inline w @7"]:
fail(f"inline_single_set_temps found {[d for d, _ in inl]}")
if "= (*(int *)(p + 4)) & ~0x20;" not in inl[0][1] or "int v;" in inl[0][1]:
fail("R6 must inline the expression at the use AND drop the now-dead declaration")
bw = block_wraps(R6FIX, "src/x.c", "r6", dict(line=1, end=9))
if [d for d, _ in bw][:2] != ["block @5", "do-while @5"] or "{ v = *(int *)(p + 4); }" not in bw[0][1]:
fail(f"block_wraps: {[d for d, _ in bw][:3]} (the readable spelling first)")
cands = recipe_candidates(RFIX, "src/x.c", "rfix", ["a", "b"])
kinds_ = {r for r, _, _ in cands}
if not {"R2", "R3", "R5"} <= kinds_ or any(c == RFIX for _, _, c in cands):
fail(f"recipe_candidates: {kinds_} (the seed must never be a candidate)")
if any("a = p;" in c.split("\n")[7] for _, _, c in cands if _ == "R3"):
fail("R3 must place its assignment after the whole declaration run (C89)")
# the address remap that propagates a reshape to a class (R48-adjacent: one crack, 134 banks)
exb = "void func_80100000(void) { D_80200000 = func_80100004(); }"
exa = "void func_80100000(void) { s32 t = func_80100004(); D_80200000 = t; }"
sib = "void func_80300000(void) { D_80400000 = func_80300004(); }"
got, why = remap_body(exb, exa, sib)
if got != "void func_80300000(void) { s32 t = func_80300004(); D_80400000 = t; }":
fail(f"remap_body produced {got!r} ({why})")
if remap_body(exb, exa, "void func_80300000(void) { D_80400000 = 0; }")[0] is not None:
fail("remap_body must refuse a sibling with a different token count")
# the oracle's crash classification on its real message forms (R103)
if not oracle.SIGNAL_LINE.search("bash: line 1: 3845091 Done mipsel-linux-gnu-cpp ...\n 3845092 Aborted (core dumped) | tools/bin/gcc-2.7.2-psx/cc1 -quiet\n"):
fail("SIGNAL_LINE must match bash's job-status block")
@@ -1792,6 +2342,13 @@ def main():
ap.add_argument("--restore", action="store_true", help="restore every in-flight file from inflight.json")
ap.add_argument("--status", action="store_true")
ap.add_argument("--scrub", action="store_true", help="remove orphan !FAKE markers (a marker whose site is gone), byte-judged per file")
ap.add_argument("--propagate", nargs=2, metavar=("TU", "FN"),
help="give every RESIDUE sibling of this banked body's class the same shape, with its own addresses")
ap.add_argument("--recipes", action="store_true",
help="rung R: the cookbook's byte-neutral shape recipes tried mechanically on every RESIDUE body")
ap.add_argument("--cap", type=int, default=60, help="--recipes: candidates tried per body (each is one compile)")
ap.add_argument("--control", type=int, default=8, help="--recipes: how many LEVER-FREE bodies the control run reproduces (R39)")
ap.add_argument("--limit", type=int, help="--recipes: stop after this many RESIDUE bodies")
ap.add_argument("--apply-body", nargs=3, metavar=("TU", "FN", "FILE"))
ap.add_argument("--rung", default="E")
ap.add_argument("--allow-residue", action="store_true")
@@ -1810,6 +2367,14 @@ def main():
sys.exit(status())
if a.scrub:
sys.exit(scrub(a))
if a.propagate:
if not a.label:
sys.exit("delever --propagate: --label is required (R48)")
sys.exit(propagate(a))
if a.recipes:
if not a.label:
sys.exit("delever --recipes: --label is required (R48: the ledger rows are keyed by it)")
sys.exit(recipes(a))
if a.apply_body:
if not a.label:
sys.exit("delever --apply-body: --label is required")
@@ -23,10 +23,12 @@ THE PIPELINE PER EXEMPLAR (probe-proven at S98, the T6 log entry):
the permuter compiles must hold ONE .text function, because the masked scorer compares whole .text.
3. cpp -P with the BUILD's own CPPFLAGS + -I<the TU's directory> (common.h, the prelude, engine_types.h and the GTE header
expand here — p16_permute.make_base_c runs its own cpp WITHOUT includes and would lose them) -> draft.c.
4. the target .s — tools/verbatim_target_s.py regenerates it from the EXTRACTED ROM IMAGE (R34: an independent
oracle; our own source is the thing under test), into the exemplar's scratch dir, where p16_permute.setup reads it.
5. tools/permuter_ils.py — warm-restarting decomp-permuter, the weight profile chosen from the NEEDED sites' kinds
(pins -> regalloc, barriers/launders/keep-alives -> schedule, mixed -> regalloc).
4. the target — the tree's OWN (levered) body compiled by the build's tail into a one-function object, so it
carries the candidates' relocations by construction; `tools/verbatim_target_s.py` regenerates the ROM listing beside it
and `match_one` must call that body a MATCH before the search starts (R34: the ROM image is the independent oracle that
PROVES the target; R56: check the baseline). S99 learned this the hard way — see THE INSTRUMENT below.
5. tools/permuter_ils.py — warm-restarting decomp-permuter, the weight profile chosen from the NEEDED sites' kinds AND
the register each pin names (callee-saved -> regalloc, caller-saved -> cse, barriers/launders/keep-alives -> schedule).
6. a score-0 winner is a CANDIDATE, never a bank: --bank puts the function's definition back through
`delever.py --apply-body` (which refuses a body that still carries a class A/B lever and judges it on the object's bytes
through every recipe of the TU), then re-folds the cpp-expanded GTE asm with `gte_consolidate.py --apply --rejudge`.
@@ -36,6 +38,15 @@ SCRATCH IS KEYED BY ALIAS+FN (R48): `.run/P36/permuter/<alias>__<fn>/` — a fun
overlays overlap in RAM, so two different bodies can both be `func_8013B274`), and the permuter's own scratch/winner paths are
keyed by the bare name. Every attempt is recorded in `.run/P36/permuter/outcomes.jsonl`, which is also the skip list.
THE INSTRUMENT, AND WHY IT IS CHECKED BEFORE IT IS BELIEVED (S99). The first two campaigns returned 0 of 16 with a straight
face. Both were the harness: the target had been ASSEMBLED FROM A DISASSEMBLY LISTING, which is a second toolchain with its own
answers — objdump prints the pseudo-instruction `move` for `addu rX,rY,$zero` and gas assembles it as `or` (24 wrong words in
one 234-instruction function), and a listing's %hi/%lo pairs come back resolved with no relocation while every candidate carries
one. The permuter scored 28 for a body that IS byte-identical, so score 0 was unreachable and every NO-MATCH was its own. The
control that names this in one line is `--positive-control`, and the base score of the tree's own body is now 0. R40: exonerate
the instrument before attributing a failure to its subject; the campaign that skips this control cannot tell a hard population
from a broken scorer.
NEVER RUN THE CAMPAIGN AS A HARNESS BACKGROUND TASK (the low-memory guard kills it): `setsid nohup … &` + a Monitor on the log.
"""
import argparse
@@ -148,15 +159,32 @@ def exemplars(only=(), limit=None, include_done=False):
needed = [s for s in r.get("sites", []) if s.get("verdict") == "NEEDED"]
out.append(dict(nhash=nh, tu=tu, fn=fn, alias=(r.get("aliases") or [None])[0], copies=len(members),
needed=len(needed), kinds=sorted({s["kind"] for s in needed}),
regs=sorted({s.get("detail", "") for s in needed if s["kind"] == "pin"}),
members=[dict(tu=t, fn=f) for (t, f), _ in members], row=r))
out.sort(key=lambda e: (-e["copies"], e["needed"], e["tu"], e["fn"]))
out = [e for e in out if matches(e, only)]
return out[:limit] if limit else out
def klass_for(kinds):
"""the permuter_weights profile for a residue's NEEDED-site mix (the profile NAME is accepted verbatim by classify())."""
CALLEE_SAVED = {f"${n}" for n in range(16, 24)} | {f"$s{n}" for n in range(8)} | {"$fp", "$30"}
def klass_for(kinds, regs=()):
"""the permuter_weights profile for a residue, from the NEEDED sites' kinds AND the REGISTER each pin names (the profile
name is accepted verbatim by classify()).
The register is the part S99 learned by reading a residual instead of assuming one. A pin on a CALLEE-SAVED register
($16-$23) is an allocation-ORDER residual — the regalloc profile's declaration/statement reordering is its lever. A pin
on a CALLER-SAVED one ($2/$3/$4-$7/…) is not: the S99 control on func_80163EC8 (`register … __asm__("$2")`) left exactly
a v0/v1 swap — `lw v1,68(s1); li v0,-33; and v0,v1,v0` against `lw v0,68(s1); li v1,-33; and v0,v0,v1` — which is the
operand ORDER of one `&`, and the regalloc profile weights `perm_commutative` 2.0 while the cse profile weights it 40.0.
Steering that search by "it was a pin, so regalloc" spends the whole budget 20x away from the one lever that closes it."""
ks = set(kinds)
rs = {r for r in regs if r}
if "pin" in ks and rs and rs & CALLEE_SAVED:
return "regalloc"
if "pin" in ks and rs and not (rs & CALLEE_SAVED):
return "cse"
if ks and ks <= SCHED_KINDS:
return "schedule"
return "regalloc"
@@ -169,12 +197,17 @@ _sites_cache = None
def tu_sites(tu):
"""the census's sites for one TU. Built ONCE, into a local, and published under the lock: the workers are threads, and
publishing the empty dict before filling it (2 s for 53,355 rows) let every other worker read "no site in this TU" and
report a whole batch UNSTRIPPABLE — a race that lies in the tool's own voice."""
global _sites_cache
if _sites_cache is None:
_sites_cache = collections.defaultdict(list)
for s in dl.load_sites():
_sites_cache[s["tu"]].append(s)
return _sites_cache[tu]
with _LOCK:
if _sites_cache is None:
built = collections.defaultdict(list)
for s in dl.load_sites():
built[s["tu"]].append(s)
_sites_cache = built
return _sites_cache.get(tu, [])
def body_sites(tu, fn):
@@ -318,7 +351,7 @@ def scratch_of(alias, fn):
return RUN / f"{alias}__{fn}"
def prepare(ex, quiet=False):
def prepare(ex, quiet=False, require_sites=True):
"""(scratch dir, draft path, target path, klass) for one exemplar — the whole probe-2 pipeline."""
tu, fn = ex["tu"], ex["fn"]
alias = ex["alias"] or alias_of(tu, ex.get("row"))
@@ -327,7 +360,7 @@ def prepare(ex, quiet=False):
ex["alias"] = alias
raw = (REPO / tu).read_text(errors="surrogateescape")
sites = body_sites(tu, fn)
if not sites:
if not sites and require_sites:
raise Unstrippable([("<census>", 0, f"the census has no site in {tu}:{fn} (stale? rerun lever_census --sites)")])
d = scratch_of(alias, fn)
if d.exists():
@@ -357,10 +390,18 @@ def prepare(ex, quiet=False):
if r.returncode or not tgt.exists():
raise Unstrippable([("<target>", 0, ((r.stderr or r.stdout).strip().splitlines() or ["no listing"])[-1][:160])])
shutil.copy(tgt, d / sub / f"{fn}.s") # p16_permute.setup / match_one.py read <dir>/<fn>.s
# THE TARGET OBJECT: the tree's own (levered) body through the build's own tail, so it carries the candidates'
# relocations by construction. Its fidelity is not assumed — `closeness(levered)` must be MATCH against the ROM
# listing above before any search runs (R34: the independent oracle proves the target; R56: check the baseline).
r = subprocess.run(["tools/permuter/compile.sh", str(d / "levered.c"), "-o", str(d / "target.o")],
capture_output=True, text=True, cwd=REPO)
if r.returncode or not (d / "target.o").exists():
raise Unstrippable([("<target.o>", 0, ((r.stderr or r.stdout).strip().splitlines() or ["compile failed"])[-1][:160])])
if not quiet:
n = len((d / "draft.c").read_text(errors="surrogateescape").splitlines())
print(f" prepared {alias}__{fn}: draft {n} lines, target {(r.stdout or '').strip().split()[-3:]}", flush=True)
return d, d / "draft.c", d / "gas" / f"{fn}.s", klass_for(ex["kinds"])
print(f" prepared {alias}__{fn}: draft {n} lines, target.o {(d / 'target.o').stat().st_size} bytes "
f"+ the ROM listing", flush=True)
return d, d / "draft.c", d / "gas" / f"{fn}.s", klass_for(ex["kinds"], ex.get("regs", ()))
# ----------------------------------------------------------------------------------------------------------------------
@@ -382,6 +423,52 @@ def closeness(d, fn, c):
return (int(mm.group(1)) if mm else None), first
def positive_control(a):
"""Can rung D close a gap it is KNOWN to be able to close? Take a body the tree already matches, perturb it by ONE
reversible source change (the first commutative operand swap `delever` can generate), confirm the perturbed body no
longer matches, and give the permuter one cycle to find its way back to score 0.
Without this, "0 of 16" says nothing about the residue: it could equally be a harness that cannot reach ANY target
(R40 — exonerate the instrument before attributing the failure to its subject). With it, a PASS means the search
space, the target, the scorer and the winner path all work, and a 0-yield campaign is a fact about the population."""
tu, fn = a.positive_control
raw = (REPO / tu).read_text(errors="surrogateescape")
d_ = next((r for r in sc.scan_text(raw, tu, shared_defs=None) if r["form"] == "def" and r["name"] == fn), None)
if d_ is None:
sys.exit(f"delever_permute --positive-control: {fn} is not defined in {tu}")
swaps = dl.commutative_swaps(raw, tu, fn, d_)
if not swaps:
sys.exit(f"delever_permute --positive-control: {tu}:{fn} has no commutative operator to perturb — pick another body")
ex = dict(nhash="", tu=tu, fn=fn, alias=alias_of(tu), copies=1, needed=0, kinds=[], regs=[], members=[], row={})
d, draft, tgt, _ = prepare(ex, quiet=False, require_sites=False)
base, base_line = closeness(d, fn, draft)
desc, perturbed = swaps[a.which]
(d / "perturbed_tu.c").write_text(perturbed, errors="surrogateescape")
pert = drop_file_scope_asm(cpp_expand(tu, isolate(tu, perturbed, fn)), tu)
(d / "draft.c").write_text(pert, errors="surrogateescape") # the permuter's seed IS the perturbed body
after, after_line = closeness(d, fn, d / "draft.c")
print(f"positive control {alias_of(tu)}__{fn}: the tree's own body {base_line[:40]} · perturbed by `{desc}` {after_line[:60]}",
flush=True)
if base != 0:
sys.exit("delever_permute --positive-control: the UNPERTURBED body does not match — calibrate first (R56)")
if after == 0:
sys.exit(f"delever_permute --positive-control: `{desc}` is byte-neutral here — nothing to find; try --which {a.which + 1}")
rel = d.relative_to(REPO).as_posix()
log = d / "positive.log"
cmd = [PY, "tools/permuter_ils.py", fn, "--draft", rel + "/draft.c", "--asm-subdir", rel + "/gas",
"--klass", "cse", "--cycles", str(a.cycles), "--secs", str(a.secs), "--j", str(a.j),
"--winners", rel, "--pd", rel + "/pd"]
with open(log, "w") as f:
f.write(" ".join(cmd) + "\n\n")
f.flush()
subprocess.run(cmd, cwd=REPO, stdout=f, stderr=subprocess.STDOUT, timeout=a.cycles * (a.secs + 45) + 300)
out = log.read_text(errors="replace")
won = (d / f"{fn}.c").exists()
print(f"positive control: {'PASS — the permuter recovered score 0' if won else 'FAIL — it did not, in '
f'{a.cycles}x{a.secs}s'} ({[l for l in out.splitlines() if 'cycle' in l][-1:] or ['no cycle line']})")
return 0 if won else 1
def calibrate_one(ex, quiet=False):
"""the control: the body AS THE TREE HAS IT (levers and all) must be MATCH against the regenerated target. It proves the
whole chain — the target from the ROM image, the isolation, the cpp expansion, the pinned triple — is measuring THIS
@@ -405,7 +492,7 @@ BEST_RE = re.compile(r"best score = (\d+)")
def run_one(ex, secs, cycles, j, max_start=None):
t0 = time.time()
row = dict(kind="attempt", ts=time.strftime("%Y-%m-%d %H:%M:%S"), nhash=ex["nhash"], tu=ex["tu"], fn=ex["fn"],
alias=ex["alias"], copies=ex["copies"], needed=ex["needed"], kinds=ex["kinds"],
alias=ex["alias"], copies=ex["copies"], needed=ex["needed"], kinds=ex["kinds"], regs=ex.get("regs", []),
secs=secs, cycles=cycles, j=j)
try:
d, draft, tgt, klass = prepare(ex)
@@ -437,7 +524,7 @@ def run_one(ex, secs, cycles, j, max_start=None):
log = d / "ils.log"
cmd = [PY, "tools/permuter_ils.py", ex["fn"], "--draft", (draft.relative_to(REPO)).as_posix(),
"--asm-subdir", rel + "/gas", "--klass", klass, "--cycles", str(cycles), "--secs", str(secs),
"--j", str(j), "--winners", rel, "--pd", rel + "/pd"]
"--j", str(j), "--winners", rel, "--pd", rel + "/pd", "--target-o", rel + "/target.o"]
row["klass"] = klass
row["cmd"] = " ".join(cmd)
rc = None
@@ -485,10 +572,34 @@ def winner_body(winner_c, fn, tu):
return None
def tidy_body(body):
"""the permuter's winner, made to read like the tree it lands in — or None when there is nothing to tidy.
pycparser reprints a body it has parsed: two-space indent where this tree uses four, and an empty statement `;` left
where a statement was inlined away. Neither generates code, and the object oracle judges the tidied text before it is
kept, so this can only ever improve the source. What it deliberately does NOT touch is the parenthesisation
(`*((s32 *) (p + 4))`) or the brace style — that is the formatting phase's job, with clang-format, over the whole tree
at once (`decomp-architect/templates/.clang-format`); a per-bank reformat would make this phase's diffs unreadable."""
lines = [l for l in body.split("\n") if l.strip() != ";"]
indents = [len(l) - len(l.lstrip(" ")) for l in lines if l.strip() and l.startswith(" ")]
if indents and min(indents) == 2: # a two-space body: double every leading run
lines = [(" " * (2 * (len(l) - len(l.lstrip(" ")))) + l.lstrip(" ")) if l.startswith(" ") else l for l in lines]
out = "\n".join(lines)
return out if out != body else None
def bank(a):
outs = load_outcomes()
banked = {(o["alias"], o["fn"], o["nhash"]) for o in outs if o.get("kind") == "bank" and o.get("applied")}
# a body ANOTHER rung already closed is not banked again (S99: --bank re-applied the permuter's reprinted body over the
# one-line version rung R had banked for the same function, and the tree lost a clean diff for a byte-identical one)
cur = {}
for r in dl.load_ledger():
if r.get("tu") and r.get("fn"):
cur[(r["tu"], r["fn"])] = r
already = {(k[0], k[1]) for k, r in cur.items() if r.get("verdict") == "LEVER-FREE"}
todo = [o for o in outs if o.get("kind") == "attempt" and o.get("verdict") == "MATCH"
and (o["tu"], o["fn"]) not in already
and (o["alias"], o["fn"], o["nhash"]) not in banked
and (not a.only or any(x in (o["fn"], o["tu"], o["alias"]) or o["nhash"].startswith(x) for x in a.only))]
if not todo:
@@ -503,11 +614,17 @@ def bank(a):
bad += 1
continue
bf = d / "body.c"
bf.write_text(body, errors="surrogateescape")
cmd = [PY, "tools/delever.py", "--apply-body", o["tu"], o["fn"], bf.relative_to(REPO).as_posix(),
"--label", a.label, "--rung", "D"] + (["--dirty-ok"] if a.dirty_ok else [])
r = subprocess.run(cmd, cwd=REPO, capture_output=True, text=True)
print(" " + (r.stdout or r.stderr).strip().splitlines()[-1][:220], flush=True)
# the tidied body FIRST (the oracle judges it like any other candidate); the winner verbatim only if it is refused
forms = [(t_, "tidied") for t_ in [tidy_body(body)] if t_] + [(body, "verbatim")]
r = None
for text_, how in forms:
bf.write_text(text_, errors="surrogateescape")
cmd = [PY, "tools/delever.py", "--apply-body", o["tu"], o["fn"], bf.relative_to(REPO).as_posix(),
"--label", a.label, "--rung", "D"] + (["--dirty-ok"] if a.dirty_ok else [])
r = subprocess.run(cmd, cwd=REPO, capture_output=True, text=True)
print(f" [{how}] " + (r.stdout or r.stderr).strip().splitlines()[-1][:210], flush=True)
if r.returncode == 0:
break
row = dict(kind="bank", ts=time.strftime("%Y-%m-%d %H:%M:%S"), nhash=o["nhash"], tu=o["tu"], fn=o["fn"],
alias=o["alias"], label=a.label, applied=(r.returncode == 0),
apply_out=(r.stdout or r.stderr).strip()[-400:])
@@ -535,7 +652,8 @@ def cmd_plan(a):
f"({sum(e['copies'] for e in ex):,} bodies behind them); NEEDED kinds {dict(kinds)}")
for e in ex[:a.show]:
print(f" {e['copies']:4d} copies · {e['needed']:2d} needed {','.join(e['kinds']):<28} "
f"{klass_for(e['kinds']):8} {e['alias'] or alias_of(e['tu'], e['row'])} {e['tu']}:{e['fn']}")
f"{klass_for(e['kinds'], e.get('regs', ())):8} {','.join(e.get('regs', [])) or '-':<12} "
f"{e['alias'] or alias_of(e['tu'], e['row'])} {e['tu']}:{e['fn']}")
if len(ex) > a.show:
print(f" … {len(ex) - a.show} more")
return 0
@@ -667,6 +785,8 @@ def main():
g = ap.add_mutually_exclusive_group(required=True)
g.add_argument("--plan", action="store_true")
g.add_argument("--prepare", nargs=2, metavar=("TU", "FN"))
g.add_argument("--positive-control", nargs=2, metavar=("TU", "FN"),
help="perturb a MATCHING body by one commutative swap and require the permuter to find its way back")
g.add_argument("--calibrate", action="store_true",
help="the control: N exemplars' levered bodies must all be MATCH against their regenerated targets")
g.add_argument("--run", action="store_true")
@@ -682,6 +802,7 @@ def main():
ap.add_argument("-j", type=int, default=16)
ap.add_argument("--label", default="d1")
ap.add_argument("--dirty-ok", action="store_true")
ap.add_argument("--which", type=int, default=0, help="--positive-control: which commutative swap to perturb with")
ap.add_argument("--max-start", type=int,
help="skip (as FAR) an exemplar whose lever-free body is further than N instructions from the target")
ap.add_argument("--include-done", action="store_true", help="draw classes that already have an outcome row")
@@ -702,6 +823,8 @@ def main():
d, draft, tgt, klass = prepare(ex)
print(f"delever_permute --prepare: {d.relative_to(REPO)} (draft.c, {tgt.name}, klass={klass})")
return 0
if a.positive_control:
return positive_control(a)
if a.calibrate:
return cmd_calibrate(a)
if a.run:
@@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""lever_progress.py — the lever-removal series: what the compiler-forcing constructs cost, and how they came off.
tools/lever_progress.py --snapshot "T6 r1" # append today's census totals as a milestone row (evidence, with HEAD)
tools/lever_progress.py --render # regenerate the generated block of docs/levers.md from the data
tools/lever_progress.py --table # print both tables
tools/lever_progress.py --check # is the last snapshot this tree's? (a stale series is a wrong chart)
WHY THIS EXISTS. The phase that takes the levers out is the only place the project ever counts them, and the count is worth
more than the phase: it is the post-100% chart, the story's spine, and the evidence behind the day-one kit's advice on
whether to prevent levers from the first bank or clean them up at the end. Numbers are DERIVED, never typed (R75).
TWO TABLES, TWO SOURCES:
* MILESTONES — one appended row per census snapshot (`--snapshot`), because a census is a moment: it is not recoverable
later. Each row carries the tree's HEAD and the census's own totals, so a reader can re-derive it from that commit.
* THE CAMPAIGN — fully derived from `.run/P36/delever/ledger.jsonl` on every run: per batch label, the date, the rungs
used, the bodies first judged, the sites REMOVED / REWRITTEN, and — scored as a TRANSITION against what that body's
previous row left — the sites a later rung closed and the bodies it made lever-free. Rebuilt from scratch each render,
so it cannot drift.
"""
import argparse
import collections
import json
import pathlib
import subprocess
import sys
REPO = pathlib.Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "tools"))
CENSUS = REPO / ".run" / "P36" / "census" / "lever_census.json"
LEDGER = REPO / ".run" / "P36" / "delever" / "ledger.jsonl"
SERIES = REPO / "docs" / "lever-progress.tsv"
DOC = REPO / "docs" / "levers.md"
BEGIN = "<!-- BEGIN GENERATED: lever_progress.py -->"
END = "<!-- END GENERATED -->"
COLS = ["date", "milestone", "head", "sites_AB", "pins", "asm", "bodies_AB", "distinct_AB", "marked", "unmarked",
"gte_levers", "per_tu_asm_macros", "class_C", "class_D", "class_E", "class_F", "class_G"]
def census():
if not CENSUS.exists():
sys.exit("lever_progress: no census — run tools/lever_census.py --sites -j 16")
return json.loads(CENSUS.read_text())
def ledger():
rows = []
if LEDGER.exists():
for l in LEDGER.read_text().splitlines():
if l.strip():
rows.append(json.loads(l))
return rows
def snapshot_row(milestone):
d = census()
ab, cl = d["levers_AB"], d["classes"]
return dict(date=d.get("generated", ""), milestone=milestone, head=d.get("head", ""),
sites_AB=ab["sites"], pins=ab["pins"], asm=ab["asm"], bodies_AB=ab["bodies"],
distinct_AB=ab["distinct_bodies"], marked=ab["marked"], unmarked=ab["unmarked"],
gte_levers=d.get("gte_levers", {}).get("sites", 0),
per_tu_asm_macros=d.get("per_tu_asm_macro_definitions", {}).get("total", 0),
**{f"class_{k}": cl.get(k, {}).get("sites", 0) for k in "CDEFG"})
def read_series():
if not SERIES.exists():
return []
lines = [l for l in SERIES.read_text().splitlines() if l.strip()]
if not lines:
return []
head = lines[0].split("\t")
return [dict(zip(head, l.split("\t"))) for l in lines[1:]]
def write_series(rows):
SERIES.write_text("\t".join(COLS) + "\n" + "".join("\t".join(str(r.get(c, "")) for c in COLS) + "\n" for r in rows))
def campaign():
"""per batch label, derived from the ledger, as STATE TRANSITIONS — not as "the first row wins".
A body is judged more than once on purpose: rung A/B settles it with some sites left NEEDED, and a later rung (R, D or
an agent) comes back and closes those. Counting only a body's first row credits the whole campaign to T4 and reports
"0 removed" for the rung that actually finished the job (rung R's first batch closed 134 bodies and read as nothing).
So each row is scored against what that body's previous row left: `removed`/`rewritten` are the first pass's, and
`closed_later` is the fall in the NEEDED count afterwards, with `made_free` counting the bodies that reached zero."""
prev, per = {}, collections.OrderedDict()
for r in ledger():
lab = r.get("label") or "-"
key = (r.get("tu"), r.get("fn"))
ts = (r.get("ts") or "")[:10]
e = per.setdefault(lab, dict(label=lab, date=ts, rungs=set(), bodies=0, removed=0, rewritten=0,
closed_later=0, made_free=0, refused=0))
if ts and (not e["date"] or ts < e["date"]):
e["date"] = ts
if r.get("rung"):
e["rungs"].add(str(r["rung"]))
sites = r.get("sites", [])
now_needed = sum(1 for s in sites if s.get("verdict") == "NEEDED")
was = prev.get(key)
if was is None:
e["bodies"] += 1
e["removed"] += sum(1 for s in sites if s.get("verdict") == "REMOVED")
e["rewritten"] += sum(1 for s in sites if s.get("verdict") == "REWRITTEN")
e["refused"] += sum(1 for s in sites if s.get("verdict") == "REFUSED")
else:
e["closed_later"] += max(0, was - now_needed)
if was > 0 and now_needed == 0:
e["made_free"] += 1
prev[key] = now_needed
return list(per.values())
def md_tables():
rows = read_series()
out = [BEGIN, "", "### Milestones — the count, at each moment a census ran", "",
"| date | milestone | pins | asm | **class A+B sites** | bodies | distinct | GTE levers | per-TU asm macros | C | D | E | F | G | HEAD |",
"|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---|"]
for r in rows:
out.append("| {date} | {milestone} | {pins} | {asm} | **{sites_AB}** | {bodies_AB} | {distinct_AB} | {gte_levers} | "
"{per_tu_asm_macros} | {class_C} | {class_D} | {class_E} | {class_F} | {class_G} | `{head}` |".format(**r))
out += ["", "### The campaign — every batch, from the ledger (derived on every render)", "",
"| batch | date | rungs | bodies first judged | sites removed | rewritten | sites closed later | bodies made lever-free |",
"|---|---|---|---:|---:|---:|---:|---:|"]
tot = dict(bodies=0, removed=0, rewritten=0, closed_later=0, made_free=0)
for e in campaign():
if not any(e[k] for k in tot):
continue # a bookkeeping batch (a scrub, a re-mark) moved no site
out.append(f"| `{e['label']}` | {e['date']} | {','.join(sorted(e['rungs'])) or '-'} | {e['bodies']:,} | "
f"{e['removed']:,} | {e['rewritten']:,} | {e['closed_later']:,} | {e['made_free']:,} |")
for k in tot:
tot[k] += e[k]
out.append(f"| **total** | | | **{tot['bodies']:,}** | **{tot['removed']:,}** | **{tot['rewritten']:,}** | "
f"**{tot['closed_later']:,}** | **{tot['made_free']:,}** |")
out += ["", f"*Generated by `tools/lever_progress.py --render` from `.run/P36/census/lever_census.json` "
f"and `.run/P36/delever/ledger.jsonl`; the series lives in `docs/lever-progress.tsv` (R75: published "
f"numbers are generated, never typed).*", "", END]
return "\n".join(out)
def render():
if not DOC.exists():
sys.exit(f"lever_progress: {DOC.relative_to(REPO)} does not exist yet")
t = DOC.read_text()
if BEGIN not in t or END not in t:
sys.exit(f"lever_progress: {DOC.relative_to(REPO)} has no generated block ({BEGIN} … {END})")
pre, rest = t.split(BEGIN, 1)
_, post = rest.split(END, 1)
DOC.write_text(pre + md_tables() + post)
print(f"lever_progress: rendered {len(read_series())} milestone row(s) + {len(campaign())} batch row(s) "
f"into {DOC.relative_to(REPO)}")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--snapshot", metavar="MILESTONE", help="append today's census totals as a milestone row")
ap.add_argument("--render", action="store_true")
ap.add_argument("--table", action="store_true")
ap.add_argument("--check", action="store_true")
a = ap.parse_args()
if a.snapshot:
rows = read_series()
row = snapshot_row(a.snapshot)
rows = [r for r in rows if not (r.get("milestone") == row["milestone"] and r.get("head") == row["head"])]
rows.append(row)
write_series(rows)
print(f"lever_progress: {row['milestone']} — {row['sites_AB']:,} class A+B sites "
f"({row['pins']:,} pins + {row['asm']:,} asm) in {row['bodies_AB']:,} bodies, "
f"{row['unmarked']} unmarked, at {row['head']}")
if a.render or a.snapshot:
if DOC.exists():
render()
if a.table:
print(md_tables())
if a.check:
rows, d = read_series(), census()
if not rows:
sys.exit("lever_progress --check: the series is empty (run --snapshot)")
last = rows[-1]
if last.get("head") != d.get("head") or int(last.get("sites_AB", -1)) != d["levers_AB"]["sites"]:
sys.exit(f"lever_progress --check: the series' last row ({last.get('milestone')}, {last.get('sites_AB')} sites "
f"at {last.get('head')}) is not this tree ({d['levers_AB']['sites']} at {d.get('head')}) — "
f"run --snapshot after the census (R75)")
print(f"lever_progress --check: OK — {len(rows)} milestone(s), last {last['milestone']} "
f"({last['sites_AB']} class A+B sites)")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -165,6 +165,43 @@ def to_gas(insns, vaddr, off, symtab):
return out
def gas_roundtrip(rows, insns, fn):
"""[(row index, ROM word)] for every emitted instruction whose RE-ASSEMBLY does not reproduce the ROM's word.
WHY (S99, and it invalidated a whole campaign before it was found): objdump prints the PSEUDO-instruction
`move s2,a0` for `addu s2,a0,$zero` (0x00809021), and gas assembles `move` as `or` (0x00809025). Every `move` in
the listing therefore came back a different word, so a target object built from this form differed from the ROM
in every one of them — and the permuter, scoring against it, reported 28 for a body that IS byte-identical. A
listing nothing ever assembled and compared is a claim, not an oracle (R98/DK-81 in a second place). Words that
carry a RELOCATION (jal/j, HI16/LO16) are excluded: the linker fills those, and both sides mask them."""
body = "\n".join([f".include \"macro.inc\"", ".set noat", ".set noreorder", ".section .text", f"glabel {fn}"] + rows)
with tempfile.NamedTemporaryFile("w", suffix=".s", delete=False) as fh:
fh.write(body + "\n")
spath = fh.name
opath = spath[:-2] + ".o"
try:
r = subprocess.run(["mipsel-linux-gnu-as", "-I", os.path.join(REPO, "include"), "-march=r3000", "-mtune=r3000",
"-no-pad-sections", "-O1", "-G0", spath, "-o", opath], capture_output=True, text=True)
if r.returncode:
return None, (r.stderr or "").strip().splitlines()[:3]
sys.path.insert(0, os.path.join(REPO, "tools"))
import masked_diff
got = masked_diff.insns_from_object(opath, None)
finally:
for p in (spath, opath):
if os.path.exists(p):
os.unlink(p)
if len(got) != len(insns):
return None, [f"re-assembly produced {len(got)} instructions, the image has {len(insns)}"]
bad = []
for k, (g, (word, _text)) in enumerate(zip(got, insns)):
if g["reloc_kind"]:
continue
if g["word"] != word:
bad.append((k, word))
return bad, None
def emit(binary, fn, outdir, quiet=False, gas=False):
fr = _fr()
vaddr, nins = func_extent(binary, fn)
@@ -195,12 +232,34 @@ def emit(binary, fn, outdir, quiet=False, gas=False):
fh.write(f' * thing under test. {nins} instructions at 0x{vaddr:08X}. $-registers, .L<addr> labels for\n')
fh.write(f' * in-function targets, symbol names for external jumps; the listing carries its delay-slot nops\n')
fh.write(f' * (hence .set noreorder). Paste WHOLE into decomp.me — its PS1 prelude defines glabel. */\n\n')
rows = to_gas(insns, vaddr, off, _symtab(binary))
# the listing must RE-ASSEMBLE to the image's own words, or it is not a target (S99). Each row that does
# not is replaced by its `.word`, with the mnemonic kept in the comment for the reader; then it is
# verified again, and a listing that still disagrees is REFUSED rather than emitted (R43).
patched = 0
for _ in range(3):
bad, err = gas_roundtrip(rows, insns, fn)
if bad is None:
return None, f'{binary}:{fn}: the gas form does not assemble ({err})'
if not bad:
break
# `rows` interleaves `.L<addr>:` label lines with instructions; `bad` counts INSTRUCTIONS. Indexing rows
# by an instruction index rewrote a label into a comment and the next round refused to assemble.
ins_rows = [i for i, r in enumerate(rows) if r.startswith('/*')]
for k, word in bad:
head, _, text = rows[ins_rows[k]].partition('*/')
rows[ins_rows[k]] = f'{head}*/ .word 0x{word:08X} /* {text.strip()} */'
patched += len(bad)
else:
return None, (f'{binary}:{fn}: {len(bad)} instruction(s) still do not re-assemble to the image '
f'(first at +{bad[0][0] * 4:#x}) — REFUSING to emit a target that is not the bytes')
fh.write('.set noat\n.set noreorder\n\n.section .text\n\n')
fh.write(f'glabel {fn}\n')
for ln in to_gas(insns, vaddr, off, _symtab(binary)):
for ln in rows:
fh.write(ln + '\n')
if not quiet:
print(f' {binary:14s} {fn:26s} {nins:5d} ins @ 0x{vaddr:08X} -> {os.path.relpath(path, REPO)} (gas form)')
print(f' {binary:14s} {fn:26s} {nins:5d} ins @ 0x{vaddr:08X} -> {os.path.relpath(path, REPO)} (gas form'
+ (f', {patched} word-patched)' if patched else ')'))
return path, None
path = os.path.join(outdir, binary, f'{fn}.s')
with open(path, 'w') as fh:
@@ -233,7 +233,7 @@ def klass_for_fn(fn):
return "", ""
def setup(fn, draft_c, asm_subdir=ASM, klass=None, where="", outdir=None):
def setup(fn, draft_c, asm_subdir=ASM, klass=None, where="", outdir=None, target_o=None):
# `outdir` keys the scratch dir by the CALLER's identity instead of the bare function name (R48). A name is not
# unique across the fleet — the overlays overlap in RAM, so two different bodies are both `func_8013B274` — and two
# concurrent runs on one name would share (and corrupt) this directory. Default: the historical path, unchanged.
@@ -247,6 +247,22 @@ def setup(fn, draft_c, asm_subdir=ASM, klass=None, where="", outdir=None):
f"'not found in base.c' and no-op in 0s. Inspect {pd}/base.c "
f"(prep order: comments -> cpp macros -> M2C_FIELD -> hide_asm -> typedefs).")
open(f"{pd}/base.c", "w").write(base)
if target_o:
# THE TARGET AS AN OBJECT, not a listing (S99). Assembling a disassembly listing is a second toolchain with its
# own answers: objdump prints the pseudo-instruction `move` for `addu rX,rY,$zero` and gas assembles it as `or`
# (24 words wrong in one 234-instruction function), and a listing's %hi/%lo pairs come back RESOLVED, with no
# relocation, while every candidate carries one — and the masked scorer compares reloc operands. Both made the
# scorer report a nonzero floor for a body that IS byte-identical, so score 0 was unreachable and every verdict
# was the instrument's. A target object compiled from the tree's own body by THIS pipeline has the candidate's
# relocations by construction; the ROM listing stays the independent oracle that PROVES it (match_one MATCH
# before the search starts, R34/R56) — the proof is what keeps this from being circular.
shutil.copy(target_o, f"{pd}/target.o")
body, prof = permuter_weights.render_settings_toml(fn, klass=klass, where=where)
open(f"{pd}/settings.toml", "w").write(body)
csh = "compile_o0.sh" if asm_subdir.rstrip("/").endswith("_o0") else "compile.sh"
open(f"{pd}/compile.sh", "w").write(f'#!/bin/bash\nexec {REPO}/tools/permuter/{csh} "$@"\n')
os.chmod(f"{pd}/compile.sh", 0o755)
return pd
s = os.path.join(REPO, asm_subdir, fn + ".s")
tgt = f"{pd}/target.s"
with open(tgt, "w") as f:
@@ -41,12 +41,14 @@ def main():
ap.add_argument("--secs", type=int, default=180, help="per-cycle time box")
ap.add_argument("--j", type=int, default=12)
ap.add_argument("--winners", default=".run/permuter-winners")
ap.add_argument("--target-o", help="use this one-function object as the target instead of assembling "
"<asm-subdir>/<fn>.s (S99: a disassembly listing is a second toolchain)")
ap.add_argument("--pd", help="scratch dir for this run (default .run/permuter/<fn>) — key it by alias+fn when "
"several runs share a function NAME (R48)")
a = ap.parse_args()
draft = open(a.draft).read()
pd = P.setup(a.fn, draft, asm_subdir=a.asm_subdir, klass=a.klass, outdir=a.pd)
pd = P.setup(a.fn, draft, asm_subdir=a.asm_subdir, klass=a.klass, outdir=a.pd, target_o=a.target_o)
if not pd:
print("ILS setup FAILED (target .s didn't assemble?)"); sys.exit(1)
print(f"ILS {a.fn}: {a.cycles} cycles x {a.secs}s @ -j{a.j}, klass={a.klass}")
+4 -3
View File
@@ -8,8 +8,8 @@
> its platform SDK need the marked adaptation. The last table lists the tools that are project-only in code (their *shape* is a task;
> their code does not transfer). *TODO(platform): the MIPS and PlayStation SDK hard-codes are the ones another platform replaces first.*
>
> **Coverage:** 301 tool files in scope (submodules, vendored and downloaded code excluded), of which 301 live rows
> below; per phase: P1 2 · P2 26 · P3 17 · P4 9 · P5 26 · P6 51 · P7 20 · P8 86 · P9 27 · P10 21 · PROJECT-ONLY 16. Superseded tools appear only as pointers to their successor (30 pointer rows); one-offs are omitted. Table rows in all: 331 (the installer checks its copy against this figure).
> **Coverage:** 302 tool files in scope (submodules, vendored and downloaded code excluded), of which 302 live rows
> below; per phase: P1 2 · P2 26 · P3 17 · P4 9 · P5 26 · P6 51 · P7 20 · P8 86 · P9 27 · P10 22 · PROJECT-ONLY 16. Superseded tools appear only as pointers to their successor (30 pointer rows); one-offs are omitted. Table rows in all: 332 (the installer checks its copy against this figure).
## P1 — extraction + manifest
@@ -335,6 +335,7 @@
| `uniquify_type.py` | give each conflicting camp of a same-named type its own name | Gives each conflicting camp of a same-named type its own name so every camp becomes liftable | repo src layout |
| `verbatim_check.py` | guard that every inline-assembly body still reproduces its target bytes | Regression guard that every inline-assembly body still reproduces its target bytes | repo src layout |
| `delever_oracle.py` | judge one translation-unit edit by the bytes of its object in under a second | The Phase-36 fast byte oracle: every object's exact build command captured once via make -n -W (the Makefile's own recipe, pad stage and -O0 overrides included), a candidate compiled in place with -o/-MF redirected to scratch and compared with the fleet run's object; --calibrate proves 100 % equality untouched, twin == primary and a positive control before any verdict is trusted | the Makefile's object rules, the twin rule |
| `lever_progress.py` | keep the lever count as a series a chart and a story can be drawn from | The Phase-36 lever series: a milestone row appended per census snapshot (its totals by class with the tree's HEAD, since a census is a moment that cannot be recovered later) plus the campaign table derived from the de-lever ledger on every render — per batch the rungs used, the bodies first judged, the sites removed and rewritten, and, scored as a transition against what that body's previous row left, the sites a later rung closed and the bodies it made lever-free; renders both into docs/levers.md's generated block and --check refuses a series that is not this tree's | the census json, the delever ledger |
| `share_census.py` | measure duplicate function bodies across the fleet and assert one source per unique function | The census of byte-identical function classes across every binary with their source forms and verdicts, plus the S1 invariant check with its exception ledger and a fixture self-test | repo paths, the registry and signature schemas |
| `ghidra_apply_symbols.sh` | mirror the curated symbol file into the analysis database with a real save | Mirrors the curated symbol file into the analysis program headlessly, with a real save | repo symbol path, project name |
| `frozen.py` | refuse, from one place, the command line of a tool the project has frozen | The one refusal a FROZEN tool prints from its main(): the tool, the successor and why; imports never exit (libraries stay usable) | nothing |
@@ -343,7 +344,7 @@
| `share_body_cycle.sh` | run the share-body batch cycle unattended: batch, verify by exit code, log, commit, periodic clean fleet check | The Phase-35 T5 driver for bucket new: per batch share_body --apply --bucket new --batches 1 --label new<k>, the gated N/N line read, the phase log entry appended, the bank committed the moment it is green, the clean fleet run every N batches — stops on the first red | repo paths, the phase-log format, the fleet count from config/check.*.sha |
| `delever_permute.py` | search the compiler's own shape space for a lever-free body that keeps the bytes | The Phase-36 rung D: one exemplar per residue text class from the delever ledger (largest class first — a match banks every copy), each prepared as a single-function translation unit (delever's rung-A rewrite of every removable site, every other definition reduced to a prototype, shared-header includes to their prototypes, INCLUDE_ASM and file-scope asm dropped, the build's own CPPFLAGS through cpp -P) against a target regenerated from the ROM image in both the assemblable and the splat form; every attempt calibrates itself first (the LEVERED body must be MATCH, or the harness is not measuring that function) and records the lever-free body's starting distance; decomp-permuter through permuter_ils with the weight profile chosen from the NEEDED kinds; a score-0 winner is banked only through delever --apply-body and the GTE re-fold; scratch, winners and the outcome ledger keyed by alias+fn | the ledger, the permuter harness, the target regenerator |
| `share_body.py` | share one byte-identical function class across its binaries through an include-at-site header, gated per binary | The Phase-35 successor of dedup_propagate + dedup_extend for the include-at-site form: exemplar by majority text, the header written once, every private copy replaced by the include at its position, per-binary byte gate with object comparison, bisect on red, the registry appended or extended by text, the exception ledger on refusal; --plan / --apply --bucket extend|new | repo paths, the registry and signature schemas |
| `delever.py` | take register pins, asm statements, volatile and register levers out of matched C, byte-gated per site | The Phase-36 de-lever engine and campaign tool: positional rewrites per lever class (pin -> plain declaration, barrier/keep-alive deleted, a launder deleted or turned into the assignment it is, a hand-placed instruction -> its C, the zero-register variable -> 0, a macro-carried site deleted/valued/refused by its macro's shape, volatile/register dropped); per body replay -> rung A strip-all -> rung B greedy through delever_oracle; the file is the write unit and its final compile through every recipe the proof; --plan/--apply batches (TUs parallel, exemplar files first; headers serial, includers parallel), the ledger keyed by tu+fn+addr with the body's nhash before/after, !FAKE markers on class A/B survivors, --redraw for bodies refused by an older tool, --restore from inflight.json, --scrub for orphan markers, --apply-body for T6/T7, --selftest with a stub oracle, --probe (T2) | the census site records, the oracle |
| `delever.py` | take register pins, asm statements, volatile and register levers out of matched C, byte-gated per site | The Phase-36 de-lever engine and campaign tool: positional rewrites per lever class (pin -> plain declaration, barrier/keep-alive deleted, a launder deleted or turned into the assignment it is, a hand-placed instruction -> its C, the zero-register variable -> 0, a macro-carried site deleted/valued/refused by its macro's shape, volatile/register dropped); per body replay -> rung A strip-all -> rung B greedy through delever_oracle; the file is the write unit and its final compile through every recipe the proof; --plan/--apply batches (TUs parallel, exemplar files first; headers serial, includers parallel), the ledger keyed by tu+fn+addr with the body's nhash before/after, !FAKE markers on class A/B survivors, --redraw for bodies refused by an older tool, --restore from inflight.json, --scrub for orphan markers, --apply-body for T6/T7, --recipes (rung R: the byte-neutral shape recipes — the formerly-pinned declarations permuted, one moved through the declaration run, an initializer split — each judged by the oracle, with an identity control on both the splice and the oracle), --selftest with a stub oracle, --probe (T2) | the census site records, the oracle |
| `verbatim_to_stub.py` | turn an inline-assembly body back into a stub the toolchain can reach | Turns an inline-assembly body back into an include-assembly stub | repo src/asm layout |
## PROJECT-ONLY — project-only in code (the shape is a task; the code does not transfer)
+6 -5
View File
@@ -5,7 +5,7 @@ freshness). The derived columns come from the tree on every run; the authored on
dictionary, whose coverage is asserted both ways. Read it by NEED: find the phrase that matches what you are trying to do, then the tool,
then what proved it. The same data generates the day-one kit's manifest and its verbatim tool corpus.*
**Coverage:** 301 tool files in scope (submodules, vendored and downloaded code excluded; `find` and `git ls-files` agree) + 38 retired under `tools/sunset/`. Classes: LIVE 241 (a runtime consumer), REFERENCED 30 (a SETUP row only), ORPHAN 30 (neither) — of 301. Portability: PORTABLE 23, ADAPT 261, PROJECT-ONLY 17.
**Coverage:** 302 tool files in scope (submodules, vendored and downloaded code excluded; `find` and `git ls-files` agree) + 38 retired under `tools/sunset/`. Classes: LIVE 241 (a runtime consumer), REFERENCED 31 (a SETUP row only), ORPHAN 30 (neither) — of 302. Portability: PORTABLE 23, ADAPT 262, PROJECT-ONLY 17.
## P1 — extraction + manifest
@@ -71,7 +71,7 @@ then what proved it. The same data generates the day-one kit's manifest and its
| When you need to… | Tool | What it does | Proven by | Adapt | Class |
|---|---|---|---|---|---|
| compare instructions with relocations masked, shared by matcher and scorer | `masked_diff.py` | Shared relocation-masked instruction comparison used by the matcher and the permuter scorer | diff_regions.py, masked_scorer.py, match_one.py, reg_renumber_swap.sh (+6) | MIPS relocation encodings | LIVE |
| compare instructions with relocations masked, shared by matcher and scorer | `masked_diff.py` | Shared relocation-masked instruction comparison used by the matcher and the permuter scorer | diff_regions.py, masked_scorer.py, match_one.py, reg_renumber_swap.sh (+7) | MIPS relocation encodings | LIVE |
| compile one function standalone and compare its masked bytes to the target | `match_one.py` | Compiles one function standalone with the pinned toolchain, masks relocations, compares to target bytes | ab_score.py, api_draft.py, aprop_autodraft.py, asm_verbatim.py (+23) | compiler triple, repo build flags | LIVE |
| dump every compiler pass file for a self-contained draft | `cc1_dumps.sh` | Dumps every compiler pass file for a self-contained draft into a private directory | cc1_dumps_tu.sh, ghost_census.py | compiler triple, repo scratch paths | LIVE |
| dump every compiler pass file for the spliced real translation unit | `cc1_dumps_tu.sh` | Same pass dumps for the spliced real translation unit, the faithful compile | alloc_table.py | absolute repo path, compiler triple | LIVE |
@@ -295,7 +295,7 @@ then what proved it. The same data generates the day-one kit's manifest and its
| build a progress timeline from the repository's own committed digests | `timeline.py` | Builds a progress timeline from the repository's own committed digests, with a self-check | Makefile | repo doc paths | LIVE |
| check that every relative link in the public docs resolves | `doc_links.py` | Checks that every relative link in the public docs resolves and the link policy holds | Makefile, wiki_render.py, wiki_sync.sh | repo doc paths | LIVE |
| compile every eligible translation unit with the pinned toolchain, using no derived bytes | `compile_only.py` | Compiles every eligible translation unit with the pinned toolchain, without any game bytes | .github/workflows/no-rom.yml, delever_oracle.py, lever_census.py, macro_to_header.py (+1) | compiler triple, repo makefile parsing | LIVE |
| compute and publish progress metrics as report, machine data and per-binary breakdowns | `progress.py` | Computes and publishes the progress metrics as report, JSON and per-binary breakdowns | Makefile, docs/wave-playbook.md, api_agent.py, atlas.py (+33) | repo src/config layout | LIVE |
| compute and publish progress metrics as report, machine data and per-binary breakdowns | `progress.py` | Computes and publishes the progress metrics as report, JSON and per-binary breakdowns | Makefile, docs/wave-playbook.md, api_agent.py, atlas.py (+34) | repo src/config layout | LIVE |
| convert progress metrics into an external progress-report schema | `objdiff_report.py` | Converts the progress JSON into the external progress-report schema | .github/workflows/progress.yml | external report schema | LIVE |
| emit the public ordinal-to-hash map and the private old-to-new map | `public_rewrite/build_commit_map.py` | Emits the public ordinal-to-new-hash map and the private old-to-new map | probe_github.sh, resolve_tokens.py | repo doc/scratch paths | LIVE |
| gate a first push: prove no derived bytes among tracked files | `audit_public.py` | The first-push gate: no derived bytes among tracked files, four independent checks | .github/workflows/no-rom.yml, gate_scan.py | repo purge-set and path lists (the kit's template reads `config/firewall.txt` instead) | LIVE |
@@ -322,7 +322,7 @@ then what proved it. The same data generates the day-one kit's manifest and its
|---|---|---|---|---|---|
| add per-site function-pointer casts so a draft can call a differently typed callee | `cast_call_sites.py` | Adds per-site function-pointer casts so a draft can call a differently typed callee | canon_sig_reconcile.py, cast_self_callers.py, family_sweep.py, gate_stage.py (+4) | repo src layout | LIVE |
| apply curated names and signatures inside the analysis tool and save | `ghidra_scripts/ApplySymbols.java` | The in-tool half of that mirror: apply curated names and signatures, save on exit | ghidra_apply_symbols.sh, ghidra_export_annotations.sh, ghidra_rebuild.sh, ExportAnnotations.java (+1) | none | LIVE |
| census every compiler-forcing construct in the C and gate the levers-off phase | `lever_census.py` | The Phase-36 census of lever sites (register pins, asm statements by kind, volatile levers, bare register, asm-label aliases, builtins, attributes; the whole-body hand-asm routines and the GTE ops set apart) derived from the shared-body scanner with a per-token coverage assertion against the raw text, four known-true controls, the !FAKE marker split, --check (0 unmarked pins/asm, 0 orphan markers) and --strict (0 pins, 0 asm) gates, --sites for the delever ledger, a fixture self-test | delever.py, delever_cycle.sh, delever_permute.py, gte_consolidate.py (+2) | repo paths, the shared-body scanner | LIVE |
| census every compiler-forcing construct in the C and gate the levers-off phase | `lever_census.py` | The Phase-36 census of lever sites (register pins, asm statements by kind, volatile levers, bare register, asm-label aliases, builtins, attributes; the whole-body hand-asm routines and the GTE ops set apart) derived from the shared-body scanner with a per-token coverage assertion against the raw text, four known-true controls, the !FAKE marker split, --check (0 unmarked pins/asm, 0 orphan markers) and --strict (0 pins, 0 asm) gates, --sites for the delever ledger, a fixture self-test | delever.py, delever_cycle.sh, delever_permute.py, gte_consolidate.py (+3) | repo paths, the shared-body scanner | LIVE |
| consolidate the GTE coprocessor inline-asm macros under Sony's names in one header and sweep dead lever macros | `gte_consolidate.py` | The Phase-36 T5 tool: every asm-bearing macro definition and direct GTE statement SIGNED by the build's own maspsx→as tail (template bytes with operands bound to fixed registers, operand counts, clobbers); one canonical text per signature named by PsyQ's inline_c.h convention (Sony's file supplies names and clobber lists, never opcode words) written to include/gte_inline.h; per file the canonical duplicates deleted, private names renamed, a variant with extra clobbers tried as canonical and kept as a marked <name>_m lever only when the object differs, direct statements rewritten into canonical calls (a concatenation of two included); every file judged through every recipe; --sweep deletes dead asm-bearing macros and drops a compound macro's inner asm when byte-neutral | delever.py, delever_cycle.sh, delever_permute.py, lever_census.py | Sony's inline_c.h path, the census sites, the oracle | LIVE |
| convert a shared-body macro header into per-function plain-C headers included at each site | `macro_to_header.py` | The Phase-35 converter: every DEFINE_ macro body becomes a plain-C header under src/shared/<space>/ included at its site, the legacy name-parameterized headers converted, the registry text-edited, the macro header deleted; --plan / --apply / --finalize / --verify | share_body.py, tool_census.py | the macro form is this project's; a kit-born project shares headers from its first bank | LIVE |
| emit a function's target assembly as an inline-assembly body | `asm_verbatim.py` | Emits the file-scope inline-assembly body form from a disassembly file | docs/wave-playbook.md, draft_prechecks.py, gate_main.py, recover_route.py | repo asm layout | LIVE |
@@ -331,6 +331,7 @@ then what proved it. The same data generates the day-one kit's manifest and its
| give each conflicting camp of a same-named type its own name | `uniquify_type.py` | Gives each conflicting camp of a same-named type its own name so every camp becomes liftable | — | repo src layout | REFERENCED |
| guard that every inline-assembly body still reproduces its target bytes | `verbatim_check.py` | Regression guard that every inline-assembly body still reproduces its target bytes | .github/workflows/no-rom.yml, lever_census.py, progress.py, verbatim_target_s.py (+1) | repo src layout | LIVE |
| judge one translation-unit edit by the bytes of its object in under a second | `delever_oracle.py` | The Phase-36 fast byte oracle: every object's exact build command captured once via make -n -W (the Makefile's own recipe, pad stage and -O0 overrides included), a candidate compiled in place with -o/-MF redirected to scratch and compared with the fleet run's object; --calibrate proves 100 % equality untouched, twin == primary and a positive control before any verdict is trusted | delever.py, delever_cycle.sh, delever_permute.py, gte_consolidate.py | the Makefile's object rules, the twin rule | LIVE |
| keep the lever count as a series a chart and a story can be drawn from | `lever_progress.py` | The Phase-36 lever series: a milestone row appended per census snapshot (its totals by class with the tree's HEAD, since a census is a moment that cannot be recovered later) plus the campaign table derived from the de-lever ledger on every render — per batch the rungs used, the bodies first judged, the sites removed and rewritten, and, scored as a transition against what that body's previous row left, the sites a later rung closed and the bodies it made lever-free; renders both into docs/levers.md's generated block and --check refuses a series that is not this tree's | — | the census json, the delever ledger | REFERENCED |
| measure duplicate function bodies across the fleet and assert one source per unique function | `share_census.py` | The census of byte-identical function classes across every binary with their source forms and verdicts, plus the S1 invariant check with its exception ledger and a fixture self-test | Makefile, audit_binaries.py, dedup_integrate.py, delever.py (+10) | repo paths, the registry and signature schemas | LIVE |
| mirror the curated symbol file into the analysis database with a real save | `ghidra_apply_symbols.sh` | Mirrors the curated symbol file into the analysis program headlessly, with a real save | — | repo symbol path, project name | REFERENCED |
| refuse, from one place, the command line of a tool the project has frozen | `frozen.py` | The one refusal a FROZEN tool prints from its main(): the tool, the successor and why; imports never exit (libraries stay usable) | aprop_autodraft.py, blocker_probe.py, canon_sig_reconcile.py, conform_decls.py (+10) | nothing | LIVE |
@@ -339,7 +340,7 @@ then what proved it. The same data generates the day-one kit's manifest and its
| run the share-body batch cycle unattended: batch, verify by exit code, log, commit, periodic clean fleet check | `share_body_cycle.sh` | The Phase-35 T5 driver for bucket new: per batch share_body --apply --bucket new --batches 1 --label new<k>, the gated N/N line read, the phase log entry appended, the bank committed the moment it is green, the clean fleet run every N batches — stops on the first red | — | repo paths, the phase-log format, the fleet count from config/check.*.sha | ORPHAN |
| search the compiler's own shape space for a lever-free body that keeps the bytes | `delever_permute.py` | The Phase-36 rung D: one exemplar per residue text class from the delever ledger (largest class first — a match banks every copy), each prepared as a single-function translation unit (delever's rung-A rewrite of every removable site, every other definition reduced to a prototype, shared-header includes to their prototypes, INCLUDE_ASM and file-scope asm dropped, the build's own CPPFLAGS through cpp -P) against a target regenerated from the ROM image in both the assemblable and the splat form; every attempt calibrates itself first (the LEVERED body must be MATCH, or the harness is not measuring that function) and records the lever-free body's starting distance; decomp-permuter through permuter_ils with the weight profile chosen from the NEEDED kinds; a score-0 winner is banked only through delever --apply-body and the GTE re-fold; scratch, winners and the outcome ledger keyed by alias+fn | — | the ledger, the permuter harness, the target regenerator | REFERENCED |
| share one byte-identical function class across its binaries through an include-at-site header, gated per binary | `share_body.py` | The Phase-35 successor of dedup_propagate + dedup_extend for the include-at-site form: exemplar by majority text, the header written once, every private copy replaced by the include at its position, per-binary byte gate with object comparison, bisect on red, the registry appended or extended by text, the exception ledger on refusal; --plan / --apply --bucket extend|new | aprop_autodraft.py, audit_binaries.py, auto_driver.py, blocker_probe.py (+18) | repo paths, the registry and signature schemas | LIVE |
| take register pins, asm statements, volatile and register levers out of matched C, byte-gated per site | `delever.py` | The Phase-36 de-lever engine and campaign tool: positional rewrites per lever class (pin -> plain declaration, barrier/keep-alive deleted, a launder deleted or turned into the assignment it is, a hand-placed instruction -> its C, the zero-register variable -> 0, a macro-carried site deleted/valued/refused by its macro's shape, volatile/register dropped); per body replay -> rung A strip-all -> rung B greedy through delever_oracle; the file is the write unit and its final compile through every recipe the proof; --plan/--apply batches (TUs parallel, exemplar files first; headers serial, includers parallel), the ledger keyed by tu+fn+addr with the body's nhash before/after, !FAKE markers on class A/B survivors, --redraw for bodies refused by an older tool, --restore from inflight.json, --scrub for orphan markers, --apply-body for T6/T7, --selftest with a stub oracle, --probe (T2) | delever_cycle.sh, delever_permute.py, gte_consolidate.py, lever_census.py | the census site records, the oracle | LIVE |
| take register pins, asm statements, volatile and register levers out of matched C, byte-gated per site | `delever.py` | The Phase-36 de-lever engine and campaign tool: positional rewrites per lever class (pin -> plain declaration, barrier/keep-alive deleted, a launder deleted or turned into the assignment it is, a hand-placed instruction -> its C, the zero-register variable -> 0, a macro-carried site deleted/valued/refused by its macro's shape, volatile/register dropped); per body replay -> rung A strip-all -> rung B greedy through delever_oracle; the file is the write unit and its final compile through every recipe the proof; --plan/--apply batches (TUs parallel, exemplar files first; headers serial, includers parallel), the ledger keyed by tu+fn+addr with the body's nhash before/after, !FAKE markers on class A/B survivors, --redraw for bodies refused by an older tool, --restore from inflight.json, --scrub for orphan markers, --apply-body for T6/T7, --recipes (rung R: the byte-neutral shape recipes — the formerly-pinned declarations permuted, one moved through the declaration run, an initializer split — each judged by the oracle, with an identity control on both the splice and the oracle), --selftest with a stub oracle, --probe (T2) | delever_cycle.sh, delever_permute.py, gte_consolidate.py, lever_census.py | the census site records, the oracle | LIVE |
| turn an inline-assembly body back into a stub the toolchain can reach | `verbatim_to_stub.py` | Turns an inline-assembly body back into an include-assembly stub | — | repo src/asm layout | REFERENCED |
## PROJECT-ONLY — project-only in code (the shape is a task; the code does not transfer)
+78 -2
View File
@@ -62,7 +62,7 @@
- Rules check (P6) after T4.
- ☐ **T5** — GTE consolidation (one header, PsyQ's names; the `"memory"`-clobber variants are levers → residue, not a second spelling) +
the dead lever-macro sweep (`SHB` cross-file use counted). Verify: `lever_census` 0 per-TU asm macro definitions; R22 green.
- ☐ **T6** — Rung R (mechanical shape recipes, each negative-controlled) and rung D (the permuter on the residue exemplars, time-boxed,
- ☑ **T6** — Rung R (mechanical shape recipes, each negative-controlled) and rung D (the permuter on the residue exemplars, time-boxed,
16 workers; a score-0 applied through `delever --apply-body` and gated). Verify: `recipes: <k> of <r> NEEDED sites removed`;
`permuter: <k> of <r> exemplars matched lever-free in <t> h`.
- ☐ **T7** (Ultracode, prompted) — The reshaping waves to zero: wave → harvest → toolify (cookbook + rung R) → sweep → next draw; wave 1
@@ -621,7 +621,83 @@ accumulate here as the phase produces them.**
refused more than a third of the population while the context was hot, and harvest→toolify applied to lever classes so a shape
sweeps the banked population instead of the lever propagating with every family remap (1,758 distinct bodies became 12,578).
## 🛑 SESSION CHECKPOINT — S98 (2026-09-09): T0 ☑ T1 ☑ T1b ☑ T2 ☑ T3 ☑ T4 ☑ T5 ☑ — all committed (this close on top of `4bc987426`); NEXT = T6 BUILD: `tools/delever_permute.py` per §2 (the design is settled and its pipeline proven by two probes — the T6 log entry); S98 ended here at 90 % context | the number: 34,091 sites in 12,712 bodies (1,759 distinct) · marked 34,091 · UNMARKED 0 · orphans 0 · GTE levers 462 · per-TU asm macro definitions 314 (0 canonical duplicates) — `lever_census --check` OK
- **S99 — T6 CLOSE: both yield lines measured, 664 lever sites gone, R22 green at every step. T6 ☑.**
**The two lines the plan asks for (R41):** `permuter: 5 of 16 exemplars matched lever-free in 0.69 h (665 of 2,131 bodies
behind them)` and `recipes: 134 of 134 bodies closed lever-free (134 NEEDED sites in the 134 drawn), 3243 compiles in 6.0 min`.
**Banked this session:** rung R's batch `r1` (134 bodies, every one `R7 do-while @<line>`, a ONE-LINE source change each) ·
rung D's four remaining exemplars (`d1`, tidied and judged) · the propagation `p1`/`p1b` (132 + 130 + 131 + 132 = **525
siblings**, 0 refused, each judged on its own objects). **34,091 → 33,427 sites** (19,982 pins + 13,445 asm) in 12,048 bodies,
33,427 marked, 0 UNMARKED, 0 orphans; `check-all: 218 passed, 0 failed of 218` after every batch.
**What T6 leaves for T7, stated rather than smoothed over:** rung R's recipe set does NOT reproduce the shape rung D found on
func_80163EC8 — R6 was generalised from "assigned once, read once" to "dead after one read" (the lever was one of two
assignments to `uVar5`) and still does not close it (154 candidates, 1.3 min, 0 hits), so that class was banked from the
permuter's own winner instead. The 11 exemplars rung D did not close are seeds, not walls: every one improved (78→9, 52→3,
50→3, 37→2, 131→35) and the ones that stayed far all removed a hand-placed `instruction` whose C spelling changed the
instruction COUNT. A fleet-wide rung-R sweep is priced but not run: one body costs ~150 compiles / ~1.3 min at `--cap 200`
when nothing hits, so the 12,000-body residue needs the run parallelised across TUs before it is affordable.
**Instrument work banked with it:** `--recipes` is killable (its judge snapshots to inflight.json first — a killed run had
left a candidate in `src/`); `--bank` skips a body another rung already closed (it had re-applied a permuter body over rung
R's cleaner one-line version, restored through the oracle as `d1fix`); `--propagate` keys the class on the FIRST bank in a
body's chain (the later row's before-hash describes a text only that body ever had — func_80163EC8 found 0 siblings until
this was fixed, then 132); the permuter's winner is TIDIED before it is offered (pycparser's two-space indent and the corpse
`;` where a statement was inlined away), with the tidy judged like any other candidate.
## 🛑 SESSION CHECKPOINT — S99 (2026-09-09): T0–T5 ☑ **T6 ☑** — all committed (this close on top of `1c2355054`); NEXT = **T7, the reshaping waves — and T7 STARTS ONLY ON DREW'S DIRECT APPROVAL IN THE SESSION THAT RUNS IT** (his words, twice: the `/effort ultracode` toggle is NOT approval); until he gives it, the drawable work is a PARALLELISED rung-R sweep (§2) | the number: **33,427 sites** (19,982 pins + 13,445 asm) in 12,048 bodies · marked 33,427 · UNMARKED 0 · orphans 0 · GTE levers 462 — `lever_census --check` OK · `lever_progress --check` OK (3 milestones)
### 0. How to use this block
A fresh session reads CLAUDE.md's load order, replays this block verbatim, confirms the effort (T7's coordinator at Max, the waves at
Ultracode — Drew toggles, R27) and executes §2. The tree is clean at HEAD = this close commit. **Never run a long job as a harness
background task** (the low-memory guard kills them; the harness also backgrounds any foreground command over 120 s, which is fine for
`make` but not for a campaign) — run campaigns DETACHED with `setsid nohup … &` and a `Monitor` on the log. **The calibration is keyed to
HEAD: EVERY commit stales it** — `tools/delever_oracle.py --calibrate ov_SC04_011 ov_SC03_015 ov_SC03_014 main -j 16` (3 s) before any
judging, and calibrate the FULL set: a single-alias calibration fails its own completeness check. A killed batch: `tools/delever.py
--restore`.
### 1. Where things stand
- **Done: T0–T6.** T6 delivered both yield lines the plan asks for: `permuter: 5 of 16 exemplars matched lever-free in 0.69 h (665 of
2,131 bodies behind them)` and `recipes: 134 of 134 bodies closed lever-free (134 NEEDED sites in the 134 drawn), 3243 compiles in
6.0 min`. Banked: rung R `r1` (134 bodies) · rung D `d1` (4 exemplars) · propagation `p1`/`p1b` (525 siblings, 0 refused).
**34,091 → 33,427 sites**; `check-all: 218 passed, 0 failed of 218` after every batch.
- **THE INSTRUMENT LESSON THAT COST TWO CAMPAIGNS** (cookbook §454, accelerators P36 S99): the permuter's target had been ASSEMBLED
FROM A DISASSEMBLY LISTING and scored **28 for a byte-identical body**, so score 0 was unreachable and two campaigns reported "0 of
16" about a healthy population. The target is now the tree's OWN body compiled by the build's tail (`--positive-control` reads base
score 0). **Run `tools/delever_permute.py --positive-control TU FN` before believing any future yield.**
- **Instruments (SETUP + dictionary rows, kit corpus regenerated, `tool_census --check: OK`):** `tools/delever.py` (+ `--recipes`
rung R with R2/R3/R4/R5/R6/R7, `--propagate TU FN`, `--cap`, `--control`), `tools/delever_permute.py` (`--plan`, `--prepare`,
`--positive-control`, `--calibrate`, `--run`, `--bank`, `--status`, `--selftest`), `tools/lever_progress.py` (the series behind
`docs/levers.md`), `tools/delever_oracle.py`, `tools/lever_census.py`, `tools/gte_consolidate.py`, `tools/delever_cycle.sh`.
- **Drew's directive, mid-session:** the lever count over time is a DELIVERABLE — `docs/levers.md` + `docs/lever-progress.tsv` +
`tools/lever_progress.py --snapshot "<task>"` **after every task that changes the count** (the post-100% chart, the story, a wiki
page, and the kit's day-one rule). §5 of that document is the prevent-vs-defer argument, written from the generated numbers:
**38% of the class A/B population came off with no understanding at all**, so the rule is *ban the silence, not the lever*.
- **Environment:** WSL2, `~/bfm-decomp`, `.venv`; R22 clean fleet ≈ 115 s; the census ≈ 35 s; `make kit-corpus` ≈ 25 s; 16 cores.
### 2. What is drawable WITHOUT Drew's wave approval — the parallelised rung-R sweep
Rung R is serial today: one body costs ~150 compiles / ~1.3 min at `--cap 200` when nothing hits, so the ~12,000-body residue is
unaffordable as written. Make `recipes()` run TU-parallel the way `apply_batch` already does (a `ThreadPoolExecutor` over files, one
worker owning a whole TU — two workers must never share a TU, and the oracle writes the candidate into the tree to compile it), then:
1. `--cap 40` first over the whole residue (the targeted recipes R5/R6 and a few R7s) to measure the cheap yield with a denominator;
2. `--cap 200` on what the cheap pass leaves, drawn by class size so a hit is worth 130 bodies;
3. after each batch: R22 → `lever_census --sites --check` → `lever_progress --snapshot "<label>"` → commit (R42, R101).
Also drawable: more rung-D exemplars (`delever_permute --run --limit N --workers 4 --secs 240 --cycles 3`), ordered by the STARTING
DISTANCE the calibration prints — everything that closed started ≤ 29 mismatched instructions, everything ≥ 37 did not; `--max-start 35`
triages the rest as FAR with their number.
### 3. T7, when Drew approves it
One agent per residue exemplar with the pack (the body, the target listing, the ledger's NEEDED sites with the register each names,
`alloc_table.py` on the real TU, §501-E/P/R + §17 + **§454**, `journal_notes`, "no lever of any class may remain", deliverable-first
R67); `delever --apply-body` judges every draft on the bytes and `--propagate` spreads it to the class. The harvest → toolify gate
between waves is not optional: every shape an agent finds becomes a rung-R recipe (R5/R6/R7 all came from rung D this way) and then
sweeps the population for free.
### 4. Gotchas known before any work
Everything in the T3/T4/T5 lists, plus: a disassembly listing is not a target (§454); `--only <fn>` in `--recipes` draws EVERY body of
that name across the fleet, which is why `r1` banked a whole class in one run; the class key for `--propagate` is the FIRST bank in a
body's chain; `pgrep -cf` matching your own command line counts itself (R79's cousin); the harness backgrounds any command over 120 s;
`lever_progress --check` fails when the series is not this tree's.
## (superseded) SESSION CHECKPOINT — S98 (2026-09-09): T0 ☑ T1 ☑ T1b ☑ T2 ☑ T3 ☑ T4 ☑ T5 ☑ — all committed (this close on top of `4bc987426`); NEXT = T6 BUILD: `tools/delever_permute.py` per §2 (the design is settled and its pipeline proven by two probes — the T6 log entry); S98 ended here at 90 % context | the number: 34,091 sites in 12,712 bodies (1,759 distinct) · marked 34,091 · UNMARKED 0 · orphans 0 · GTE levers 462 · per-TU asm macro definitions 314 (0 canonical duplicates) — `lever_census --check` OK
### 0. How to use this block
A fresh session reads CLAUDE.md's load order, replays this block verbatim, confirms the effort (T6/T8/T9 xHigh per the plan; S98 ran at Max —
+7 -3
View File
@@ -1705,10 +1705,14 @@ def propagate(a):
shape, with its own addresses, and judge each on its own objects."""
tu, fn = a.propagate
rows = load_ledger()
src_row = next((r for r in reversed(rows) if r.get("tu") == tu and r.get("fn") == fn
and r.get("verdict") == "LEVER-FREE" and r.get("after_text")), None)
if src_row is None:
chain = [r for r in rows if r.get("tu") == tu and r.get("fn") == fn
and r.get("verdict") == "LEVER-FREE" and r.get("after_text")]
if not chain:
sys.exit(f"delever --propagate: no banked reshape of {tu}:{fn} in the ledger (its row must carry after_text)")
# THE CLASS is what the body looked like when the campaign found it, so the key and the "before" text come from the
# FIRST bank in this body's chain; the text to spread is the LAST one (a body reshaped, then tidied, has two rows, and
# taking the last row's before-hash would look for siblings of a text only this body ever had).
src_row = dict(chain[-1], nhash_before=chain[0]["nhash_before"], before_text=chain[0]["before_text"])
key = src_row["nhash_before"]
cur = {}
for r in rows: