fix(phase-30 S1e): the distinct-code "regression" was a STALE DIGEST — alias lever ungated

The S38 checkpoint gated the phase's best lever ("do NOT scale the alias lever") on
distinct-code falling 89.3 -> 89.2. It never fell.

PROOF (each commit's metric recomputed from its OWN committed tree, 0 unresolved):
  commit:1426 TRUE     : instr 12394533  distinct 5022306  (77895 uniq)
  commit:1426 COMMITTED: instr 12402412  distinct 5029324  (78025 uniq)   <- stale
  HEAD TRUE == COMMITTED: instr 12405402  distinct 5025082  (77952 uniq)
  => true delta 843->HEAD: instr +10869, distinct +2776 ins / +57 uniq. ALL ROSE.
The 843 digest was generated from a working tree still holding work REVERTED before the
commit landed (+7,879 ins / +130 uniq overstated) and never regenerated, so the next
HONEST digest read as a fall. => THE ALIAS LEVER IS UNGATED (scale it, §61 small batches).

Both recorded leads were wrong (R14): progress.py:423's SIG regex feeds fn-count ONLY
(neither weighted metric sees a C identifier — both derive matched = sig - corpus.stubs),
and "the harvest reverted functions to INCLUDE_ASM" died on one grep (483 removed, 0 added).
The 3-grep proof: identical sigs + unchanged tools/ + zero +INCLUDE_ASM => HEAD's stub set
is a strict subset => both numerators are FORBIDDEN to fall.

THREE INSTRUMENT DEFECTS, all one class (a bare except around a fail-CLOSED oracle):
- progress.py stub_addrs wrapped corpus.stubs in `except Exception: return set()`. An empty
  stub set means "could not answer", not "no stubs", so matched = sig - stubs credited EVERY
  function. Byte-witnessed: instr 100.00% / distinct 100.00% in a tree with no asm/. Now
  propagates.
- cast_call_sites.tu_for + reconcile_tu.tu_for had the identical swallow, falling back to the
  default <ov>.c instead of the jr/-O0 split TU — silently reinstating the exact bug
  cast_call_sites' own docstring says it exists to fix. A wrong-TU reconcile fails the gate,
  and this phase's base rate is ~24k PLUMBING vs 4,917 DIFF, so it presents as a codegen wall.
  Now propagate CorpusError; ValueError fallback for curated names preserved; derived-TU path
  re-verified (a _jr_ split stub resolves correctly, both tools agree).

NEW GATE (R34 — the byte-gate is a null oracle for DOCUMENTS; check-all stays 140/140 over a
stale digest forever): tools/audit_digest.py + `make audit-digest`, wired into tools-health
after report. Recomputes the three headline metrics from the current tree and fails if the
committed digest disagrees. Compares INTEGERS, not percentages — the +7,879-instruction
staleness printed as "94.4%" on both sides. Negative-control-proven against the stale 843
digest (fails, exit 1) and green on HEAD.

Verified: make report exit 0 (dedup-check 1910 validated / 0 failed, C1 coverage
241216/241216); audit-digest OK; cookbook-index OK (398 sections); metrics unchanged by the
fix (94.40% / 89.18%). No src/ or config/ edits — no bytes touched, nothing banked.

cookbook §140 · decision-log 2026-08-04 · SETUP.md inventory (R21) · R14/R32/R34/R35.
This commit is contained in:
Drew T
2026-08-04 21:48:57 -06:00
parent d8016c49c8
commit 1576570271
10 changed files with 347 additions and 32 deletions
+12 -1
View File
@@ -140,7 +140,7 @@ CC1_SMOKE_FLAGS := -quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-lin
BINUTILS_WARN_MAJOR := 2
BINUTILS_WARN_MINOR := 38
.PHONY: help check-env extract build check expected clean report sig-refresh sig-overlays sig-resident build-all check-all audit-corpus audit-cdecl audit-binaries audit-text-sources tools-health
.PHONY: help check-env extract build check expected clean report sig-refresh sig-overlays sig-resident build-all check-all audit-corpus audit-cdecl audit-binaries audit-text-sources audit-digest tools-health
# -----------------------------------------------------------------------------
help:
@@ -193,6 +193,14 @@ audit-cdecl:
audit-binaries:
$(VENV_PY) tools/audit_binaries.py
# P30 S1e: the committed fleet digest must still describe the CURRENT tree. A digest generated from
# a working tree that later changed (work reverted before the commit landed) is BYTE-INVISIBLE —
# check-all stays 140/140 over it — and the next honest regeneration then reads as a REGRESSION that
# never happened. That cost a session-opening false alarm and gated the best-performing lever on a
# phantom. R34: the byte-gate is a null oracle for documents, so this is a second one that disagrees.
audit-digest:
$(VENV_PY) tools/audit_digest.py
# P30 S28: every tracked C source must be TEXT. A raw NUL inside a char literal (`'<NUL>'` instead
# of `'\0'`) COMPILES — the fleet stayed byte-identical — but grep treats the file as BINARY and
# reports nothing, silently, so the file vanishes from every grep-based audit and hand-search. The
@@ -221,6 +229,9 @@ tools-health:
$(MAKE) --no-print-directory audit-binaries
$(MAKE) --no-print-directory audit-text-sources
$(MAKE) --no-print-directory report BINARY=main
# AFTER report (which regenerates the digest), so this asserts the freshly-written digest agrees
# with the tree — and, on a tree whose digest was committed stale, says so instead of staying green.
$(MAKE) --no-print-directory audit-digest
# The cookbook index is DERIVED (R33) and self-asserts its coverage (R32). Stale = agents can't
# find documented idioms and re-derive them at full token cost (measured, P30 wave 1).
$(VENV_PY) tools/cookbook_index.py --check
+2 -1
View File
@@ -448,7 +448,7 @@ Modern cpp preprocesses → **vintage cc1** compiles to asm → **maspsx** emula
- **`make report` is genuinely fail-closed** — any of its gates failing exits non-zero (verified by a negative control: the same broken gate exits 0 under the old `-c`, non-zero under `-ec`).
- **`make check-all` / `extract-all` assert COVERAGE (`pass == N`), not the absence of a failure marker** — the old `fail == 0` form was a *vacuous pass* on an empty pipeline. `check-all`'s `pass=$(grep -c …)` carries `|| true` (grep -c exits 1 on zero matches, which `-e` would otherwise treat as fatal — it would fail check-all exactly when nothing failed).
- **`make check-env` opts OUT** (`set +e` at the top of its recipe) — its contract is accumulate-every-failure-and-report, which `-e` would truncate at the first missing tool. It is the **only** intended opt-out; add `set +e` to a recipe only with the same justification.
- **`make tools-health`** (new) = regenerate the byte-derived sigs (`sig-overlays` + `sig-resident`) then run `audit-corpus` + `audit-cdecl` + `report`, fail-closed — the deliberate pre-matching ritual the roadmap's standing invariant names. Deliberately NOT a prerequisite of `report`/`build` (audit-cdecl cross-compiles every C declaration through real gcc, ~minutes). `audit-cdecl` ≈ several minutes; `audit-corpus` ≈ 7 s.
- **`make tools-health`** (new) = regenerate the byte-derived sigs (`sig-overlays` + `sig-resident`) then run `audit-corpus` + `audit-cdecl` + `audit-binaries` + `report` + `audit-digest`, fail-closed — the deliberate pre-matching ritual the roadmap's standing invariant names. Deliberately NOT a prerequisite of `report`/`build` (audit-cdecl cross-compiles every C declaration through real gcc, ~minutes). `audit-cdecl` ≈ several minutes; `audit-corpus` ≈ 7 s.
- **`make sig-resident`** (Phase-27 T10) signs the resident flat blob with `sig_image` (byte-derived) so `make audit-corpus`'s second, independent boundary oracle (R34) now covers the **resident** — probed clean (0 phantom/truncated). `sig-overlays` derives its payload list from `config/overlays.mk` (not a `0.4.dec` glob, which dropped the 4 SC07 index-1 overlays). **main** stays a boundary blind spot — `sig_image` can't sign the PS-X EXE yet (header offset + interleaved islands + one text range); scoped + deferred in `docs/second-oracle.md`. `progress.py --fleet` now reports a separate **MAIN game-code weighted** line (provisional, from a LINKED-excluding Ghidra sig) — the metrics-contract "main in the denominators", honestly un-folded.
### §6.4 asm-differ + baseline discipline
@@ -721,6 +721,7 @@ Every script under `tools/` (plus the two report make-targets), grouped by purpo
| | `tools/serve_local.py` | **Serve the fine-tuned model on the GPU** (base+LoRA via Unsloth, `.venv-train`, OpenAI endpoint) — the in-repo replacement for LM Studio. Run: `LD_LIBRARY_PATH=$(ls -d .venv-train/lib/python3.12/site-packages/nvidia/*/lib \| tr '\n' :) .venv-train/bin/python tools/serve_local.py --adapter models/bfm-match-7b-v3 --name bfm-match-7b-v3 --port 1234`. (Prebuilt `llama-cpp-python` CUDA wheels SIGILL on this no-AVX-512 CPU; the Unsloth/torch path is reliable, no build.) |
| | `tools/export_pairs.py` / `format_finetune.py` / `train_lora.py` / `eval_lora.py` | The corpus→LoRA pipeline (`.venv-train`): mine (asm↔C) pairs incl. the `engine_core.h` **macro bodies** + `engine_types.h` structs (corpus-v3) → Qwen chat-template + compile-filter → Unsloth QLoRA (3080 Ti) → held-out gate-true eval. Datasets/weights gitignored (`datasets/`, `models/`, `.venv-train/`). |
| **The derived oracles** (Phase 26-A tooling audit; R33 before R32 — *the best outcome is a deleted scanner, not a fixed regex*) | `tools/corpus.py` + **`make audit-corpus`** | THE corpus oracle. Derives from the FILESYSTEM (which `.c` files make a binary; the `INCLUDE_ASM` line is self-describing — its first argument *is* the asm subdir) and from the PROVEN INVARIANT (`matched = sig − stubs`, never re-parsed from C). Killed ~10 hand-maintained layout models. `audit-corpus` is a **second oracle that can disagree**: it cross-checks splat's boundaries against `sig_image`'s independent ones (0 phantom + 0 truncated since A4; was 193 unmatchable slices). |
| | `tools/audit_digest.py` + **`make audit-digest`** | **(P30 S1e, cookbook §140)** The **scoreboard** oracle: recomputes the three headline metrics from the CURRENT tree and fails if the committed `docs/progress.fleet.md` disagrees. Wired into `tools-health` AFTER `report`. Exists because a digest generated from a working tree that later changed (work reverted before the commit landed) is **byte-invisible** — `check-all` stays 140/140 over it forever (R34: the byte-gate is a null oracle for DOCUMENTS) — and the next honest regeneration then reads as a REGRESSION that never happened. That is exactly what the `commit:1426` digest did: overstated **+7,879 ins / +130 unique fns**, which parked the phase's best lever on a phantom for a session. Compares **integers, not the printed percentages** (the staleness rendered as "94.4%" on both sides). Negative-control-proven against that stale digest. Same task hardened `progress.py stub_addrs`, which wrapped the fail-closed `corpus.stubs` in a bare `except` → empty stub set → `matched = sig − stubs` credited EVERY function: byte-witnessed reporting **instr 100.00% / distinct 100.00%** in a tree with no `asm/`. The identical swallow was fixed in `cast_call_sites.tu_for` + `reconcile_tu.tu_for`, where it silently reconciled drafts against the default `<ov>.c` instead of the jr/-O0 split TU — the very bug `cast_call_sites`' docstring exists to fix. |
| | `tools/cdecl.py` + **`make audit-cdecl`** | **THE C-declaration oracle (cookbook §51g).** ONE recursive-descent parser of C's **declarator grammar**, replacing fifteen tools' private regex models — models that disagreed with each other and were, all fifteen, blind to fn-ptr/jump-table decls (`extern void (*D_X[])(void);`), sized arrays (`[4]`), and multi-declarators (where the *whole line* was dropped). Total by construction, not by shape enumeration. **Two statement paths, because the inputs differ:** `tu_statements()` derives a TU's file scope from **`cpp`** (a decl inside a `DEFINE_func_*` macro body declares nothing until invoked — §8c; 54 ms/TU), and `split_statements()` is a **span-preserving** raw split for drafts (which get rewritten). API: `parse` / `scope` / `tu_scope` / `Declarator{name,kind,type,params,pnames,is_proto,is_definition}`. Verified: **2,952,246 depth-0 statements → 2,731,521 declarators, 0 parser defects**; **50,405 distinct declarations round-tripped through the real cross-gcc, 0 rejected**; residue adjudicated NOT-C *by gcc*, not by opinion. **Phase-27 T4 — the canonical draft-typedef strip:** `typedef_names(tu_path)` (the names a TU declares as typedefs, robust `tu_statements`-based so a coverage gap can't crash the byte-gate) + `strip_provided_typedefs(draft, provided)` (drop a draft's self-contained typedefs the target already supplies, splitting multi-typedef lines and covering scalar AND struct typedefs). Replaced **six** copied scalar-name regexes with complementary holes: `harvest_verify` now strips per-TU (unblocks the 39 struct-typedef drafts `_TD` dropped) and **surfaces cc1 stderr** so a `redefinition`/`conflicting types` failure reports as **PLUMBING**, not a byte mismatch (`.run/harvest_failed.classified.txt`); `masked_diff.strip_scalar_typedefs()` (used by `match_one`/`p16_permute`) fixes the multi-typedef-line skip that discarded 42 masked-MATCH drafts over whitespace (`func_8015C030` → `MATCH (23 ins)` unedited). `canon_sig_reconcile`/`eval_lora`/`format_finetune` keep their own copies for now (migrate per-bank, byte-gated — the audit-prescribed cadence). |
| | Phase 26-A tool-hygiene close (A9d–A10) | **DELETED** (R33, dead Phase-17 chain): `tools/census_conflict_callees.py` + `tools/derive_canonical_sigs.py` — `reconcile_tu`/`cdecl` answer their question from the build. **`overlay_src_split.py`**: `scan_construct` force_decl latch fixed (no longer swallows a def sharing a line with leading externs) + `hidden_definitions()` R32 coverage oracle wired into `selftest`. **`jr_isolate_all.py` `jr_inventory`**: `banked` DERIVED FROM THE IMAGE (`family_remap.reloc_targets` owns-a-carve) not a gitignored roster (R33) + curated-name via `addr_of` + 1:1 carve-ownership assert. **`family_remap.reloc_targets`**: optional `data=` param (read the image once, pass to N calls). **`backlog.py`**: `BACKLOG_NO_RENDER` env so parallel `gate_stage` workers skip the render race (append is atomic). `reconcile_tu` confirmed live on BOTH banking paths (`gate_stage` + `jtbl_family_bank.recover`→`bank_exemplar`). |
+18 -6
View File
@@ -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 / 393 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 / 398 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.
@@ -58,7 +58,7 @@
- **§3-The** — attribution primitive (use this before calling anything a scheduling residual) <sub>L6050</sub>
- **§3-The** — scheduling rules (refining §135-2 and §135-4) <sub>L8902</sub>
### register allocation & pins (32)
### register allocation & pins (33)
- **§10** — Closing the regalloc/scheduling hard tail by hand (LZSS, Phase 7 session F — the full close) <sub>L835</sub>
- **Residual** — A — commutative `|`/`&`/`+` result lands in the wrong source-operand register <sub>L856</sub>
@@ -92,6 +92,7 @@
- **§86** — Pinned-exemplar templatability is a PER-FAMILY property, not a per-member rate; and the §42e pin guard is now over-conservative (Phase 29 SESSION-20) <sub>L6582</sub>
- **§3-Two** — further notes worth keeping <sub>L8241</sub>
- **§137** — REGALLOC-PERM is a TWO-COMPILE ARITHMETIC PROBLEM, not a permuter job <sub>L9334</sub>
- **§3-The** — same swallow, twice more, in the integration spine <sub>L9699</sub>
### CSE / redundancy / rematerialization (2)
@@ -342,7 +343,7 @@
- **§136c** — SIBLING-FIRST is a DERIVATION shortcut, not just a conflict fix (the fastest route in a family wave) <sub>L9059</sub>
- **§138** — The propagation lanes: a gate refusal is a DECLARATION, and which lever you owe depends on blast radius <sub>L9405</sub>
### integration / TU plumbing (35)
### integration / TU plumbing (36)
- **§8c** — Splitting a TU means rebuilding its DECLARATION ENVIRONMENT, not moving text (Phase 26 session 6) <sub>L437</sub>
- **§8d** — Templating a body INTO a TU must not CHANGE its declaration environment — demote the carried data externs (Phase 26 session 8, byte-proven on `func_8015AE2C` ×133) <sub>L483</sub>
@@ -379,8 +380,9 @@
- **§3-The** — integration idioms (these decide whether a byte-correct draft BANKS) <sub>L8797</sub>
- **§3-The** — declaration surface (integration, not codegen) <sub>L8931</sub>
- **Reconciling** — a gate-refused draft: which way you edit depends on WHERE the TU's decl is <sub>L9555</sub>
- **§3-The** — same swallow, twice more, in the integration spine <sub>L9699</sub>
### build graph, splat & the harness (96)
### build graph, splat & the harness (97)
- **§4** — Flag/toolchain gotchas <sub>L190</sub>
- **Build** — mechanism — per-file opt override (splat resegmentation) <sub>L288</sub>
@@ -478,8 +480,9 @@
- **§134** — again, in a second tool — and the waiter rule corrected <sub>L9516</sub>
- **Reconciling** — a gate-refused draft: which way you edit depends on WHERE the TU's decl is <sub>L9555</sub>
- **§139** — A GATE THAT GREPS FOR VERDICTS MUST ASSERT 1:1 ACCOUNTING; and a `--src` filter must not survive a carve (P30 S38, wave 6: 10 of 16 drafts vanished) <sub>L9584</sub>
- **§140** — A METRIC IS NOT A MEASUREMENT UNTIL IT IS REPRODUCIBLE FROM THE COMMITTED TREE (P30 S1e: a phantom regression that gated the session's best lever) <sub>L9650</sub>
### process, measurement & doctrine (53)
### process, measurement & doctrine (54)
- **§8e** — The jtbl ALIGNMENT LAW + the pad-spec filter — multi-table .rodata spans (Phase 29, byte-proven; `.run/probe_jtbl/verdict.md`) <sub>L530</sub>
- **§3-The** — mechanism: game-code dedup is SOURCE-LEVEL, not an object swap (R-D1, the key lesson) <sub>L926</sub>
@@ -534,8 +537,9 @@
- **§136h** — CORRECTION: the zero-crack pool does NOT "refill with cheap work" (my error, byte-measured) <sub>L9230</sub>
- **§136j** — The failure MIX flips with function size (measured across four bands, one session) <sub>L9292</sub>
- **Rank** — the lane by measured concentration, not by class count <sub>L9471</sub>
- **§140** — A METRIC IS NOT A MEASUREMENT UNTIL IT IS REPRODUCIBLE FROM THE COMMITTED TREE (P30 S1e: a phantom regression that gated the session's best lever) <sub>L9650</sub>
### (unbucketed — title matched no symptom vocabulary) (112)
### (unbucketed — title matched no symptom vocabulary) (115)
- **§3-How** — to use this <sub>L30</sub>
- **§1** — Idiom catalog (asm pattern → C that produces it) <sub>L39</sub>
@@ -649,6 +653,9 @@
- **STEP** — 0 of sibling-first: grep `src/` for a distinctive LITERAL from the `.s` <sub>L9536</sub>
- **§3-The** — generalisation — three corollaries worth more than the bug <sub>L9616</sub>
- **§3-And** — the inverse-lookup trap, same session <sub>L9633</sub>
- **§3-The** — three-line proof (do this before diagnosing any metric movement) <sub>L9670</sub>
- **§3-The** — two instrument defects it exposed <sub>L9679</sub>
- **§3-Two** — wrong mechanisms I chased first, and why they were wrong <sub>L9710</sub>
## All sections, in order
@@ -1046,3 +1053,8 @@
- **§139** — A GATE THAT GREPS FOR VERDICTS MUST ASSERT 1:1 ACCOUNTING; and a `--src` filter must not survive a carve (P30 S38, wave 6: 10 of 16 drafts vanished) <sub>L9584</sub>
- **§3-The** — generalisation — three corollaries worth more than the bug <sub>L9616</sub>
- **§3-And** — the inverse-lookup trap, same session <sub>L9633</sub>
- **§140** — A METRIC IS NOT A MEASUREMENT UNTIL IT IS REPRODUCIBLE FROM THE COMMITTED TREE (P30 S1e: a phantom regression that gated the session's best lever) <sub>L9650</sub>
- **§3-The** — three-line proof (do this before diagnosing any metric movement) <sub>L9670</sub>
- **§3-The** — two instrument defects it exposed <sub>L9679</sub>
- **§3-The** — same swallow, twice more, in the integration spine <sub>L9699</sub>
- **§3-Two** — wrong mechanisms I chased first, and why they were wrong <sub>L9710</sub>
+61
View File
@@ -2033,3 +2033,64 @@ no releases, and it requires a separate per-game repo to produce a playable resu
reference, a risk as a dependency.
**Verdict: PARKED for Gen3. Do not evaluate again before Gen2 exit.**
---
## 2026-08-04 (P30 S1e) — "distinct-code FELL" was a stale digest, not a regression; the alias lever is UNGATED
**Context + belief.** S38 closed with the def-side asm-label alias cracking a 208-conflict class
138/138 — the phase's best-performing lever. Its checkpoint then gated it in bold: *"distinct-code
FELL 89.3 → 89.2 — UNEXPLAINED. Do NOT scale the alias lever until it is resolved. The BYTES are
proven (R22); the ACCOUNTING is not."* The recorded lead was `progress.py:423`'s `SIG` regex booking
`void aF80146A6C(…)` under the alias name — with the honest caveat, written at the time, that a pure
naming artifact would move fn-count and distinct-code *together*, and these had diverged.
**What failed.** Two mechanisms, both mine, both asserted before being derived (R14):
1. *The recorded lead.* Real blindness — but it feeds `classify()`, which computes **fn-count only**.
Neither weighted metric ever sees a C identifier; they derive from `matched = sig − corpus.stubs`.
2. *"The harvest reverted functions to INCLUDE_ASM."* Attractive because that is **byte-neutral**
(INCLUDE_ASM pastes the original asm), so R22 would stay 140/140 across genuine coverage loss —
the R34 blind spot. Refuted by one grep: **483 stub lines removed, 0 added.**
**The pivot.** Stop hypothesizing; prove the arithmetic. Identical sigs (both denominators unchanged)
+ unchanged `tools/` + zero `INCLUDE_ASM` additions ⇒ HEAD's stub set is a strict subset of the
prior commit's ⇒ HEAD's matched set is a superset ⇒ **both numerators are forbidden to fall.** A
reported fall is therefore a statement about the *digest*, not the tree.
**The byte/measurement-grounded why.** Reconstructing each commit's stub set from its own committed
tree (0 unresolved symbols):
| | instr | distinct | unique fns |
|---|---|---|---|
| `commit:1426` **true** | 12,394,533 | 5,022,306 | 77,895 |
| `commit:1426` *as committed* | 12,402,412 | 5,029,324 | **78,025** |
| `HEAD` true **= committed** | 12,405,402 | 5,025,082 | 77,952 |
True delta: **instr +10,869, distinct +2,776 ins / +57 unique fns — everything rose.** The
`commit:1426` digest was **committed stale** (generated from a working tree still holding work that was
reverted before the commit landed; overstated +7,879 ins / +130 unique fns, never regenerated). The
next honest digest was lower than the stale one, so the metric *appeared* to fall.
**Consequences (what changed).**
- **The alias lever is UNGATED** — the blocker was a phantom. It is the phase's cheapest large lever
and should be scaled (S4 onward), subject only to the §61 small-batch discipline the two real R22
failures taught.
- `progress.py stub_addrs` no longer swallows `corpus.stubs`' refusal. The old bare `except` turned a
fail-closed oracle into a guess: byte-witnessed reporting **instr 100.00% / distinct 100.00%** in a
tree with no `asm/`.
- **`make audit-digest` is new** (in `tools-health`, after `report`): recomputes the three headline
metrics from the current tree and fails if the committed digest disagrees. Compares **integers, not
percentages** — the +7,879-instruction staleness printed as "94.4%" both before and after.
- The same swallow was found twice more in the **integration spine** (`cast_call_sites.tu_for`,
`reconcile_tu.tu_for`), where it silently reconciled drafts against the default `<ov>.c` instead of
the jr/-O0 split TU — the exact bug `cast_call_sites`' own docstring says it exists to fix. Both now
propagate. Given the phase's ~24k PLUMBING vs 4,917 DIFF base rate, that class presents as a
codegen wall.
**Hindsight — the better path.** The three greps (denominators / `tools/` diff / `+INCLUDE_ASM`
count) cost under a minute and settle the question *before* any hypothesis is formed. The general
form, now cookbook §140: **a committed number is a claim about a tree; if it cannot be recomputed
from that tree it is not evidence, and it must never gate a lever.** The deeper repeat is that this
is the *fourth* consecutive phase where a "wall" resolved to our own instruments — and this time the
instrument was the scoreboard itself, which is the one nobody thought to audit because the byte-gate
is green over it by construction (R34).
+80
View File
@@ -9644,3 +9644,83 @@ family up BY.
Symptom lines for the index: **"a gate reports fewer verdicts than it had drafts"** · **"a tool
reports NO FAMILY for a head another tool just enumerated"** · **"a gate result got worse after a
revert"** (→ re-extract, then re-measure).
---
## §140 — A METRIC IS NOT A MEASUREMENT UNTIL IT IS REPRODUCIBLE FROM THE COMMITTED TREE (P30 S1e: a phantom regression that gated the session's best lever)
**The alarm.** S38's checkpoint recorded, in bold: *"distinct-code FELL 89.3 → 89.2 across the last
commit — UNEXPLAINED. Do NOT scale the alias lever until it is resolved. The BYTES are proven (R22);
the ACCOUNTING is not."* The def-side asm-label alias had just cracked a 208-conflict class 138/138
— the best-performing lever of the phase — and it was parked on that one line.
**The regression never happened.** True values, recomputed from each commit's own tree:
| | instr | distinct | unique fns |
|---|---|---|---|
| `commit:1426` **true** | 12,394,533 | 5,022,306 | 77,895 |
| `commit:1426` *as committed* | 12,402,412 | 5,029,324 | **78,025** |
| `HEAD` true **= committed** | 12,405,402 | 5,025,082 | 77,952 |
The real delta over that span is **instr +10,869, distinct +2,776 ins / +57 unique fns — everything
rose.** The `843` digest was **committed stale**: generated from a working tree that still held work
REVERTED before the commit landed (overstating by +7,879 ins / +130 unique fns), and never
regenerated. The next honest digest was lower than the stale one, so the metric *appeared* to fall.
### The three-line proof (do this before diagnosing any metric movement)
The weighted metrics are `matched = sig − corpus.stubs`. So if, between two commits, (a) the sigs are
identical (both **denominators** unchanged is sufficient evidence), (b) `tools/` is unchanged, and
(c) `git diff A B -- src/ | grep -c '^+.*INCLUDE_ASM('` is **0** — then HEAD's stub set is a strict
subset of A's, HEAD's matched set is a superset, and **both numerators are mathematically forbidden
to fall.** A reported fall is then a statement about the *digest*, not the tree. Three greps settle
it before a single hypothesis is formed; I burned two wrong mechanisms first (see below).
### The two instrument defects it exposed
**(1) `progress.py stub_addrs` swallowed the oracle's refusal.** It wrapped `corpus.stubs` in
`except Exception: return set()` — and an empty stub set does not mean "no stubs", it means "the
oracle could not answer", after which `matched = sig − stubs` credits **every** function as banked.
Byte-witnessed: running the metric in a tree with no `asm/` made `corpus.stubs` raise its correct,
coverage-asserted `CorpusError` for all 140 binaries, and the tool reported **instr 100.00% /
distinct-code 100.00%** — a complete decomp, out of a swallowed error. `corpus.stubs` is
*deliberately* fail-closed ("it refuses to answer rather than guess 'banked'"); a bare `except`
around a fail-closed oracle reinstates precisely the guess it refuses to make. **Fixed: propagates.**
**(2) Nothing ever re-checked a committed digest.** R34, again: the whole-binary byte-gate is a
perfect CORRECTNESS oracle and a **null oracle for documents** — a stale digest is byte-irrelevant,
so `check-all` stays 140/140 across it forever, and `audit-corpus`/`audit-binaries` assert things
about the CODE. **Fixed: `make audit-digest`** (`tools/audit_digest.py`, wired into `tools-health`
after `report`) recomputes the three headline metrics from the current tree and fails if the
committed digest disagrees. Negative-control-proven against the known-stale `843` digest. Note it
compares **integers, not the printed percentages**: the +7,879-instruction staleness rendered as
"94.4%" both before and after, so a percentage comparison would have seen nothing.
### The same swallow, twice more, in the integration spine
`cast_call_sites.tu_for` and `reconcile_tu.tu_for` had the identical `except Exception: pass` around
`corpus.stubs`, falling back to the default `src/<ov>/<ov>.c`. `cast_call_sites`' own docstring, three
lines above, says the function exists *because* "the recovery passes were reconciling against a
DIFFERENT TRANSLATION UNIT than the one that would compile the code, and the heavy Phase-26 cores
live in exactly those jr files" — so the swallow silently reinstated the bug the function was written
to fix. A wrong-TU reconcile fails the gate, and this phase's base rate is **~24k PLUMBING vs 4,917
DIFF**, so it would present as a codegen wall. Both now propagate `CorpusError` while keeping the
`ValueError` fallback for curated (non-`func_ADDR`) names.
### Two wrong mechanisms I chased first, and why they were wrong
- **The recorded lead — `progress.py:423`'s `SIG` regex** (it books `void aF80146A6C(…)` under the
alias name). Real blindness, but it feeds `classify()`, which computes **fn-count only**. Neither
weighted metric ever sees a C identifier. *A lead that names a function must be checked against
which metric that function actually feeds.*
- **"The harvest reverted functions to INCLUDE_ASM"** — plausible because reverting to `INCLUDE_ASM`
is **byte-neutral** (it pastes the original asm), so R22 would stay green over real coverage loss.
Refuted in one grep: 483 stub lines removed, **0 added**.
**Symptom lines for the index:** **"a progress metric fell but the byte-gate is green"** · **"two
metrics moved in opposite directions"** · **"a digest disagrees with the tree it describes"** ·
**"a coverage oracle reports 100%"** · **"a recovery pass reconciled against the wrong TU"**.
**The law:** *a committed number is a claim about a tree; if it cannot be recomputed from that tree,
it is not evidence — and it must never gate a lever.* (R32/R34/R35; and R14 — I asserted two
mechanisms before deriving either.)
+12 -10
View File
@@ -191,8 +191,9 @@ stub on a named wall/behemoth/queue ledger** — 140/140 byte-identical througho
## FLEET — R22 **140 passed / 0 failed of 140**
**96.46% fn-count · 94.4% instr-weighted · 89.2% distinct-code** (77,952 uniq) · 0 NON_MATCHING.
Session opened 96.28 / 94.1 / 88.7 ⇒ **+37,166 instructions**, ~0 agent tokens after the opening wave.
**⚠️ distinct-code FELL 89.3 → 89.2 across the last commit — UNEXPLAINED. See task #11 (S1e); do NOT
scale the alias lever until it is resolved. The BYTES are proven (R22); the ACCOUNTING is not.**
**✅ RESOLVED S39 (S1e) — the "distinct-code FELL 89.3 → 89.2" alarm was a STALE COMMITTED DIGEST, not
a regression. True delta over that span: instr +10,869 · distinct +2,776 ins / +57 uniq — everything
ROSE. THE ALIAS LEVER IS UNGATED. Guard added: `make audit-digest` (in `tools-health`). See §140.**
## 🔑 THE SESSION'S BIGGEST FIND — the def-side asm-label alias is a CLASS lever
The dominant sweep blocker was `conflicting types for func_80146A6C` (**208 of ~398** conflicts).
@@ -232,16 +233,17 @@ Both passed their per-binary/per-draft gate and FAILED the clean-tree rebuild:
| **S2** jr families | DONE — 10 members; 7 families now CLASSIFIED |
| **S3** the whale | DONE-PARTIAL — **137/138**; `ov_SC07_010` open; the 61 SC07 -O0 members NOT attempted |
| **S1d** the alias class | **138/138 on family 1**; generalised harvest committed w/ the accounting caveat |
| **S1e** (NEW #11) · S4 · S5 · S6 · S7 | pending |
| **S1e** (#11) | ✅ **RESOLVED — THE REGRESSION NEVER HAPPENED (S39).** The `commit:1426` digest was committed **STALE** (generated from a tree still holding work reverted before the commit landed; overstated **+7,879 ins / +130 uniq**, never regenerated), so the next honest digest read as a fall. True delta 843→HEAD: **instr +10,869 · distinct +2,776 ins / +57 uniq — everything ROSE.** HEAD's digest reproduces EXACTLY. **⇒ THE ALIAS LEVER IS UNGATED — scale it (§61 small batches).** Both recorded leads were wrong (R14): `progress.py:423`'s `SIG` feeds **fn-count only**, and "reverted to INCLUDE_ASM" died on one grep (483 removed, **0 added**). Fixes: `stub_addrs` no longer swallows `corpus.stubs` (the bare `except` byte-witnessed reporting **100.00%/100.00%** in a tree with no `asm/`); **NEW `make audit-digest`** in `tools-health` (integers, not percentages — the staleness printed as "94.4%" both sides), negative-control-proven vs the stale digest; the SAME swallow fixed in `cast_call_sites.tu_for` + `reconcile_tu.tu_for`, where it reconciled against the **wrong TU** (the bug that file's own docstring exists to fix). cookbook **§140** · decision-log 2026-08-04. |
| S4 · S5 · S6 · S7 | pending |
## ▶ RESUME HERE
1. **S1e (task #11) FIRST** — resolve the distinct-code drop before scaling the alias lever. Lead:
`progress.py:423`'s `SIG` regex records the identifier before the paren, so `void aF80146A6C(...)`
is recorded as `aF80146A6C`; the same scanner's docstring documents that exact blindness for K&R
defs ("silently erased ~190k banked instructions"). **But that does NOT explain fn-count RISING
while distinct-code FELL** — under a pure naming artifact they move together. Fix by resolving a
definition through its `__asm__` label; `overlay_src_split.asm_label_aliases` already does this
(R33, reuse it).
1. ~~**S1e (task #11) FIRST**~~ ✅ **DONE (S39)** — no regression existed; the alias lever is ungated
and is the cheapest large lever on the board. Both recorded leads were wrong (R14): the
`progress.py:423` `SIG` lead feeds **fn-count only** (neither weighted metric sees a C
identifier — they derive from `matched = sig − corpus.stubs`), and the "reverted to INCLUDE_ASM"
theory died on one grep (483 removed, 0 added). Still worth doing opportunistically: the `SIG`
alias blindness is REAL for fn-count — resolve a def through its `__asm__` label via
`overlay_src_split.asm_label_aliases` (R33, reuse it) rather than a new regex.
2. **`ov_SC06_030` + `ov_SC07_010`** — both reverted, both re-attemptable (see the failures above).
3. **S4** (task #6) + wave 6's 3 still-failing alias drafts (the three LARGEST — size-correlated).
4. **S5 the wave** (task #7) — 1,689 h_norm clusters / 326,261 ins at 2.7×. **VERIFY Fable's pool
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""tools/audit_digest.py — does the COMMITTED fleet digest still describe the CURRENT tree?
WHY THIS EXISTS (Phase 30 S1e)
===============================
`docs/progress.fleet.md` is the project's headline instrument: the burn-down, the roadmap's
re-baselines, and every "is this lever paying?" call are read off its three numbers. It is written
by `tools/progress.py --fleet` from whatever tree happened to be on disk at that moment — and then
COMMITTED, at which point nothing ever checks it again.
That gap produced a real, costly false alarm. The S38 digest committed at `commit:1426` claimed:
instr 12402412 distinct 5029324 (78025 uniq)
The same metric recomputed from `commit:1426`'s own committed tree gives:
instr 12394533 distinct 5022306 (77895 uniq)
— overstated by +7,879 instructions and +130 unique functions, because the digest was generated
from a working tree that still held banked work which was REVERTED before the commit landed, and
was never regenerated afterwards. The next honest digest was therefore LOWER than the stale one, so
the metric appeared to FALL (distinct-code 89.3 -> 89.2) while the tree had in fact only gained
(true delta over that span: instr +10,869, distinct +2,776 ins / +57 unique fns — everything rose).
A session then opened with "distinct-code FELL — UNEXPLAINED; do NOT scale the alias lever until it
is resolved", i.e. the project's best-performing lever was gated on a phantom regression, and the
recorded lead (a name-scanning regex in classify()) pointed at a function that does not feed either
weighted metric at all.
WHY THE EXISTING GATES CANNOT SEE IT (R34)
==========================================
The whole-binary byte-gate is a perfect CORRECTNESS oracle and a NULL oracle for documents: a stale
digest is byte-irrelevant, so `make check-all` stays 140/140 across it forever. `audit-corpus` and
`audit-binaries` assert things about the CODE. Nothing asserted that the number we publish about the
tree is a number OF that tree. This is that second, disagreeing oracle — and its only job is to
disagree when the digest and the tree have drifted apart.
WHAT IT ASSERTS
===============
Recompute the three headline metrics from the CURRENT tree and require the committed digest to
match. A mismatch is not "the numbers moved" — it is "the published number was never true of this
tree", and the fix is always the same: `make report`.
Exit 0 = the digest describes this tree. Exit 1 = it does not (message says which metric drifted).
Usage: tools/audit_digest.py [--fix]
"""
import re
import sys
import os
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(REPO, "tools"))
DIGEST = os.path.join(REPO, "docs/progress.fleet.md")
# The three headline lines, exactly as progress.fleet() writes them. Parsed as integers (the
# printed percentages are rounded to 1dp, so they are far too coarse to catch a 130-function drift —
# the +7,879-instruction staleness above still rendered as "94.4%" both before and after).
_LINES = {
"fn-count": re.compile(r'^FLEET fn-count byte-ident:\s*(\d+)\s*/\s*(\d+)', re.M),
"instr": re.compile(r'^FLEET instr-weighted\s*:\s*(\d+)\s*/\s*(\d+)', re.M),
"distinct": re.compile(r'^FLEET distinct-code\(uniq\):\s*(\d+)\s*/\s*(\d+)\s*=\s*[\d.]+%\s*\((\d+)/(\d+)', re.M),
}
def committed():
"""The three metrics as published in docs/progress.fleet.md."""
if not os.path.exists(DIGEST):
raise SystemExit(f"audit-digest: {os.path.relpath(DIGEST, REPO)} does not exist — run `make report`.")
txt = open(DIGEST).read()
out = {}
for key, rx in _LINES.items():
m = rx.search(txt)
if not m:
# R32: a line we cannot read is a DEFECT, not a skip — a silently-unparsed headline is
# exactly the blindness this file exists to remove.
raise SystemExit(f"audit-digest: cannot parse the '{key}' line out of "
f"{os.path.relpath(DIGEST, REPO)} — the digest format changed and this "
f"oracle went blind. Fix the pattern, do not delete the check.")
g = [int(x) for x in m.groups()]
out[key] = tuple(g)
return out
def live():
"""The same three metrics, recomputed from the CURRENT tree."""
import progress
# fn-count comes from the per-binary classification; the weighted pair from the derived stub
# oracle. Both are recomputed here exactly as `progress.fleet()` does it.
order = [b for b in ("main", "resident") if b in progress.BINARIES] + \
sorted(b for b in progress.BINARIES if b not in ("main", "resident"))
rows = [progress.report(b, write=False) for b in order]
byte = sum(r["byteident"] for r in rows)
match = sum(r["matchable"] for r in rows)
wm = progress.weighted_metrics()
if not wm:
raise SystemExit("audit-digest: no .run/sig.*.jsonl — run `make sig-overlays sig-resident`.")
return {
"fn-count": (byte, match),
"instr": (wm["fleet_m"], wm["fleet_t"]),
"distinct": (wm["dedup_m"], wm["dedup_t"], wm["dedup_fns"], wm["dedup_total_fns"]),
}
def main():
fix = "--fix" in sys.argv
have, want = committed(), live()
bad = [k for k in want if have.get(k) != want[k]]
if not bad:
print(f"audit-digest: OK — docs/progress.fleet.md describes the current tree "
f"(instr {want['instr'][0]}/{want['instr'][1]}, "
f"distinct {want['distinct'][2]}/{want['distinct'][3]} uniq).")
return 0
print("audit-digest: FAIL — the committed digest does not describe the current tree.")
print(" A stale digest is BYTE-INVISIBLE (check-all stays green over it), and a later honest")
print(" regeneration then reads as a REGRESSION that never happened. See this file's header.")
for k in bad:
print(f" {k:9s} committed {have.get(k)} actual {want[k]}")
if fix:
print(" --fix: regenerating…")
import progress
progress.fleet()
return 0
print(" Fix: `make report` (regenerates the digest), then commit it WITH the work it describes.")
return 1
if __name__ == "__main__":
sys.exit(main())
+13 -5
View File
@@ -69,11 +69,19 @@ def tu_for(overlay, fn, override=None):
if override:
return override
try:
st = _corpus.stubs(overlay).get(int(fn[5:], 16))
if st:
return os.path.join(REPO, st.path)
except Exception:
pass
addr = int(fn[5:], 16) # a curated (non-func_ADDR) name has no address here
except ValueError:
return os.path.join(REPO, f'src/{overlay}/{overlay}.c')
# CorpusError PROPAGATES (R32/R35, P30 S1e). This was a bare `except Exception: pass`, which on
# any oracle failure fell through to the default `<ov>.c` — silently reinstating the exact bug
# the docstring above says this function exists to fix (reconciling against the WRONG TU, where
# the heavy Phase-26 jr cores live). A wrong-TU reconcile fails the gate, and this phase's base
# rate is ~24k PLUMBING vs 4,917 DIFF, so it would read as a codegen wall. Verified safe: at
# P30 S1e HEAD, corpus.stubs raises for 0 of 140 binaries — if it fires, the tree really is
# inconsistent and reconciling through it is worse than stopping.
st = _corpus.stubs(overlay).get(addr)
if st:
return os.path.join(REPO, st.path)
return os.path.join(REPO, f'src/{overlay}/{overlay}.c')
# A function declaration line (prototype ending in `;`, NOT a definition): optional `extern`,
+11 -4
View File
@@ -637,10 +637,17 @@ def weighted_metrics():
return None
def stub_addrs(binary):
try:
return set(corpus.stubs(binary)) # {addr:int -> Stub}; the derived INCLUDE_ASM set (R33)
except Exception:
return set()
# FAIL LOUD (R32/R35). This was `except Exception: return set()` — and an empty stub set does
# not mean "no stubs", it means "the oracle could not answer": `matched = sig − stubs` then
# credits EVERY function in that binary as banked. Byte-witnessed during P30 S1e: running
# this metric in a tree with no `asm/` made corpus.stubs raise its (correct, coverage-asserted)
# CorpusError for all 140 binaries, the swallow turned each into an empty set, and the tool
# cheerfully reported **instr-weighted 100.00% / distinct-code 100.00%** — a 100%-complete
# decomp, from a swallowed error. corpus.stubs is deliberately fail-closed ("it refuses to
# answer rather than guess 'banked'"); wrapping it in a bare except reinstated exactly the
# guess it refuses to make. A loud failure nobody counts is as invisible as a silent one
# (R32) — so here it is neither swallowed nor merely logged: it aborts the metric.
return set(corpus.stubs(binary)) # {addr:int -> Stub}; the derived INCLUDE_ASM set (R33)
fm = ft = 0
cls_nins, matched_cls = {}, set()
+9 -5
View File
@@ -72,11 +72,15 @@ def tu_for(overlay, fn, override=None):
if override:
return override
try:
st = corpus.stubs(overlay).get(int(fn[5:], 16))
if st:
return os.path.join(REPO, st.path)
except Exception:
pass
addr = int(fn[5:], 16) # a curated (non-func_ADDR) name has no address here
except ValueError:
return os.path.join(REPO, f'src/{overlay}/{overlay}.c')
# CorpusError PROPAGATES (R32/R35, P30 S1e) — see the identical note in cast_call_sites.tu_for.
# A swallow here silently reconciles the draft against the DEFAULT `<ov>.c` instead of the jr/-O0
# split TU that actually compiles it, which is a §51g LAW 10 violation dressed up as a gate refusal.
st = corpus.stubs(overlay).get(addr)
if st:
return os.path.join(REPO, st.path)
return os.path.join(REPO, f'src/{overlay}/{overlay}.c')