feat(draw): route the model tier off the prior RESIDUAL CLASS, not nins

Measured on S71's own wave: wall-clock tracks iteration count, and iteration count
tracks the residual class, not size. A 26-instruction function took 18 min / 31 tool
calls (regalloc, finished NEAR); a 122-instruction one took 80 s / 10. The 20-33 min
runs were all compiler-internal residuals — scheduling ties, birthing boost, register
colouring, LUID order — where every hypothesis costs a compile-and-measure cycle.

arm_for keys on nins alone, so a 47-instruction regalloc wall could not be drawn at
the higher tier and nothing escalates mid-run. arm_from_history() now reads the
function's own journal notes at draw time and returns fable when they name one of
those classes; it never downgrades the size ladder's choice.
R39 control over 3,147 functions with history x 3 bands = 9,441 decisions:
4,020 upgrades (43%), 0 downgrades.

The control's FIRST form passed over an empty set — it keyed on journal rows carrying
a binary, and there are none: the agent verdict schema never had that field, so every
historical note is name-keyed and the same name is a different function in another
overlay (§238). claude_wave_draft.js's VERDICT now requires `binary`, so new rows are
exact. Cookbook §413.
This commit is contained in:
Drew T
2026-09-02 01:29:59 -06:00
parent fecbfac5f8
commit bfea4affd8
4 changed files with 98 additions and 7 deletions
+5 -2
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 / 1080 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 / 1081 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.
@@ -1336,7 +1336,7 @@
- **§408** — ★★★ — §406 REFUTED AS A SWEEP: THE SHAPE IS THE FAMILY, THE DISAGREEMENT IS THE DEFECT (P31 S71; 0 MATCH / 14 applied, 0 / 210) <sub>L33209</sub>
- **§411** — ★★★ — THE PACK MUST CARRY THAT FUNCTION'S OWN HISTORY (P31 S71; measured 38/39 vs 124/131) <sub>L33386</sub>
### (unbucketed — title matched no symptom vocabulary) (323)
### (unbucketed — title matched no symptom vocabulary) (324)
- **§3-How** — to use this <sub>L30</sub>
- **§1** — Idiom catalog (asm pattern → C that produces it) <sub>L39</sub>
@@ -1661,6 +1661,7 @@
- **§407** — ★★ — LATE-WAVE ADDENDA TO §405 (the last agents in) <sub>L33180</sub>
- **§409** — ★★★ — THE S71 JOURNAL-FUELLED WAVE: 100% FIRST-PASS MATCH, AND THE NINE LAWS IT BROUGHT BACK (P31 S71) <sub>L33261</sub>
- **§3-The** — nine laws this wave produced <sub>L33288</sub>
- **§413** — ★★★ — DIFFICULTY IS THE RESIDUAL CLASS, NOT `nins` — ROUTE THE MODEL TIER OFF HISTORY (P31 S71, Drew) <sub>L33473</sub>
## All sections, in order
@@ -2745,6 +2746,7 @@
- **§410** — ★★★ — COPY THEN ACCUMULATE ON THE COPY: resolving the birthing-boost vs register-allocation dilemma (P31 S71; byte-proven `ov_SC04_015/func_8017EB78`, 98 ins) <sub>L33353</sub>
- **§411** — ★★★ — THE PACK MUST CARRY THAT FUNCTION'S OWN HISTORY (P31 S71; measured 38/39 vs 124/131) <sub>L33386</sub>
- **§412** — ★★★ — §323 CARVE BLOCKER 2 WAS A REGEX THAT COULD NOT SEE PAST `__attribute__` (P31 S71) <sub>L33424</sub>
- **§413** — ★★★ — DIFFICULTY IS THE RESIDUAL CLASS, NOT `nins` — ROUTE THE MODEL TIER OFF HISTORY (P31 S71, Drew) <sub>L33473</sub>
---
@@ -3837,3 +3839,4 @@ Notes routinely quote that as a section id. This table resolves it. Grep bait: `
| L33353 | §410 | ★★★ — COPY THEN ACCUMULATE ON THE COPY: resolving the birthing-boost vs register-allocatio |
| L33386 | §411 | ★★★ — THE PACK MUST CARRY THAT FUNCTION'S OWN HISTORY (P31 S71; measured 38/39 vs 124/131) |
| L33424 | §412 | ★★★ — §323 CARVE BLOCKER 2 WAS A REGEX THAT COULD NOT SEE PAST `__attribute__` (P31 S71) |
| L33473 | §413 | ★★★ — DIFFICULTY IS THE RESIDUAL CLASS, NOT `nins` — ROUTE THE MODEL TIER OFF HISTORY (P31 |
+37
View File
@@ -33469,3 +33469,40 @@ position relative to a brace or a keyword must strip `__attribute__((…))` firs
appear between `}` and the name, after the name, and after the parameter list. This is the §134 class
(a scanner that cannot start where the C grammar actually puts things), and it is now the seventh tool
in this project to hit it.
## §413 ★★★ — DIFFICULTY IS THE RESIDUAL CLASS, NOT `nins` — ROUTE THE MODEL TIER OFF HISTORY (P31 S71, Drew)
**The observation.** S71's wave ran one agent per function and logged wall-clock and tool-call counts.
Duration tracks **iteration count**, and iteration count tracks the **residual class** — not size:
| function | ins | arm | wall | tool calls | what it was |
|---|---|---|---|---|---|
| `func_80181294` | **26** | opus | 18 min | 31 | REGALLOC-PERM, finished NEAR/2 |
| `func_80185D44` | **47** | opus | 21 min | 48 | LUID contradiction (read cc1's `-dS` trace) |
| `func_80185F4C` | **60** | opus | 22 min | 33 | sched1 birthing-boost tie |
| `func_800D24D0` | 141 | opus | 33 min | 51 | `memrefs_conflict_p` PLUS-vs-LO_SUM |
| `func_8017DB98` | 122 | opus | **80 s** | 10 | body recovered from a prior attempt |
| `func_80182CBC` | 28 | opus | 115 s | 12 | §193-A twin remap |
A 26-instruction function took 18 minutes; a 122-instruction one took 80 seconds. **Size predicted
neither.** What separates the two groups is whether the residual is a compiler-internal one
(scheduling ties, birthing boost, register colouring, LUID order) where every hypothesis costs a
compile-and-measure cycle — or an ordinary body question that a twin or a neighbour answers at once.
**Why they never escalated.** `draw_waves.arm_for` keys the model tier on `nins` alone, so a
47-instruction regalloc wall is STRUCTURALLY unable to be drawn at the higher tier, and nothing
escalates mid-run: each agent runs to its own budget and no watcher re-hands the target.
**The fix, available only because the packs now carry history (§411).** `arm_from_history()` reads the
function's own journal notes at draw time and returns `fable` when they name a compiler-internal
residual; it never downgrades what the size ladder chose. Negative-controlled over all 3,147 functions
with history × 3 size bands = 9,441 decisions: **4,020 upgrades (43%), 0 downgrades.**
**A defect the control caught, worth as much as the lever.** The first negative control iterated only
journal keys carrying a binary and reported `0 decisions / 0 downgrades` — a clean pass over an EMPTY
set. The agent verdict schema never had a `binary` field, so **every historical note is name-keyed**,
and the same name is a different function in another overlay (§238). Two consequences: the S71
past-attempt fuel can serve one overlay's history to another's target, and any future join on these
rows inherits it. `claude_wave_draft.js`'s `VERDICT` schema now requires `binary`, so new rows are
exact; the historical corpus stays name-keyed and should be read with that caveat.
`check-against-a-known-true-case`, again: the instrument passed because it measured nothing.
+47 -1
View File
@@ -25,6 +25,7 @@ Then, per wave: t5_cards.py -> claude_wave_packs.py -> wave_args.py, and launch
tools/workflows/claude_wave_draft.js with the args wave_args.py printed (never hand-typed).
"""
import argparse, collections, glob, json, os, sys
import re
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.chdir(REPO)
@@ -69,6 +70,51 @@ def arm_for(n):
return 'opus' if n <= 150 else 'fable'
# THE RESIDUAL CLASS PREDICTS DIFFICULTY BETTER THAN `nins` DOES (Drew, 2026-09-02, S71).
# Measured over the S71 wave's own agent runs: wall-clock and iteration count track the RESIDUAL,
# not the size. A 26-instruction function took 18 minutes and 31 tool calls (`func_80181294`,
# still NEAR); a 122-instruction one took 80 seconds and 10 (`func_8017DB98`). The 20-30 minute
# runs were all compiler-internal residuals — scheduling ties, birthing boost, register colouring —
# where each hypothesis costs a compile-and-measure cycle:
#
# func_80185D44 47 ins opus 21 min 48 tool calls (LUID contradiction, read cc1 -dS)
# func_80185F4C 60 ins opus 22 min 33
# func_800D24D0 141 ins opus 33 min 51
#
# `arm_for` keys on size alone, so a 47-instruction regalloc wall was STRUCTURALLY unable to be
# drawn at the higher tier, and nothing escalates mid-run. Now that every pack carries the
# function's own history (§411), the prior residual class is known AT DRAW TIME — and 1,352 of the
# 3,147 functions with history (43%) have a note naming one of these classes.
_WALL_RE = re.compile(
r'permuter|regalloc|register (?:alloc|colou?ring|pressure)|schedule[- ]reorder|SCHEDULE-'
r'|birthing|LUID|sched1|sched2|scheduler-internal|cross_?jump|delay[- ]slot|colou?ring',
re.I)
def arm_from_history(binary, fn, n, _cache={}):
"""`fable` when this function's own journal history names a compiler-internal residual.
Escalating SOONER is the standing finding (see arm_for); this applies it to the axis that
actually predicts cost. Falls back to the size ladder when there is no history, and never
DOWNGRADES what the size ladder chose."""
if not _cache:
try:
sys.path.insert(0, os.path.join(REPO, 'tools'))
import journal_notes
_cache['idx'] = journal_notes.load()
_cache['mod'] = journal_notes
except Exception:
_cache['idx'] = None
base = arm_for(n)
idx = _cache.get('idx')
if not idx or base == 'fable':
return base
rows = _cache['mod'].notes_for(idx, binary, fn)
if rows and _WALL_RE.search(" ".join(r.get('note') or '' for r in rows)):
return 'fable'
return base
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('--prefix', required=True, help='wave dir prefix; waves are <prefix>1, <prefix>2, ...')
@@ -128,7 +174,7 @@ def main():
continue
pool.append(dict(name=s.symbol, addr='0x%08x' % s.addr, nins=n, binary=b,
sub=s.asm_dir, asm=s.asm_path, tu=s.path,
cls='FRONTIER', arm=arm_for(n), **{'from': 'draw_waves'}))
cls='FRONTIER', arm=arm_from_history(b, s.symbol, n), **{'from': 'draw_waves'}))
pool.sort(key=lambda t: (t['nins'], t['binary'], t['name']))
print('population: %d open stub(s) in [%d,%d] ins, undrawn, over %d binaries (%d oracle refusals: %s)'
+9 -4
View File
@@ -14,6 +14,11 @@ const VERDICT = {
type: 'object',
properties: {
fn: { type: 'string' },
// BINARY IS PART OF A FUNCTION'S IDENTITY (R48/§238). Without it every journal note is
// name-keyed, and the same name is a DIFFERENT function in another overlay — so the S71
// past-attempt fuel (§411) could serve one overlay's history to another's target. Every
// historical journal row lacks this; stamping it now makes future ones exact.
binary: { type: 'string' },
arm: { type: 'string' },
status: { type: 'string', enum: ['MATCH', 'NEAR', 'FAIL', 'NO-DRAFT'] },
closeness: { type: ['integer', 'null'] },
@@ -21,7 +26,7 @@ const VERDICT = {
draft_path: { type: 'string' },
note: { type: 'string' },
},
required: ['fn', 'arm', 'status', 'closeness', 'compiles', 'draft_path', 'note'],
required: ['fn', 'binary', 'arm', 'status', 'closeness', 'compiles', 'draft_path', 'note'],
}
phase('Draft')
log(`wave ${WAVE}: ${TARGETS.length} targets (${TARGETS.filter(t => t.arm === 'sonnet').length} sonnet / ${TARGETS.filter(t => t.arm === 'opus').length} opus)`)
@@ -41,10 +46,10 @@ CLI equivalents (run from ${REPO}):
submit -> write your FINAL draft to ${REPO}/${WAVE}/${t.arm}/${t.name}.c (mkdir -p the dir) and return the JSON verdict.
HARD RULES: never modify anything under src/, config/, include/, asm/, build/ or run make; never touch other agents' files; write only to ${WAVE}/${t.arm}/${t.name}.c and scratch under ${WAVE}/${t.arm}/scratch_${t.name}/. Budget: up to ~24 compile/match_one iterations, then stop honestly.
Your final answer is the JSON verdict only: fn, arm="${t.arm}", status (MATCH if match_one says MATCH; NEAR if it compiles with closeness>0; FAIL if it never compiled; NO-DRAFT if you wrote nothing), closeness (integer or null), compiles, draft_path, note (one line: what blocked you, or which cookbook § unlocked it).`,
Your final answer is the JSON verdict only: fn, binary="${t.binary}", arm="${t.arm}", status (MATCH if match_one says MATCH; NEAR if it compiles with closeness>0; FAIL if it never compiled; NO-DRAFT if you wrote nothing), closeness (integer or null), compiles, draft_path, note (one line: what blocked you, or which cookbook § unlocked it).`,
{ label: `${t.arm}:${t.name}`, phase: 'Draft', model: t.arm, schema: VERDICT }
).then(v => v || { fn: t.name, arm: t.arm, status: 'NO-DRAFT', closeness: null, compiles: false, draft_path: '', note: 'agent returned null' })
.catch(e => ({ fn: t.name, arm: t.arm, status: 'NO-DRAFT', closeness: null, compiles: false, draft_path: '', note: 'agent error: ' + String(e).slice(0, 120) }))))
).then(v => v || { fn: t.name, binary: t.binary, arm: t.arm, status: 'NO-DRAFT', closeness: null, compiles: false, draft_path: '', note: 'agent returned null' })
.catch(e => ({ fn: t.name, binary: t.binary, arm: t.arm, status: 'NO-DRAFT', closeness: null, compiles: false, draft_path: '', note: 'agent error: ' + String(e).slice(0, 120) }))))
const byArm = {}
for (const r of results.filter(Boolean)) { (byArm[r.arm] = byArm[r.arm] || []).push(r) }
for (const arm of Object.keys(byArm)) {