diff --git a/config/kit_coverage_map.tsv b/config/kit_coverage_map.tsv
index fd1f599bb..412ba7c55 100644
--- a/config/kit_coverage_map.tsv
+++ b/config/kit_coverage_map.tsv
@@ -65,3 +65,4 @@ rule R103 DK-69 a failure-cause extractor is negative-controlled against the com
rule R104 DK-9 build the disagreeing oracle before the batch runs and measure the disagreement first — DK-9 (the differential-oracle harness) with DK-61's known-true case
rule R105 DK-70 a registry lists a member only after the gate spoke for it; a listed member whose site does not share is a named defect — DK-70 (the health suite asserts the work was done)
rule R106 DK-71 a policy taken on a remembered precedent is a belief — read the target project's tree — DK-71 (what earns belief)
+accelerator P37 S106 DK-65 a stress-test before the fleet run and an audit's rejections read before believed: the struct-spelling byte finding corrects DK-65 (per-access spelling under the gate); the probe-first order and the known-true discipline are DK-61; DK-65
diff --git a/decomp-architect/corpus/cookbook/cookbook-index.md b/decomp-architect/corpus/cookbook/cookbook-index.md
index f3c3f24fc..49158d8fc 100644
--- a/decomp-architect/corpus/cookbook/cookbook-index.md
+++ b/decomp-architect/corpus/cookbook/cookbook-index.md
@@ -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 / 1175 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 / 1176 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.
@@ -455,7 +455,7 @@
- **§472** — 🔴 §148-A's HOIST THRESHOLD IS 29, NOT 58, WHEN THE LOOP CONTAINS A CALL L35499
- **§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) L37157
-### structs, block moves & memcpy (92)
+### structs, block moves & memcpy (93)
- **§3-T2** — Source statement order drives instruction scheduling L78
- **§5** — Known hard-residual classes (instruction-identical, one byte-exact blocker) L199
@@ -549,6 +549,7 @@
- **§468** — THE `%lo`-FOLD EXTENDS TO STORES ONLY VIA `extern Struct SYM[]`, AND MASKING HID THE OPERAND ORDER L35390
- **§469** — THE `MEM_IN_STRUCT_P` ALIAS UNLOCK (and §463's spill law, independently confirmed) L35424
- **§497** — ★ — A BODILESS `typedef struct Tag Alias;` DEFINES THE ALIAS, NOT THE TAG: THE CARRIER'S FALSE "CONFLICTING BODIES" REFUSAL (P32 T1b; ov_SC02_017 `func_80186C64` isolation) L36705
+- **§458** — Struct spelling is a per-access dial: the T2 probe's byte evidence (Phase 37 S106) L37886
### types, signedness & load/store width (98)
@@ -1320,7 +1321,7 @@
- **§453** — ★★★ — ONE SOURCE PER UNIQUE FUNCTION: THE INCLUDE-AT-SITE SHARE, ITS GATE, AND THE FOUR DEFECTS THAT SHAPED IT (P35 S94–S96) L37561
- **§455** — Rung G, the guided search: score the object, classify the residual, compose the moves (Phase 36 S101) L37692
-### process, measurement & doctrine (143)
+### process, measurement & doctrine (144)
- **§8e** — The jtbl ALIGNMENT LAW + the pad-spec filter — multi-table .rodata spans (Phase 29, byte-proven; `.run/probe_jtbl/verdict.md`) L549
- **§3-The** — mechanism: game-code dedup is SOURCE-LEVEL, not an object swap (R-D1, the key lesson) L945
@@ -1465,6 +1466,7 @@
- **§479** — ★★★ — WHERE THE PERMUTER ACTUALLY PAYS: A MEASURED YIELD CURVE (P31 S77, 8 candidates) L35817
- **§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) L37157
- **§454a** — the recipe rung's real shape: replication, not discovery (S99, measured) L37677
+- **§458** — Struct spelling is a per-access dial: the T2 probe's byte evidence (Phase 37 S106) L37886
### (unbucketed — title matched no symptom vocabulary) (338)
@@ -2985,6 +2987,7 @@
- **§455** — Rung G, the guided search: score the object, classify the residual, compose the moves (Phase 36 S101) L37692
- **§456** — The lever-removal move catalog: what the S104 agents found under ~205 pins, barriers and launders (Phase 36 T7) L37765
- **§457** — The lever-removal move catalog, S105: eight TU batches, 42 of 42 at 0 (Phase 36 T7) L37814
+- **§458** — Struct spelling is a per-access dial: the T2 probe's byte evidence (Phase 37 S106) L37886
---
@@ -4172,3 +4175,4 @@ Notes routinely quote that as a section id. This table resolves it. Grep bait: `
| L37692 | §455 | Rung G, the guided search: score the object, classify the residual, compose the moves (Pha |
| L37765 | §456 | The lever-removal move catalog: what the S104 agents found under ~205 pins, barriers and l |
| L37814 | §457 | The lever-removal move catalog, S105: eight TU batches, 42 of 42 at 0 (Phase 36 T7) |
+| L37886 | §458 | Struct spelling is a per-access dial: the T2 probe's byte evidence (Phase 37 S106) |
diff --git a/decomp-architect/corpus/cookbook/matching-cookbook.md b/decomp-architect/corpus/cookbook/matching-cookbook.md
index cfc2e23e8..6644112c0 100644
--- a/decomp-architect/corpus/cookbook/matching-cookbook.md
+++ b/decomp-architect/corpus/cookbook/matching-cookbook.md
@@ -37882,3 +37882,22 @@ closed; the ledger (`.run/P36/delever/ledger.jsonl`) is the record of which rung
**The do-while tell.** Both marked do-while closes this session were reference-weight levers with the same arithmetic: the
loser needs EXACTLY +1 flow-time ref (`flow.c:2067`) on a statement that does not mention the winner; a real callee argument
does it when one exists — never a fake one.
+
+## §458 — Struct spelling is a per-access dial: the T2 probe's byte evidence (Phase 37 S106)
+
+**The fact.** In gcc 2.7.2 `*(T *)(p + k)` is a MEM without `MEM_IN_STRUCT_P`; `((S *)p)->f`, `p->f`, `q[k]` (typed `q`) and `*(Blk *)p`
+carry it (`expr.c:4568-4577`, `:4888`); `sched.c:837-865` lets a `/s` varying-address non-QImode access pass a non-`/s` FIXED-address one,
+and cse's kill table differs by `/s` (`docs/gcc-2.7.2-map/cse_expr.md` §4). So converting a cast to a member is a byte edit.
+**The measurement (`tools/restruct.py --probe`, 165 bodies, seed 37).** Rung 1 — every raw cast on ONE base of a body → `((P37S *)base)->unkK`
+with the type built from the struct map — is IDENTICAL on 126/139 judged bodies (716 sites moved); the 13 DIFFERS are COUNT/ORDER
+residuals, never a register change. Rung S2 — leave one site as a cast, else revert cumulatively in source order and minimise — closes
+13/13 with 27 casts kept.
+**The byte-read exemplar: `func_801814AC` (ov_SC05_010, `src/ov_SC05_010/ov_SC05_010_jr_80180F84.c:3030`).** Six `D_801C7E30`-based sites
+(`*(u16 *)(D_801C7E30 + 0x34) = 0; … + 2) = 9; *(u8 *)(… + 0xC2) = 0xE; var = D_801C7E30; …`). Every site a member EXCEPT the `+0x34` store →
+IDENTICAL. That store a member → the next statement's reload of the pointer global (`lui/lw D_801C7E30`, fixed, non-`/s`) hoists above the
+store (`/s`, varying): `nop; sh zero,52(v0); lui …` becomes `lui …; sh zero,52(v0)`. The pointer global itself read as a struct member
+(`((struct { S *p; } *)&D_801C7E30)->p` — both sides `/s`, they conflict, the order is kept) fixes that store — and moves a `li v0,10`
+belonging to the `param_1 + 0x1C` store elsewhere. The original therefore set `/s` per access; there is no uniform spelling of this body.
+**The rule.** Convert uniformly first (rung 1), then find the minimal kept-cast set (S2), then hand the kept casts to the readings (which
+side of each pair the original spelled scalar — a walked pointer, a reinterpretation, a cached global). A kept cast is not a fake: it is the
+original's spelling until a reading proves otherwise, counted and ledgered with its pass (SCHED-ALIAS here). Related: §30, §351, §379, §469.
diff --git a/decomp-architect/corpus/decomp-kernels.md b/decomp-architect/corpus/decomp-kernels.md
index 5c71c9028..c3ae197ff 100644
--- a/decomp-architect/corpus/decomp-kernels.md
+++ b/decomp-architect/corpus/decomp-kernels.md
@@ -800,8 +800,15 @@ data symbols; 3,558 of 4,287 C files carve splits; no formatter configuration; a
```
provenance: BFM gen3-standards (2026-09-07) + gen3-handoff §2.2; the sotn-decomp style guide read as data
-### DK-65 — Types are a banking lever and a width lever, not a byte lever
-- **Kernel:** a type name never moves a byte — the compiler does not care what a field is called — and the source
+### DK-65 — Types are a banking lever and a width lever — and, in gcc 2.7.2, a struct MEMBER is a scheduling lever too
+- **Correction (source project, Phase 37 T2, 2026-09-12):** the kernel below was written from Phase 17's measurement, which tested a
+ decompiler's drafting with a struct in context — not the spelling of a matched body. On the bytes, `*(T *)(p + k)` and `p->f` are NOT
+ the same code in gcc 2.7.2: a member/array access carries `MEM_IN_STRUCT_P` (`expr.c:4568-4577`, `:4888`), a cast on a sum does not, and
+ the scheduler's alias escape (`sched.c:837-865`) and cse's kill table read that flag. A struct-spelling probe on 165 matched bodies was
+ byte-neutral on 90.6 % and moved instructions on the rest; a per-site minimal kept-cast set closed every one. So: a canonical type
+ layer still pays as a banking and width lever from the first bank — and it must be introduced under the byte gate, per access, never by
+ a blanket rewrite that assumes neutrality. The name still moves nothing; the SPELLING can.
+- **Kernel (as first written):** a type name never moves a byte — the compiler does not care what a field is called — and the source
project measured exactly that early, correctly, and then drew the wrong second conclusion: that types could wait until
after 100%. Read the other way, its record says types are the largest *banking* lever it underweighted. Banking, not
cracking, was the bottleneck, and a large share of gate failures were declaration conflicts — a signed halfword against
diff --git a/decomp-architect/corpus/record/docs/accelerators.md b/decomp-architect/corpus/record/docs/accelerators.md
index b5ebfcf5a..6a7812f49 100644
--- a/decomp-architect/corpus/record/docs/accelerators.md
+++ b/decomp-architect/corpus/record/docs/accelerators.md
@@ -962,3 +962,19 @@ regen pass reports its refusal REASONS as a histogram (`&p`, `no base`, `re-seat
defect, not a property of the population (R32's "assert your coverage" for generators); (3) when an agent closes with a move
a generator claims to make, run the generator on the agent's START text before the bank — the cheapest instrument check in
the loop, and this session it paid three times.
+
+## P37 S106 (2026-09-12) — the Plan agent's stress-test before the fleet run, and an audit's rejections that were the probe's
+
+Four premises of the approved plan were corrected by a Plan agent reading the compiler source and the P36 packs BEFORE `type_census.py`
+ran on the fleet (the relocation-spelling oracle blind spot, function identity by body hash, the sixth site form `((T *)p)->f`, duplicates
+by layout + evidence) — each would otherwise have surfaced as a wrong number in a committed census. The struct map's clustering then went
+through four rounds against the classic over-merge (one cluster of 297,668 sites): typed-use parameters only, a width-conflict veto,
+single-source locals, positive shared evidence (≥1 common offset, the callee's map ≥2 offsets) — each rule measured by the biggest cluster's
+size. The layout audit against cc1 started at 4,498 rejections of 5,295 and ended at 0 of 5,283: five were the probe's (the environment
+lacked the canonical header; typedefs out of order; block-scope typedefs hoisted; a shared scratch name raced across workers; dead
+`#ifndef BFM_ENGINE_TYPES_H` blocks), two were the engine's (nested-paren attributes, per-field `aligned(N)`).
+**What would have been sooner:** (1) a struct-spelling probe on 150 bodies (6 min) as the FIRST thing in a types phase — it priced the
+campaign and rewrote the rewrite table five times; (2) rung S2 (leave-one-out, then cumulative revert) as the second rung of any
+per-site spelling campaign — 13/13 DIFFERS closed at ≤2n+1 compiles, no agent; (3) read every rejection message of a new audit before
+believing any (R40 for audits): 4,498 → 9 → 0 with two real findings along the way; (4) unique scratch names per probe (R111) — the
+cross-address copies produce IDENTICAL probe bodies, and a hash-keyed scratch file raced.
diff --git a/decomp-architect/corpus/record/docs/decision-log.md b/decomp-architect/corpus/record/docs/decision-log.md
index 90554e227..834f649b7 100644
--- a/decomp-architect/corpus/record/docs/decision-log.md
+++ b/decomp-architect/corpus/record/docs/decision-log.md
@@ -3813,3 +3813,42 @@ packs (21 classes: comment-boundary strips, macro-carried launders, one-off inst
pack-builder gap an agent works around from `body_tree.c` — fix the six comment-boundary strips in the tool, draw the rest
with the note. (4) The three parked signature changes of this session join the structs phase's list with ready patches;
none of them needed a new mechanism to read.
+
+## P37 S106 (2026-09-11/12) — the structs phase opens: struct spelling moves bytes, and the plan is built on that
+
+**Context/belief.** The Gen3 charter and `docs/gen3-handoff.md` §4 carried Phase 17's verdict forward — "types are a comprehension lever,
+not a byte lever: struct-ification is byte-neutral by construction as long as the layout and the access widths are right" — and
+`include/common.h:28-30` says `M2C_FIELD(p,t,o)` has "identical codegen to `p->field`". Phase 36 had found, one agent at a time, that the
+last third of its lever residue was signatures, structs, carves and one GTE spelling; its evidence (d10's register union, a18's
+`extern u16 X[]`, f10's body-local structs, f3's `s16[3]` arrays) said the struct spelling was the MISSING piece.
+**What the gate-1 reading found.** The claim is false in gcc 2.7.2 and the tree already knew it (cookbook §30/§351/§379/§469,
+`docs/gcc-2.7.2-map/cse_expr.md` §4): `*(T*)(p+k)` gets no `MEM_IN_STRUCT_P`, `p->f`/`q[k]`/`*(Blk*)p` do, and `sched.c:830-865`'s
+alias escape and cse's kill table read that flag. Phase 17 had measured m2c DRAFTING with a struct in context (0/10/2 on a
+structural-miss sample), not member-vs-cast spelling in a matched body — and the project generalised it into "types can wait". A Plan
+agent's stress-test then corrected four more premises before any tool ran: caller bytes DO depend on a narrow prototype's widths in both
+directions (`c-typeck.c:1740` converts to the parameter type, then re-promotes); a whole-object oracle says DIFFERS on a correct
+global-block edit because the relocation SPELLING changes while the linked bytes are identical (proven: f3's `D_801F8870[3]`, object
+DIFFERS, `make build BINARY=md_SC07_004` byte-identical); a function's identity is never its bare name (6,415 names carry more than one
+body text); and "one definition per layout" is too strong (`{s16 ×4}` carries 177 names, some genuinely different types) — a duplicate is
+same layout AND identical meaningful member names, an opaque copy block, or flow evidence.
+**The decisions (Drew, gate 1).** Grind to zero on raw casts, lying declarations and levers, with each "zero" defined so it is honest and
+reachable (reinterpret macros/unions for the genuine reinterpretations; byte-proven K&R sites counted apart; legitimate GTE sequences as
+project-local macros in the one header); placeholder names + cited evidence only; the P36 agent lane at his cap, started on his word; the
+full canonical declaration layer. After T0 he asked whether the phase should first analyse every function and build a map of every needed
+struct — my answer: yes, as T1's deliverable (the struct map), because it makes "zero casts" a checkable statement and lets T4's prototypes
+carry the parameter types in one pass; he approved. Also: no tags or releases at phase closes (the `v2.2.0` tag deleted; the P36 close
+pushed as a normal commit on his word).
+**What the census and the probe then measured (T1/T2).** 503,016 raw dereferences in four forms (the readability series' regex saw one,
+411,850 over raw text) + 18,912 address-of + 36,681 typed cast-member accesses that are already COMPONENT_REFs; 7,255 struct definitions
+(5,995 inside `.c` files) over 527 layouts; a `--strict` lever finish line of 4,010 + 6,717 direct GTE statements + 314 per-TU macros.
+Rung 1 (body-local `((T *)base)->unkK`) is byte-neutral on 126/139 judged bodies; the 13 DIFFERS are the `/s` asymmetry between a
+converted pointer base and the body's bare fixed globals, and a per-site greedy (rung S2) closes all 13 with 27 of 716 sites kept as
+casts — **the `/s` flag is a dial the original set per ACCESS, so a uniform conversion is the first rung and a per-site minimal kept-cast
+set is the second, before any agent.** The definition's ANSI signature is a free declaration for 92.6 % of (TU, callee) pairs with zero
+DIFFERS; the refusals are arity contradictions (K&R sites) and multi-spelled callees. The layout engine agrees with cc1 on 5,283
+definitions after the audit found two real gaps in it.
+**Hindsight (the better path).** (1) Re-read the compiler map before restating a Phase-17 verdict in a charter: the `/s` model was in
+the tree's own documents for two months while the handoff said "byte-neutral". (2) Price a struct campaign by a byte probe on the tree
+(one afternoon) before designing the engine — the probe fixed the rewrite table five times. (3) Build the disagreeing oracle for the new
+question BEFORE the campaign (R104): the relocation-spelling control took one binary build to settle and would otherwise have read every
+global-block conversion as a failure.
diff --git a/docs/accelerators.md b/docs/accelerators.md
index b5ebfcf5a..6a7812f49 100644
--- a/docs/accelerators.md
+++ b/docs/accelerators.md
@@ -962,3 +962,19 @@ regen pass reports its refusal REASONS as a histogram (`&p`, `no base`, `re-seat
defect, not a property of the population (R32's "assert your coverage" for generators); (3) when an agent closes with a move
a generator claims to make, run the generator on the agent's START text before the bank — the cheapest instrument check in
the loop, and this session it paid three times.
+
+## P37 S106 (2026-09-12) — the Plan agent's stress-test before the fleet run, and an audit's rejections that were the probe's
+
+Four premises of the approved plan were corrected by a Plan agent reading the compiler source and the P36 packs BEFORE `type_census.py`
+ran on the fleet (the relocation-spelling oracle blind spot, function identity by body hash, the sixth site form `((T *)p)->f`, duplicates
+by layout + evidence) — each would otherwise have surfaced as a wrong number in a committed census. The struct map's clustering then went
+through four rounds against the classic over-merge (one cluster of 297,668 sites): typed-use parameters only, a width-conflict veto,
+single-source locals, positive shared evidence (≥1 common offset, the callee's map ≥2 offsets) — each rule measured by the biggest cluster's
+size. The layout audit against cc1 started at 4,498 rejections of 5,295 and ended at 0 of 5,283: five were the probe's (the environment
+lacked the canonical header; typedefs out of order; block-scope typedefs hoisted; a shared scratch name raced across workers; dead
+`#ifndef BFM_ENGINE_TYPES_H` blocks), two were the engine's (nested-paren attributes, per-field `aligned(N)`).
+**What would have been sooner:** (1) a struct-spelling probe on 150 bodies (6 min) as the FIRST thing in a types phase — it priced the
+campaign and rewrote the rewrite table five times; (2) rung S2 (leave-one-out, then cumulative revert) as the second rung of any
+per-site spelling campaign — 13/13 DIFFERS closed at ≤2n+1 compiles, no agent; (3) read every rejection message of a new audit before
+believing any (R40 for audits): 4,498 → 9 → 0 with two real findings along the way; (4) unique scratch names per probe (R111) — the
+cross-address copies produce IDENTICAL probe bodies, and a hash-keyed scratch file raced.
diff --git a/docs/cookbook-index.md b/docs/cookbook-index.md
index f3c3f24fc..49158d8fc 100644
--- a/docs/cookbook-index.md
+++ b/docs/cookbook-index.md
@@ -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 / 1175 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 / 1176 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.
@@ -455,7 +455,7 @@
- **§472** — 🔴 §148-A's HOIST THRESHOLD IS 29, NOT 58, WHEN THE LOOP CONTAINS A CALL L35499
- **§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) L37157
-### structs, block moves & memcpy (92)
+### structs, block moves & memcpy (93)
- **§3-T2** — Source statement order drives instruction scheduling L78
- **§5** — Known hard-residual classes (instruction-identical, one byte-exact blocker) L199
@@ -549,6 +549,7 @@
- **§468** — THE `%lo`-FOLD EXTENDS TO STORES ONLY VIA `extern Struct SYM[]`, AND MASKING HID THE OPERAND ORDER L35390
- **§469** — THE `MEM_IN_STRUCT_P` ALIAS UNLOCK (and §463's spill law, independently confirmed) L35424
- **§497** — ★ — A BODILESS `typedef struct Tag Alias;` DEFINES THE ALIAS, NOT THE TAG: THE CARRIER'S FALSE "CONFLICTING BODIES" REFUSAL (P32 T1b; ov_SC02_017 `func_80186C64` isolation) L36705
+- **§458** — Struct spelling is a per-access dial: the T2 probe's byte evidence (Phase 37 S106) L37886
### types, signedness & load/store width (98)
@@ -1320,7 +1321,7 @@
- **§453** — ★★★ — ONE SOURCE PER UNIQUE FUNCTION: THE INCLUDE-AT-SITE SHARE, ITS GATE, AND THE FOUR DEFECTS THAT SHAPED IT (P35 S94–S96) L37561
- **§455** — Rung G, the guided search: score the object, classify the residual, compose the moves (Phase 36 S101) L37692
-### process, measurement & doctrine (143)
+### process, measurement & doctrine (144)
- **§8e** — The jtbl ALIGNMENT LAW + the pad-spec filter — multi-table .rodata spans (Phase 29, byte-proven; `.run/probe_jtbl/verdict.md`) L549
- **§3-The** — mechanism: game-code dedup is SOURCE-LEVEL, not an object swap (R-D1, the key lesson) L945
@@ -1465,6 +1466,7 @@
- **§479** — ★★★ — WHERE THE PERMUTER ACTUALLY PAYS: A MEASURED YIELD CURVE (P31 S77, 8 candidates) L35817
- **§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) L37157
- **§454a** — the recipe rung's real shape: replication, not discovery (S99, measured) L37677
+- **§458** — Struct spelling is a per-access dial: the T2 probe's byte evidence (Phase 37 S106) L37886
### (unbucketed — title matched no symptom vocabulary) (338)
@@ -2985,6 +2987,7 @@
- **§455** — Rung G, the guided search: score the object, classify the residual, compose the moves (Phase 36 S101) L37692
- **§456** — The lever-removal move catalog: what the S104 agents found under ~205 pins, barriers and launders (Phase 36 T7) L37765
- **§457** — The lever-removal move catalog, S105: eight TU batches, 42 of 42 at 0 (Phase 36 T7) L37814
+- **§458** — Struct spelling is a per-access dial: the T2 probe's byte evidence (Phase 37 S106) L37886
---
@@ -4172,3 +4175,4 @@ Notes routinely quote that as a section id. This table resolves it. Grep bait: `
| L37692 | §455 | Rung G, the guided search: score the object, classify the residual, compose the moves (Pha |
| L37765 | §456 | The lever-removal move catalog: what the S104 agents found under ~205 pins, barriers and l |
| L37814 | §457 | The lever-removal move catalog, S105: eight TU batches, 42 of 42 at 0 (Phase 36 T7) |
+| L37886 | §458 | Struct spelling is a per-access dial: the T2 probe's byte evidence (Phase 37 S106) |
diff --git a/docs/decision-log.md b/docs/decision-log.md
index 90554e227..834f649b7 100644
--- a/docs/decision-log.md
+++ b/docs/decision-log.md
@@ -3813,3 +3813,42 @@ packs (21 classes: comment-boundary strips, macro-carried launders, one-off inst
pack-builder gap an agent works around from `body_tree.c` — fix the six comment-boundary strips in the tool, draw the rest
with the note. (4) The three parked signature changes of this session join the structs phase's list with ready patches;
none of them needed a new mechanism to read.
+
+## P37 S106 (2026-09-11/12) — the structs phase opens: struct spelling moves bytes, and the plan is built on that
+
+**Context/belief.** The Gen3 charter and `docs/gen3-handoff.md` §4 carried Phase 17's verdict forward — "types are a comprehension lever,
+not a byte lever: struct-ification is byte-neutral by construction as long as the layout and the access widths are right" — and
+`include/common.h:28-30` says `M2C_FIELD(p,t,o)` has "identical codegen to `p->field`". Phase 36 had found, one agent at a time, that the
+last third of its lever residue was signatures, structs, carves and one GTE spelling; its evidence (d10's register union, a18's
+`extern u16 X[]`, f10's body-local structs, f3's `s16[3]` arrays) said the struct spelling was the MISSING piece.
+**What the gate-1 reading found.** The claim is false in gcc 2.7.2 and the tree already knew it (cookbook §30/§351/§379/§469,
+`docs/gcc-2.7.2-map/cse_expr.md` §4): `*(T*)(p+k)` gets no `MEM_IN_STRUCT_P`, `p->f`/`q[k]`/`*(Blk*)p` do, and `sched.c:830-865`'s
+alias escape and cse's kill table read that flag. Phase 17 had measured m2c DRAFTING with a struct in context (0/10/2 on a
+structural-miss sample), not member-vs-cast spelling in a matched body — and the project generalised it into "types can wait". A Plan
+agent's stress-test then corrected four more premises before any tool ran: caller bytes DO depend on a narrow prototype's widths in both
+directions (`c-typeck.c:1740` converts to the parameter type, then re-promotes); a whole-object oracle says DIFFERS on a correct
+global-block edit because the relocation SPELLING changes while the linked bytes are identical (proven: f3's `D_801F8870[3]`, object
+DIFFERS, `make build BINARY=md_SC07_004` byte-identical); a function's identity is never its bare name (6,415 names carry more than one
+body text); and "one definition per layout" is too strong (`{s16 ×4}` carries 177 names, some genuinely different types) — a duplicate is
+same layout AND identical meaningful member names, an opaque copy block, or flow evidence.
+**The decisions (Drew, gate 1).** Grind to zero on raw casts, lying declarations and levers, with each "zero" defined so it is honest and
+reachable (reinterpret macros/unions for the genuine reinterpretations; byte-proven K&R sites counted apart; legitimate GTE sequences as
+project-local macros in the one header); placeholder names + cited evidence only; the P36 agent lane at his cap, started on his word; the
+full canonical declaration layer. After T0 he asked whether the phase should first analyse every function and build a map of every needed
+struct — my answer: yes, as T1's deliverable (the struct map), because it makes "zero casts" a checkable statement and lets T4's prototypes
+carry the parameter types in one pass; he approved. Also: no tags or releases at phase closes (the `v2.2.0` tag deleted; the P36 close
+pushed as a normal commit on his word).
+**What the census and the probe then measured (T1/T2).** 503,016 raw dereferences in four forms (the readability series' regex saw one,
+411,850 over raw text) + 18,912 address-of + 36,681 typed cast-member accesses that are already COMPONENT_REFs; 7,255 struct definitions
+(5,995 inside `.c` files) over 527 layouts; a `--strict` lever finish line of 4,010 + 6,717 direct GTE statements + 314 per-TU macros.
+Rung 1 (body-local `((T *)base)->unkK`) is byte-neutral on 126/139 judged bodies; the 13 DIFFERS are the `/s` asymmetry between a
+converted pointer base and the body's bare fixed globals, and a per-site greedy (rung S2) closes all 13 with 27 of 716 sites kept as
+casts — **the `/s` flag is a dial the original set per ACCESS, so a uniform conversion is the first rung and a per-site minimal kept-cast
+set is the second, before any agent.** The definition's ANSI signature is a free declaration for 92.6 % of (TU, callee) pairs with zero
+DIFFERS; the refusals are arity contradictions (K&R sites) and multi-spelled callees. The layout engine agrees with cc1 on 5,283
+definitions after the audit found two real gaps in it.
+**Hindsight (the better path).** (1) Re-read the compiler map before restating a Phase-17 verdict in a charter: the `/s` model was in
+the tree's own documents for two months while the handoff said "byte-neutral". (2) Price a struct campaign by a byte probe on the tree
+(one afternoon) before designing the engine — the probe fixed the rewrite table five times. (3) Build the disagreeing oracle for the new
+question BEFORE the campaign (R104): the relocation-spelling control took one binary build to settle and would otherwise have read every
+global-block conversion as a failure.
diff --git a/docs/gen3-handoff.md b/docs/gen3-handoff.md
index 7e3ef986d..a35ae91b5 100644
--- a/docs/gen3-handoff.md
+++ b/docs/gen3-handoff.md
@@ -162,6 +162,9 @@ Two subtleties the census hides:
it to the decompiler produced identical bytes (`docs/struct-core-pivot.md`): the compiler does not care what you call
a field. That is good news for Gen3 — struct-ification is byte-neutral by construction *as long as the layout and
the access widths are right* — and it is why every such edit still goes through the gate.
+ *Corrected 2026-09-12 (Phase 37 T2): in gcc 2.7.2 the SPELLING moves bytes — a member/array access carries `MEM_IN_STRUCT_P`, a cast on a
+ sum does not, and the scheduler's alias escape and cse read that flag; a 165-body probe was byte-neutral on 90.6 % and a per-site
+ minimal kept-cast set closed the rest (cookbook §458, `docs/decision-log.md` P37 S106). The gate is the arbiter, per access.*
## 5. Levers Gen3 inherits
diff --git a/docs/matching-cookbook.md b/docs/matching-cookbook.md
index cfc2e23e8..6644112c0 100644
--- a/docs/matching-cookbook.md
+++ b/docs/matching-cookbook.md
@@ -37882,3 +37882,22 @@ closed; the ledger (`.run/P36/delever/ledger.jsonl`) is the record of which rung
**The do-while tell.** Both marked do-while closes this session were reference-weight levers with the same arithmetic: the
loser needs EXACTLY +1 flow-time ref (`flow.c:2067`) on a statement that does not mention the winner; a real callee argument
does it when one exists — never a fake one.
+
+## §458 — Struct spelling is a per-access dial: the T2 probe's byte evidence (Phase 37 S106)
+
+**The fact.** In gcc 2.7.2 `*(T *)(p + k)` is a MEM without `MEM_IN_STRUCT_P`; `((S *)p)->f`, `p->f`, `q[k]` (typed `q`) and `*(Blk *)p`
+carry it (`expr.c:4568-4577`, `:4888`); `sched.c:837-865` lets a `/s` varying-address non-QImode access pass a non-`/s` FIXED-address one,
+and cse's kill table differs by `/s` (`docs/gcc-2.7.2-map/cse_expr.md` §4). So converting a cast to a member is a byte edit.
+**The measurement (`tools/restruct.py --probe`, 165 bodies, seed 37).** Rung 1 — every raw cast on ONE base of a body → `((P37S *)base)->unkK`
+with the type built from the struct map — is IDENTICAL on 126/139 judged bodies (716 sites moved); the 13 DIFFERS are COUNT/ORDER
+residuals, never a register change. Rung S2 — leave one site as a cast, else revert cumulatively in source order and minimise — closes
+13/13 with 27 casts kept.
+**The byte-read exemplar: `func_801814AC` (ov_SC05_010, `src/ov_SC05_010/ov_SC05_010_jr_80180F84.c:3030`).** Six `D_801C7E30`-based sites
+(`*(u16 *)(D_801C7E30 + 0x34) = 0; … + 2) = 9; *(u8 *)(… + 0xC2) = 0xE; var = D_801C7E30; …`). Every site a member EXCEPT the `+0x34` store →
+IDENTICAL. That store a member → the next statement's reload of the pointer global (`lui/lw D_801C7E30`, fixed, non-`/s`) hoists above the
+store (`/s`, varying): `nop; sh zero,52(v0); lui …` becomes `lui …; sh zero,52(v0)`. The pointer global itself read as a struct member
+(`((struct { S *p; } *)&D_801C7E30)->p` — both sides `/s`, they conflict, the order is kept) fixes that store — and moves a `li v0,10`
+belonging to the `param_1 + 0x1C` store elsewhere. The original therefore set `/s` per access; there is no uniform spelling of this body.
+**The rule.** Convert uniformly first (rung 1), then find the minimal kept-cast set (S2), then hand the kept casts to the readings (which
+side of each pair the original spelled scalar — a walked pointer, a reinterpretation, a cached global). A kept cast is not a fake: it is the
+original's spelling until a reading proves otherwise, counted and ledgered with its pass (SCHED-ALIAS here). Related: §30, §351, §379, §469.
diff --git a/docs/wiki/Where-the-project-goes-next.md b/docs/wiki/Where-the-project-goes-next.md
index 8592dfdce..5054e1a86 100644
--- a/docs/wiki/Where-the-project-goes-next.md
+++ b/docs/wiki/Where-the-project-goes-next.md
@@ -64,7 +64,7 @@ wrongly."* To that the project adds four rules of its own, because its code was
**The one invariant.** Every Gen3 edit is gated exactly like a match was: the per-binary check for every binary a change
touches, and the clean fleet run after anything that touches a shared body, a shared header or the executable. A rename
is a symbol-file change mirrored into Ghidra by the headless script, never a hand edit of assembly. A shared body is one
-source and up to 138 binaries, so the gate runs on all of them. Types are a comprehension lever, not a byte lever — the
+source and up to 138 binaries, so the gate runs on all of them. Types are a comprehension lever, not a byte lever — the *(Corrected 2026-09-12, Phase 37 T2: the struct SPELLING does move bytes in gcc 2.7.2 through `MEM_IN_STRUCT_P` — byte-neutral on 90.6 % of a 165-body probe, the rest closed per site; the gate decides every access. Cookbook §458.)*
compiler does not care what a field is called — which is exactly why struct work is safe and exactly why it still goes
through the gate.
diff --git a/phase-ends/CURRENT_PHASE.md b/phase-ends/CURRENT_PHASE.md
index 2fd3166f5..acfe68b45 100644
--- a/phase-ends/CURRENT_PHASE.md
+++ b/phase-ends/CURRENT_PHASE.md
@@ -271,66 +271,146 @@ bucketed by what each bucket needs before any plan is proposed. **Candidates for
probe tables above · the census re-run `type_census: 7255 definitions (527 layouts, 206 duplicate classes, 40 variant camps) · 503016
cast sites … coverage OK … controls 4/4`; `readability_progress --snapshot` (a T2 row) + `progress.py --check` fresh.
-## 🛑 SESSION CHECKPOINT — S106 (2026-09-11/12, LIVE — refreshed at every task close): gate 1 APPROVED, **T0 ☑ T1 ☑ T2 ☑** (census, map, the probe — every number in the T2 log entry) — 🛑 **T3 NEXT (the tools: `restruct.py`'s full form, Max design)** | R22 `check-all: 218 passed, 0 failed of 218` at `db212f167` | the P36 close `79b2f6f15` is committed AND PUSHED (`origin/main`); no tag (Drew: no tags/releases at closes); T0 `0a55cb0fd` local
+## 🛑 SESSION CHECKPOINT — S106 (2026-09-11/12, FINAL — written for a FRESH session; the session's last commit follows this): gate 1 APPROVED, **T0 ☑ T1 ☑ T2 ☑** (baseline · the type census + the struct map · the probe) — 🛑 **T3 NEXT (the tools: `restruct.py`'s full form + the linked-relocation oracle + `struct_layout.py` + the canonical type writer; Max for the design, xHigh to finish)** | R22 `check-all: 218 passed, 0 failed of 218` at `db212f167` (no `src/` byte changed since) | HEAD after this commit is the checkpoint's commit; tree clean; nothing pushed after `79b2f6f15`
-**Replay this block into the chat at the next session start (R64); it is the ONLY in-phase context the next session inherits.**
+**Replay this block into the chat at the next session start (R64); it is the ONLY in-phase context the next session inherits. Everything below
+is what S106 knew and the next session must not re-derive.**
-1. **Where we are.** Phase 37 (the structs phase) opened 2026-09-11 in S106 at Max. Gate 1 approved with Drew's four decisions: **grind to
- zero** (raw casts, lying declarations, levers — the plan's §"What zero means" defines each honestly); **placeholders + cited evidence**
- for every type/field name; **T7 = the P36 agent lane at Drew's cap, started only on his word**; **the full canonical declaration layer**.
- R107–R117 ratified. T0, T1 (census + map) and T2 (the probe; its five measurements and the pricing are the T2 log entry) are
- committed. **NEXT = T3 — the tools (Max for the design, xHigh to finish):** grow `tools/restruct.py` from its T2 form into the engine —
- rungs L (definition fold WITH member renames), D (the declaration solver: per callee, every declaration in the TU replaced; admissible =
- the definition's TU and every caller TU IDENTICAL; the named causes: definition width/return/arity, K&R sites marked `// K&R:`), S (rung 1
- per base, all bases of a body), S2 (the greedy kept-cast set), S+A (levers stripped), X (the residual classified by pass), R (recipes),
- the ledger `.run/P37/restruct/ledger.jsonl` keyed alias+tu+fn+body-hash with per-site verdicts, `inflight.json` the only restore,
- `--plan/--apply/--only/--batch/--label`, workers own whole TUs, header edits serialized and judged on every includer with ONE retry of a
- COMPILE-ERROR, selftest + a known-true no-op run + negative controls; `tools/struct_layout.py` factored out of the census (the o32
- engine, now cc1-validated on 5,283 definitions) with the field assertion; the canonical type files' writer (final style: hex offset
- comments, `unk`, `// size = 0x..`; `Unkstruct_`; PsyQ names for SDK-identical layouts; one file per symbol space); the
- reinterpret macros in `include/common.h`; **the linked-relocation oracle mode** (the f3 control: object DIFFERS, binary identical) with
- its known-true and negative controls, and the committed linker fragment for any new base symbol; `tools/restruct_cycle.sh` (detached).
- Verify: selftest; the calibration line; one batch on one overlay end-to-end with R22 green.
-2. **What the next session must know (T1's findings, for T2–T5).** (a) The phase's counters, re-derived by `tools/type_census.py` (coverage OK, controls 4/4): **503,016 raw dereferences** (P 409,007 +
- I 60,666 + X 13,800 + M 19,543) in 69,497 bodies + 18,912 address-of + 36,681 typed cast-member (rung 1's form, counted apart);
- 7,261 struct definitions (1,179 canonical, 5,995 inside `.c`), 525 layouts, 206 duplicate classes (2,789 names; 46 layout-twin classes /
- 1,844 names are NOT duplicates without flow evidence), 39 VARIANT names, 141 dead canonical names; 98,648 lying declarations / 1,609
- callees; 6,415 names with >1 body text (identity = body hash, never the name); **the lever finish line is 4,010 marked sites + 6,717
- direct GTE statements in bodies + 314 per-TU asm macro definitions** (`lever_census --check --strict`, now literal). (b) The struct map
- (`.run/P37/census/struct_map_top.json`, `docs/struct-map.md`): 18,760 types explain 99.2 % of the sites; the two largest — 107,598 sites /
- 0x10C over 9,753 pointer globals (partly fused at low offsets) and 65,397 sites / 0x24C with the instance global `D_80126B58` (the
- actor record) — carry width conflicts the map reports; the map's merge rules are in SETUP §P37 S106; it is a MODEL — T2 tests it on
- bytes (does the member spelling hold?) before T5 authors anything from it. (c) The Plan agent's four corrections (the log entry "T1
- design"): the relocation-spelling oracle mode + linker fragment for base symbols (T3), body-hash identity (done), form C (done),
- duplicates = layout + evidence (done; milestone item 1 reads "per type"). (d) Two doc corrections owed at T9: `actor-struct.md`'s
- `s16` at +0x108/+0x10C (the code declares 4-byte fields; the bytes decide), and `include/common.h:28-30`'s "identical codegen to
- `p->field`" for `M2C_FIELD` (false — the `/s` flag differs; `gen3-handoff.md:161-164` says the same). (e) The 24 parked
- P36 classes are now ONE file, `.run/P37/census/parked.tsv` (fn, alias, tu, copies, sites, kinds, regs, needs {signature 13, struct 7, ? 4},
- pack, patches, headline; 453 bodies / 691 needed sites; 22 more PARK names are minimum-lever banks) — generated by the census from the
- P36 ledger + `pick.py`'s PARK set + each pack's `mechanism.md`; patch files exist for 6 (`main__func_800385C0/scratch/signature.patch`,
- `main__func_80029D3C/scratch/patch_signature.c`, `ov_SC07_007__func_80180FA4/scratch/signature_3args.patch`,
- `ov_SC02_027__func_80180A54/scratch/signature.patch`, `md_SC07_004__func_801A1E94/signature.diff`, `…__func_801A5C44/signature.diff`),
- whole-TU scratch copies for 8 more; `func_80180200` has no pack (it lives in `ov_SC03_115__func_8017FC5C/`). (f) `func_8013D9B0`'s 268
- markers (kind `gte-unsigned`) are now inside `--strict`'s direct-GTE count (resolved: they are asm statements outside the header, to
- become macro calls). `lever_census`'s controls are re-keyed and 4/4 OK. (g) The 4,010 reconcile exactly against the ledger: 1,637 drawable RESIDUE + 1,080 minimum-lever banks in PARK + 593 parked RESIDUE +
- 450 GTE clobber variants (`gte-lever`; 64 per-TU `_m` macro definitions, `gte_rt_m` 55) + 94 arg-only pins (83 classes) + 83 whose latest
- ledger row is a failed attempt (invisible to `pick.py`; `delever.py --redraw`) + 24 + 18 + 13 asm-body + 10 drawn + 8 file-scope.
- (h) The §396(a) refuting body IS recorded: `.run/P36/agents/ov_SC03_110__func_801800F0/mechanism.md:64-70` (d11) — T9 writes the correction.
- (i) The type tools' reach: `lift_types.py` scans only `src/ov_*/*.c` (line 49); `engine_types.h` is included by 3,818 TUs via
- `engine_prelude.h` but by 0 of the 85 root `.c` and 12 of 87 `md_*`; `decl_repair.py` compares `.text/.rodata/.data` sections (the scratch
- path lands in the object) while `delever_oracle` compares whole objects compiled in place.
-3. **Environment.** `~/bfm-decomp` on ext4; `.venv/bin/python`; every build with `JOBS=16`/`-j`; the headless Ghidra MCP was started by
- the SessionStart hook (pid in `.run/ghidra-mcp.log`) and is NOT needed until T8 — stop it via the sentinel before any commit that touches
- `ghidra/` (nothing does until T8; R23). The oracle's snapshot baseline: `.run/P36/delever/baseline/` (7,428 objects at `db212f167`, taken
- after the green R22 — R112). The Plan agent (tool design stress-test) may still be running or have reported: its report goes into T3's
- design notes; if it is lost, T3's design at Max re-derives it from the plan's §B questions.
-4. **Commands that work.** Probe/engine: `.venv/bin/python tools/restruct.py --try [--base bclass:name]`; `--probe --seed 37 -j 12`
- (6.5 min); `--rerun-differs`; `--probe-decls -j 12` (16 min); `--probe-defs`; `--audit-layouts -j 8` (80 s); `--fanout-cost -j 16` (35 s;
- NOTHING else may compile meanwhile — it writes `engine_types.h` in place). Census: `.venv/bin/python tools/type_census.py -j 16 [--sites]` (≈3.5 min; `--selftest` 21/21; outputs under
- `.run/P37/census/`; `docs/struct-map.md` regenerated). R22: `make clean && make extract-all JOBS=16 && make check-all JOBS=16` (≈86 s; read the exit code with
- `pipefail`). Oracle: `.venv/bin/python tools/delever_oracle.py --calibrate -j 16` (147/147 in 2.3 s). Census (P36's, for the
- lever side): `.venv/bin/python tools/lever_census.py --check -j 16`. Series: `tools/readability_progress.py --snapshot