# homemaker — Design & Plan **Status:** validated direction, pre-implementation. Reviewed against the Urb source 2026-06-12; review findings folded in (see §4.5 evidence note, §4.6 throughput arithmetic, §5 decision 6, §6 port-scope expansion, §7 re-scoped phases, §8). **Audience:** a fresh session that will break this into `bd` (beads) tasks (note: no beads database exists yet — run `bd init` first). Self-contained — assumes no memory of the originating conversation. --- ## 1. Purpose `homemaker-layout` is a clean-room Python successor to the Perl **Urb** project (`/home/bruno/src/urb`). Urb models a building as a binary **slicing tree** and evolves layouts with mutation + crossover, scored against Christopher Alexander–style pattern fitness. Two long-standing problems motivate the rewrite: 1. **It doesn't scale** — beyond a few rooms, evolution never finds layouts an architect would consider obvious. 2. **Local minima** — even small programmes converge to poor optima. The eventual goal is a **100% Python** system. During bring-up, Perl Urb is kept as a throwaway **fitness oracle** behind the `.dom` file format. --- ## 2. Constraints that fix the representation These come from the problem domain and are **not negotiable**; importantly, they *vindicate* the slicing tree rather than argue against it: - **Multi-storey with stacked walls.** An upper storey retains the storey below, except additional divisions/undivisions. Load-bearing walls must stack ⇒ every cut is a full edge-to-edge **guillotine** cut. Urb already enforces this via `Below`-inheritance (an upper quad reads its geometry from the matching quad below). - **Quadrilateral rooms only** (no L/Z shapes) — recursive bisection produces exactly this. - **No pinwheel / non-slicing layouts** — undesirable for load-bearing construction and adaptability (cf. Brand, *How Buildings Learn*). This is the one class a slicing tree *can't* express, and we don't want it anyway. - **Plots are near-rectangular but general convex quadrilaterals** (not axis-aligned). Geometry must handle skew; the slicing *combinatorics* are unaffected. **Conclusion:** the slicing tree is the correct phenotype. The rewrite is about the *genotype*, the *search*, and the *fitness shape* — not about leaving the slicing class. --- ## 3. What we built this session (all committed) Package `src/homemaker_layout/`: - **`dom.py`** — `.dom` YAML ⇄ `Node` tree. Linkage (`parent`/`below`/`position`), `wall_outer` inset on load with raw-corner stash for byte-perfect round-trip, emit. - **`geometry.py`** — faithful port of Urb's top-down geometry (`Coordinate`/`Coordinate_a`/`_b`/`Area`/`Length`) + `Coordinate_Offset` wall inset. **Memoised** (uncached recursion is exponential in depth). - **`programme.py`** — parse `patterns.config` `spaces:` into per-code size/width/proportion/adjacency/level/count requirements. - **`solver.py`** — bottom-up division-ratio solver (scipy `least_squares`). *(Outcome: falsified as a standalone component — see §4.2.)* - **`oracle.py`** — Phase-1 fitness bridge: write `.dom`, run `urb-fitness.pl`, parse `.score` + `.fails`. Experiments in `experiments/`: `dump_areas.{py,pl}`, `resolve_ratios.py`, `refine_sweep.py`, `sweep_failtypes.py`, `optimize_fullfitness.py`. --- ## 4. Empirical findings (the core of this document) ### 4.1 Geometry port — VALIDATED Per-leaf areas computed in Python are **byte-identical to Urb across all 35 programme-house `.dom` files**, including the wall inset and multi-storey wall-stacking inheritance. (`experiments/dump_areas.{py,pl}`.) The infrastructure is trustworthy. ### 4.2 Bottom-up area-proxy sizing solver — FALSIFIED The original hypothesis: give leaves *target sizes*, solve cut ratios bottom-up, let the EA search only topology. Tested by re-solving an evolved candidate's ratios from programme targets and scoring via the oracle. - `resolve_ratios.py` on candidate-002: areas recovered accurately (errors collapsed, e.g. t1/t2/t3 from +1.4/+2.4/+4.8 → ~+0.05), and it *fixed* the original's `size` failure — **but total fitness dropped** (0.00737 → 0.00065, 4 fails) because it introduced shape/relational failures. - `refine_sweep.py` (warm-start refine of all 34 candidates): **0/34 improved.** Total failures 124 → 297 (equal-offset cuts) and 124 → 626 (independent-offset cuts). - `sweep_failtypes.py` (failure-type histogram, equal-offset): | type | area-dominant Δ | shape-aware Δ | |---|---|---| | width | +82 | +29 | | proportion | +35 | +7 | | crinkliness | +18 | +4 | | adjacency | +18 | +13 | | size | **−15** | **+15** | | access | +29 | +39 | | **total added** | +173 | +110 | **Why it fails:** in Urb's fitness, every cut position is simultaneously a *size* knob **and** an *adjacency/access/shape* knob. A solver that optimises only size/shape is blind to access/adjacency and trades them away. Refining a co-evolved local optimum with a *partial* objective is **structurally unable to win**, and the `0.5^n` failure penalty makes every new failure catastrophic while fixes are only linear. The proxy solver is strictly worse than optimising real fitness. **Do not pursue it.** ### 4.3 "Perpendicular" failures were an artifact — RESOLVED Letting the two ends of a cut float independently produced skewed cuts and many `perpendicular` failures. Tying the two ends (**equal offset, `a == b`**, one DOF per cut) produces near-perpendicular walls on these near-rectangular plots and yields **zero** `perpendicular` failures. **Equal-offset cuts are the only mode to use.** This also halves the variable count and matches the slicing model. ### 4.4 DOF / over-determination — partially real, not fatal A topology with *R* rooms has ~*R−1* cut DOF but ~2–3 size/shape constraints per room, so a *fixed* topology can be over-determined: you cannot always hit area + width + proportion for every room at once (heavy shape weighting traded straight into `size`, §4.2 table). This limits any single-objective sizing pass — but it is **not** fatal, because optimising the *full* objective still found large gains (§4.5). The earlier "infeasibility" worry was overstated. ### 4.5 Full-fitness frozen-topology optimisation — VALIDATED ✅ Drive the equal-offset ratios with Nelder-Mead against the **real oracle fitness** (whole objective, no proxy), topology frozen (`experiments/optimize_fullfitness.py`): | candidate | DOF | original | optimised | gain | fails | |---|---|---|---|---|---| | 2f45907 (best evolved) | 7 | 0.012617 | 0.015684 | ×1.24 | 2→2 | | candidate-002 (MCP-refined) | 6 | 0.007375 | 0.012319 | ×1.67 | 2→2 | | c964435 (MCP baseline) | 6 | 0.003667 | 0.005836 | ×1.59 | 3→3 | **Every design improved 24–67%, none added a failure.** Headroom *widens* on weaker designs. Because the optimiser sees the whole objective (including the `0.5^n` penalty), it never trades into a new failure — **the cliff that destroys the proxy solver protects the full-objective optimiser.** **Implications:** - There is large, unclaimed **geometry headroom above every EA design** — even the best. Urb's EA under-optimises geometry: source inspection confirms `slide()` (Mutate.pm:256-269) *re-randomises* the cut position uniformly across the span — Urb has **no fine-tuning geometry operator at all**, which fully explains the headroom. - A **full-objective geometry inner loop is genuinely valuable** (the proxy solver is not). - The EA/search should therefore own **topology**; geometry is delegated to the inner loop. This is the memetic architecture (§5). - Corroboration for §4.3: Urb's own mutations use equal offsets (`Divide($division, $division)`) — equal-offset cuts match how every corpus design was generated. ### 4.6 Oracle throughput (measured) `urb-fitness.pl` scores **many `.dom` files per invocation**, so the Perl startup (~0.65 s) amortises across a batch and cached fields (e.g. occlusion) persist. Measured on the 35-file corpus: **0.99 s/dom batched** vs **1.65 s/dom** for a single-file call. The cost is **assessment-dominated** (~1 s/dom of actual work), so startup amortisation gives ~40% — useful but bounded. Consequences: - **Batching only helps when evaluations are submitted together** — favour **population/parallel-evaluating optimisers** (CMA-ES, differential evolution, island EA, pattern search) over inherently sequential ones (Nelder-Mead), both inner loop and outer search, so a whole generation scores in one oracle call. - **Do the arithmetic before scoping topology search on the oracle.** §4.5 used ~200 inner evaluations per topology ⇒ ~3 min/topology at 1 s/dom. A run comparable to `urb-evolve` (pop 128 × 768 generations) is *years* of oracle time; even 32 topologies × 100 generations with a trimmed 50-eval inner loop is ~2 days. Therefore: - The oracle supports **Phase 1 fully** and **Phase 2 only as a small-scale proof** (tens of topologies, budgets counted in oracle calls). - A **native Python fitness is effectively a gate for topology search at any real scale** — not merely a later optimisation. (It also brings independence, penalty reshaping, and large programmes.) - **Warm-starting the inner loop from the parent's optimised ratios** (Lamarckian inheritance, §5 decision 6) is the main lever for cutting the per-topology cost — with high-locality moves most cuts survive a mutation, so an order-of-magnitude reduction is plausible. Measure this in Phase 1. ### 4.7 Occlusion-disabled re-baseline (measured 2026-06-12) With the §6 descope in place (`URB_NO_OCCLUSION=1` patch in Urb), the corpus re-baseline (`experiments/rebaseline_no_occlusion.py`): all 35 scores change (mostly up, ×1.0–×1.24 — daylight terms pin to 1), exactly one failure-set change (458aa8b8 gains two `crinkliness` fails — expected mechanism: no shading discount on external wall area), batched oracle ~8% faster (0.92 s/dom). New inner-loop reference gains (deterministic seed, budget 400, `accept_innerloop.py` bars): 2f45907 0.01304→0.02128 (×1.63), candidate-002 0.00808→0.01373 (×1.70), c964435 0.00400→0.00674 (×1.68, fails 3→2); ~35 oracle calls per topology. All Phase-2+ work uses the flag; flag-off numbers above are historical. ### 4.8 The `0.5^n` failure penalty is a first-order pathology Multiplicative `0.5^n` over failure *count* (a) makes the landscape a cliff (no gradient across the huge zero-feasibility region), (b) rewards fewer *flags* over better *geometry* (the original outscored better-sized solved designs purely on flag count), and (c) is representation-independent. Reshaping it (additive / soft / multi-objective Pareto) is a high-leverage change that helps Urb today and homemaker tomorrow. ### 4.9 Penalty reshaping decision: lexicographic outer search (measured 2026-06-14) `experiments/penalty_reshape.py`, `URB_NO_OCCLUSION=1`, programme-house. **Inner-loop protection** (nm_search, budget 80, 3 files × 3 seeds = 9 runs): All runs show `n_fails ≤ x0_n_fails`. **0/9 regressions.** The `0.5^n` cliff in the native fitness scalar is unchanged and continues to protect the inner loop. **Outer-search comparison** (budget 3000, 3 seeds, seed = 2f45907): | scheme | seed | best | fails | note | |--------|------|------|-------|------| | lex | 0 | 0.01781 | 2 | | | lex | 1 | 0.01793 | 2 | | | lex | 2 | 0.01785 | 2 | | | scalar | 0 | 0.01781 | 2 | (same outcome) | | scalar | 1 | **0.01890** | **3** | trapped by high-score 3-fail design | | scalar | 2 | 0.02632 | 2 | (different topology path) | `lex` mean: 0.01786 / 2.00 fails. `scalar` mean: 0.02101 / 2.33 fails. Key result (seed 1): scalar promoted a 3-fail design whose raw score (×0.125 penalty) beat the pool's 2-fail candidates — exactly the §4.8 pathology. Lexicographic comparison (`-n_fails` first, then `fitness`) is immune: any 2-fail design beats any 3-fail design regardless of raw score. Within a homogeneous fail tier both schemes are identical (seeds 0 and 2 agree in serendipitous runs where scalar also stays in the 2-fail tier). **Decision: lexicographic. `0.5^n` stays in the fitness scalar (inner loop unchanged). Outer search uses `(-n_fails, fitness)` as comparison key.** ### 4.10 Deceptive level-fix valley and compound operators (measured 2026-06-14/15) **Context:** programme-house, Phase 3 native fitness + Phase 4 lex search, seed `warmstart-2f4.dom` (best Phase-3 result, 2 fails at score 0.032). Goal: reach ≤ 1 fail, beating the Perl optimiser (2–3 fails). **The deceptive valley.** The 2-fail state has l1 (living room, min 27 m², required level 0) on level 1. The obvious repair is `level_fix`: swap l1 with a leaf on level 0. But every single-step `level_fix` move creates 5+ new fails because the displaced room (t3, the WC) is dropped into an arbitrary slot that violates adjacency, size, and access constraints simultaneously. The lex comparator (`-n_fails, fitness`) correctly rejects these — but the result is that the 2-fail state appears completely surrounded by ≥ 5-fail states, and the search stalls. This is a textbook deceptive valley: the fitness gradient points away from the global optimum. **Compound operator.** `mutate_level_compound_fix` (added `operators.py`) escapes the valley by doing two things atomically: 1. Move l1 to level 0 by swapping it with the *largest* leaf there (the circulation C node, because C is generic and can absorb the swap without producing a new structural failure). 2. Re-insert the displaced t3 by dividing the sibling of that C node (so t3 lands adjacent to C, satisfying the adjacency requirement). The new split gets `division=[0.25,0.25]` (giving t3 ≈ 3.4 m², barely in range) and `rotation=0` (t3 on the left, adjacent to the C sibling). **The `warm_x0` initialization bug.** The compound operator sets specific ratios on a newly-created split node. But `driver.py` was initialising the NM inner loop from `parent.ratios`, which has no entry for the new node (it was a leaf). `warm_x0` defaulted the new node to 0.5, giving t3 ≈ 6.8 m² — a size fail — so NM started at 3 fails instead of 1. Lex then always rejected the compound child; `level_compound_fix` was completely invisible to the outer search for ~12 000 evals (until `warm_x0` was fixed). The correct fix distinguishes genuinely-new split nodes from stale hidden nodes that become visible after structural mutations (e.g. `swap` can flip a `b.below` pointer, revealing pre-writeback division values from a different topology). Only use the child's explicit ratio for node `(li, path)` if the matching node in the parent was *not already divided*; everything else falls through to `parent.ratios` or defaults to 0.5. Fix in `driver.py` lines 259–267. **Results (50 000 evals each, pop 8, child_budget 80, 4 workers):** | seed | event | eval | fails | score | |------|-------|------|-------|-------| | warmstart-2f4 | seed | 200 | 2 | 0.032 | | warmstart-2f4 | `level_compound_fix` fires | 12 280 | 1 | 0.000122 | | warmstart-2f4 | `level_retype 0/ll<->1/l` | 17 880 | 1 | 0.00497 | | warmstart-2f4 | final | 50 040 | **1** | **0.00518** | | compound3-raw | seed (1-fail hand-built) | 200 | 1 | 0.000118 | | compound3-raw | `level_retype 0/ll<->1/l` | 18 360 | 1 | 0.00383 | | compound3-raw | final | 50 040 | **1** | **0.00523** | Perl optimiser reference: **2–3 fails**. **The two-C topology breakthrough.** After `level_compound_fix` fires, the topology is: level 0 = `ll(l1), lr(t2), rl(C), rrl(t3), rrr(O)` — but now l1 is at level 0 (correct) and t3 is adjacent to rl(C) (staircase). However l1 is occupying ll, and rl(C) is the staircase core — so t3-adj-C is satisfied via rl, but there is no second C to satisfy staircase independently. Score ≈ 0.000157 (1 fail). At eval ≈ 18 000, `level_retype 0/ll<->1/l` (swap the type of ll on level 0 with l on level 1) creates a TWO-C configuration at level 0: `ll(C), lr(t2), rl(C), rrl(t3), rrr(O)`, with l1 moving to level 1. The score jumps 25× to ≈ 0.005. Why two C nodes work: - `ll(C)` (bottom-left, 23 m²) satisfies t3-adj-C via geometric contact at the l/r zone boundary with `rrl(t3)`. - `rl(C)` (top-right, 8.5 m²) satisfies staircase adjacency via tree adjacency to `rrr(O)` (its right sibling when `r.rotation=3`). Both constraints are simultaneously met because binary-tree sibling adjacency and cross-zone geometric adjacency provide *independent* paths. **Why 0 fails is geometrically impossible on this programme + plot.** l1 needs min 27 m² at level 0. The only space large enough is `ll` (≈ 23 m², the entire left half of level 0). Putting l1 at `ll` removes the t3-adj-C provider. The alternative — dividing `ll` into `lll(l1)+llr(C)` — gives `llr` a proportion of ≈ 6:1 (width ≈ 0.73 m), failing both the proportion and width constraints. 0 fails is not achievable on this programme+plot with a binary slicing tree representation; 1 fail is the geometric optimum. --- ## 5. Validated architecture **Memetic search, full objective throughout:** ``` ┌─────────────────────── topology search (OUTER) ───────────────────────┐ │ genome = slicing topology + per-leaf type assignment + per-floor │ │ divide/undivide deltas (base floor is master) │ │ operators = high-locality topology moves (see §6) │ │ │ │ for each proposed topology: │ │ ┌──────────── geometry inner loop ────────────┐ │ │ │ optimise equal-offset cut ratios (1 DOF/cut) │ │ │ │ against the FULL fitness (derivative-free / │ │ │ │ gradient), to convergence │ │ │ └──────────────────────────────────────────────┘ │ │ score = best full-fitness over inner loop │ └──────────────────────────────────────────────────────────────────────────┘ fitness: NATIVE Python (fast), reshaped penalty ``` Key decisions, all evidence-backed: 1. **Geometry = inner optimisation against full fitness** (§4.5), *not* an area proxy (§4.2). Equal-offset cuts, one DOF per free branch (§4.3). 2. **Search owns topology only.** The base-floor tree is the primary genome; per-floor deltas are a small secondary genome (multi-storey constraint as a regulariser, via `Below`-inheritance). 3. **Prefer population/batch-evaluating optimisers** so the batched oracle is efficient (§4.6). A **native Python fitness** (faithful to Urb, validated against the oracle on the 35-file corpus) **gates topology search at scale** (§4.6 arithmetic); the oracle suffices for the inner loop and a small-scale topology-search proof only. 4. **Reshape the failure penalty** (§4.8) — additive/soft or multi-objective — so the search has a gradient and isn't dominated by flag-count. **Caution:** the `0.5^n` cliff is what *protects* the inner loop from trading into new failures (§4.5); reshaping must not lose that property. Candidate resolutions: keep the cliff inside the inner loop only, lexicographic ordering (failure count first, score second), or genuine multi-objective Pareto. Decide in Phase 4 with measurements. 5. **Representation upgrade (later):** canonical slicing encoding (normalized Polish expression / skewed slicing tree, Wong–Liu) for redundancy-free, high-locality topology moves; bottom-up shape feasibility checks. Defer until the inner loop + native fitness are in place. 6. **Lamarckian geometry inheritance.** A child topology's inner loop warm-starts from the parent's optimised ratios (cuts that survive the topology move keep their values; new cuts get heuristic defaults). This is the main cost lever for the memetic loop (§4.6) and a standard memetic design choice (Lamarckian vs Baldwinian — we write the optimised geometry back into the genome). Validate the warm-vs-cold speedup in Phase 1. What we are **not** doing: the bottom-up area-proxy solver; independent-offset cuts; non-slicing representations (sequence-pair/B*-tree — excluded by §2). --- ## 6. Component plan | component | status | notes | |---|---|---| | `dom.py` (I/O + linkage) | ✅ done | round-trips byte-perfect; keep | | `geometry.py` (port + cache) | ✅ done, validated | the trusted geometry kernel | | `programme.py` | ✅ done | extend as fitness needs grow | | `oracle.py` (Perl bridge) | ✅ done | throwaway; the validation reference | | `solver.py` (area proxy) | ⚠️ keep as artifact | falsified; do not build on it | | **geometry inner loop** | ❌ to build | full-objective ratio optimiser (DOF = free branches); batch/population so the oracle batches; warm-start support (§5.6) | | **topology genome + operators** | ❌ to build | base tree + per-floor deltas; high-locality moves | | **search driver** | ❌ to build | memetic EA / SA over topology; small-scale on oracle, full-scale needs native fitness | | **native fitness** | ❌ to build | **gates topology search at scale** (§4.6); port + validate vs oracle; scope is larger than the term list — see below | | **penalty reshaping** | ❌ to design | additive/soft or multi-objective; must preserve inner-loop cliff protection (§5.4) | | canonical encoding (Polish expr.) | ❌ later | representation upgrade once core lands | Urb fitness terms the native port must reproduce (all couple to geometry): **size, width, proportion, adjacency, access/inaccessible, crinkliness, perpendicular, level, staircase volume/count, public access, circulation & outside ratios, min internal area.** Source of truth: `/home/bruno/src/urb/lib/Urb/Dom/Fitness/ProgrammeDriven.pm` and the `Storey`/ `Building`/`Leaf`/`Base` submodules. **Port scope beyond the term list** (found by source review — budget for these): - **Daylight + occlusion subsystem — DESCOPED (decision 2026-06-12).** Occlusion is orthogonal to building a scalable optimiser. Instead of porting `Urb::Misc::Sun`/`Urb::Field::Occlusion`/CIESky, disable it in Urb behind an env flag (`quality_daylight` → 1 everywhere; `Crinkliness`/`Area_Outside` pins the `CIEsky_vertical` illumination factor to 1 — *simple crinkliness* = unweighted external wall area / floor area). The boundary-overlap geometry (`Dom->Walls`) stays in scope; the sky model does not. The native fitness ports simple crinkliness only; a Python occlusion subsystem is rebuilt post-Phase-5 once optimisation is fully native. **Flipping the flag changes every score** — re-baseline the corpus, the §4.5 table, and gate bars at one clean boundary, and run the Phase-2 urb-evolve benchmark under the same flag. - **The cost denominator.** Fitness is value/**cost**: per-leaf area costs, interior/exterior wall edge costs, boundary costs (Leaf.pm:194-251, Storey.pm:122-147). Cost couples to geometry too. - **Structural failures** not in the term list: "edge too long" (>8 m, two variants), "unsupported covered outside", "covered outside above ground", "level N not connected". - **Missing-space failure stacking** (ProgrammeDriven.pm:192-212): a missing space generates 2 base failures plus one per size/width/proportion/adjacency/ level requirement — up to ~7 failures. Penalty reshaping (Phase 4) must preserve this hierarchy or the search will happily drop rooms. - **Two-phase graph build**: adjacency/level/vertical checks run on the *unmerged* tree; graphs are rebuilt after `Merge_Divided` for storey processing (ProgrammeDriven.pm:83-103). Easy to get subtly wrong; the 35-file validation gate will catch it, but anticipate it. - **Known stub to decide on** (fidelity-vs-fix, §8.1): `has_vertical_connection` (ProgrammeDriven.pm:399-423) matches any leaf of the target type anywhere on the level below — no spatial-overlap check. A faithful port reproduces the bug; decide explicitly. --- ## 7. Phased roadmap - **Phase 0 — diagnostics** *(done)*: geometry port validated; proxy solver falsified; full-fitness geometry headroom validated; oracle throughput measured (~1 s/dom batched). - **Phase 1 — geometry inner loop (on batched oracle)**: full-objective ratio optimiser; use a population/batch optimiser so a generation scores in one oracle call. Reproduce/exceed the §4.5 gains. Integrate as `optimise(topology, x0=None) -> (geometry, fitness)`. Two cheap experiments belong here: (a) **warm-vs-cold start** — quantify the §5.6 speedup; (b) **optimiser bake-off** — DOF is only ≈ rooms−1, so batched multi-start pattern search may beat CMA-ES on simplicity; measure, don't commit blind. *Gate:* match §4.5 gains at materially lower oracle-call budget. - **Phase 2 — topology search, small-scale proof (on batched oracle)**: base-tree + per-floor-delta genome, high-locality operators, memetic driver wrapping the Phase-1 inner loop. **Explicitly small** (§4.6 arithmetic): tens of topologies, budgets counted in **oracle evaluations**, not generations. Compare against `urb-evolve` from the same seeds/programmes *at equal oracle-call budget* (urb-evolve has diversity injection/culling baked in, so generations are not comparable). *Gate:* memetic loop beats equal-budget urb-evolve. Scaling up waits for Phase 3. **Gate result (homemaker-py-way, 2026-06-13, `URB_NO_OCCLUSION=1`, budget 2000):** `experiments/benchmark_vs_urbevolve.py`; urb-evolve scores unchanged, memetic scores corrected (patterns.config missing from re-score cwd in first run, fixed in same session). | seed | system | best@1000 | final@2000 | fails | |------|--------|-----------|------------|-------| | init.dom | memetic | 8.84e-10 | 3.37e-09 | 18 | | init.dom | urb-evolve p16 | 9.10e-06 | 9.36e-05 | 6 | | init.dom | urb-evolve p128 | 4.83e-09 | 3.27e-05 | 6 | | c964435 | memetic | 7.65e-03 | **7.65e-03** | 2 | | c964435 | urb-evolve p16 | 4.00e-03 | 4.00e-03 | 3 | | c964435 | urb-evolve p128 | 4.00e-03 | 4.00e-03 | 3 | | 2f45907 | memetic | 2.13e-02 | **2.13e-02** | 2 | | 2f45907 | urb-evolve p16 | 1.30e-02 | 1.30e-02 | 2 | | 2f45907 | urb-evolve p128 | 1.30e-02 | 1.30e-02 | 2 | **Verdict: 2/3 seeds → REVIEW.** - *Seeded designs (c964435, 2f45907)*: memetic beats urb-evolve by 1.91× and 1.63×; topology search adds value over the inner-loop-only reference (crossover finds a better topology at eval 372 for c964435). - *Blank-slate (init.dom)*: memetic stalls at 18 fails after 2000 evals; urb-evolve reaches 6 fails. The `0.5^n` cliff means each fail adds ~2× penalty; 12-fail gap = ×4096. Root cause: single-seed topology mutation chain builds structure one room at a time; urb-evolve's random-population initialisation explores broader topology diversity upfront. **Not a regression** — this is a scope gap: blank-slate construction is harder than seeded improvement, and addressed separately (random multi-start bootstrap, or Phase 4 penalty reshaping which flattens the fail cliff). - The memetic loop is confirmed correct and competitive on the realistic use case (seeded designs). Phase 3 (native fitness) unblocks scaled runs where this gap will also narrow. - **Phase 3 — native Python fitness** (**gates scaled topology search**): first disable occlusion/daylight in Urb behind an env flag and re-baseline (§6 descope note); then port Urb's programme-driven fitness — the §6 "port scope beyond the term list" items (simple crinkliness, cost denominator, structural failures, failure stacking, two-phase graph build). Validate score + failure set against the *flagged* oracle across the 35-file corpus (float tolerance, identical failure sets). Swap behind the same interface; retire the oracle. Then re-run Phase 2 at scale. **Gate result (homemaker-py-ccw, 2026-06-13, `URB_NO_OCCLUSION=1`, budget 20000):** `experiments/run_search_scaled.py`; native fitness only, no oracle. pop_size=16, child_budget=80, seed_budget=300. 71.8 evals/s, 279.8s elapsed. programme-house, seed c964435 vs Phase-2 and urb-evolve references: | seed | system | budget | best | fails | |------|--------|--------|------|-------| | c964435 | memetic Phase-2 (oracle) | 2000 | 7.65e-03 | 2 | | c964435 | urb-evolve p16 | — | 4.00e-03 | 3 | | c964435 | urb-evolve p128 | — | 4.00e-03 | 3 | | c964435 | **memetic Phase-3 (native)** | **20000** | **1.04e-02** | **2** | **Verdict: PASS.** - Best 1.04e-02 beats Phase-2 oracle run (7.65e-03) by **1.36×** and urb-evolve p128 (4.00e-03) by **2.60×**; both at 2 fails. - Winning topology found at eval 10357 via `rotate 1/ll` — unreachable within the Phase-2 budget of 2000. - Population diverse: 16 members, all at 2 fails (top 15), range 5.99e-03–1.04e-02. - Throughput 71.8 evals/s vs ~0.5 evals/s for the batched oracle (≈140× speedup). - harbor-house (16 rooms, oracle-impossible): run attempted, results below. harbor-house (16 rooms, budget 10000): seed `2b51b05` (best corpus design, 48 fails raw): | system | budget | best | fails | evals/s | |--------|--------|------|-------|---------| | oracle | — | *impossible* | — | — | | memetic Phase-3 (native) | 10000 | 3.73e-18 | 49 | 15.8 | Search found 3.73e-18 vs seed inner-loop baseline 8.73e-19 (4.3× lift). 638 topologies in 633s. 49-fail landscape: still many fails, but topology search is finding structure (best 3 population members all at 49 fails). The 16-room programme is qualitatively beyond the oracle's capability — this run is only possible with native fitness. - **Phase 4 — penalty reshaping** *(done, homemaker-py-yg5, 2026-06-14)*: **Decision: lexicographic outer-search comparison** (see §4.9). Inner loop unchanged — still uses raw `0.5^n` fitness scalar (cliff protection preserved, §5.4). Outer search compares individuals by `(-n_fails, fitness)`: fewer fails always beats more fails; within a tier, compare by score. Implemented in `driver.search(use_lex=True)`. `_CHILD_INNER_KW` stale `sigmas` entry also removed (NM default has no `sigmas` parameter). - **Phase 5 — representation upgrade**: canonical slicing encoding (Polish expression) + bottom-up shape feasibility; scale to larger programmes. Each phase has a concrete go/no-go gate; do not advance on faith. --- ## 8. Risks & open questions (decisions for the next session) 1. **Native-fitness fidelity vs simplification.** Port Urb's fitness exactly (maximise comparability) or take the opportunity to clean up known issues (the `0.5^n` cliff, the t3 width-default contradiction below, the `has_vertical_connection` no-overlap stub — §6)? Recommend: *port faithfully first* (bugs included), validate, then reshape in Phase 4. 2. **Programme contradictions exist.** e.g. t3 (3 m² WC) inherits the 4 m `width_inside` default (Fitness/Base.pm:60) — geometrically impossible; the original "passes" only by failing `size` instead. *Confirmed in source.* Need a sane width default scaled to area, or per-room widths. 3. **Inner-loop optimiser choice — RESOLVED (homemaker-py-d0s, 2026-06-13).** Bake-off over 3 files × 4 methods × 3 seeds at budget 200 (`experiments/bakeoff_innerloop.py`), cold-start, `URB_NO_OCCLUSION=1`: | method | x@40 | x@80 | x@200 | s/eval | oracle calls | fails+ | |-------------|------|------|-------|--------|--------------|--------| | Nelder-Mead | 1.45 | 1.50 | 1.56 | 2.05 | 200 | 0 | | CMA-ES | 1.09 | 1.32 | 1.41 | 1.69 | 18 | 0 | | compass | 0.71 | 0.92 | 1.48 | 1.69 | 12 | 3 | | compass-ms | 0.71 | 0.92 | 0.92 | 1.44 | 13 | 4 | **Decision: keep CMA-ES (already the default) for the Perl oracle era.** Nelder-Mead wins quality per eval (+x0.15 at @200) but is inherently sequential — 200 Perl invocations vs 18 for CMA (§4.6 batching matters). Compass stalls on narrow-valley landscapes (2f45907: x0.62 vs x1.30) and introduces fail regressions 3/9 runs. Multi-start compass wastes budget on phase splits. **Phase 3+ note:** once native fitness replaces the oracle, oracle-call count disappears. Revisit Nelder-Mead then — its quality advantage is real. Gradient-based (autograd through native fitness) is also an option. 4. **Search algorithm for topology.** Memetic GA (keep crossover — now meaningful, since a subtree = a contiguous region) vs simulated annealing (the floorplanning workhorse with M1/M2/M3 moves on Polish expressions). 5. **Penalty reshaping vs inner-loop protection — RESOLVED (homemaker-py-yg5, 2026-06-14).** Lexicographic outer-search comparison (§4.9). Inner loop unchanged. 6. **Other continuous DOF are out of scope for Phase 1 — deliberately.** Floor-to-floor height is an Urb mutation (Mutate.pm:279-291, bounded 2.7–3.6 m) and feeds cost and stair fit; stair riser/width similar. Cut ratios dominate. Revisit (+1 DOF per storey) if Phase 2 plateaus. 7. **End-state confirmed: 100% Python**; Perl oracle is scaffold only. --- ## 9. How to reproduce (for the next session) ```bash cd /home/bruno/src/homemaker-layout # deps: pyyaml numpy scipy (shapely networkx for later phases) # geometry port vs Urb (must be identical): for d in /home/bruno/src/urb/examples/programme-house/*.dom; do diff <(perl -I/home/bruno/src/urb/lib experiments/dump_areas.pl "$d") \ <(python3 experiments/dump_areas.py "$d") || echo "MISMATCH $d" done python3 experiments/resolve_ratios.py # proxy solver (falsified) python3 experiments/sweep_failtypes.py # failure-type histogram python3 experiments/optimize_fullfitness.py 200 # full-fitness headroom (validated) ``` Oracle invocation (see `oracle.py`): `cwd` = the `.dom`'s directory (so `patterns.config` is found), `perl -I/lib /bin/urb-fitness.pl `, env `DEBUG=1` to defeat the skip-if-newer cache; reads `.score` and `.fails`. --- ## 10. Key gotchas discovered (carry forward) - **Wall inset:** the `.dom` plot is the *outer* boundary; Urb insets the root by `wall_outer` on load (`Urb::Dom::_deserialise`, Dom.pm:458) and offsets back out on save. `geometry.offset_quad` mirrors it; `dom.py` stashes raw corners in `node_file`. Skipping this makes all areas ~14% too large. - **Multi-storey `Below`-inheritance:** an upper quad's coordinates come from the matching quad below; a cut is "owned" by the *lowest* storey where its path is divided (`solver.free_branches` selects these). Walls stack for free. - **Geometry must be cached** — the pull-based recursion is exponential in depth otherwise (`geometry._cache`, cleared on `dom.load` and after each solver mutation). - **Equal-offset cuts** (`a == b`) ⇒ perpendicular walls, 1 DOF/cut. Independent offsets are wrong. - **`0.5^n` cliff** dominates fitness; it punishes new failures catastrophically (good for the inner loop, brutal for search gradient). - **Oracle ≈ 1 s/dom batched** (1.65 s single; assessment-dominated, startup ~0.65 s amortises across a batch). Submit many `.dom`s per call and prefer population optimisers; native fitness is a later speed/scale win, not a gate. --- ## 11. Phase 6 — topology-search quality for full / multi-storey programmes **Epic:** `homemaker-py-c4c`. **Status:** scoped 2026-06-17, pre-implementation. This section is the experiment ledger for the epic; each subsection is stubbed now and **filled in by the session that runs the experiment** (record the command, the numbers, and a one-line verdict, in the style of §4). ### 11.0 Diagnosis (why this phase exists) The delivered speedups landed in the two layers that were **never the bottleneck**. The native fitness (~140× over the oracle, §7 Phase 3) and the geometry inner loop (~1.6×, §4.5/§4.7) both operate *within a fixed topology*: the inner loop polishes geometry **inside a failure tier** and, by design, the `0.5^n` cliff stops it ever changing the failure count (§4.5: 0-fail-change across the headroom table). But final design quality is dominated by **failure count**, which is almost entirely a **topology** property. So faster fitness and better geometry do not move the number an architect would notice. Topology search on full programmes is the weakness: - **blank-slate programme-house** (`init.dom`): memetic stalls at **18 fails**; urb-evolve reaches **6** (§7 Phase 2 verdict). - **harbor-house** (16 rooms): `out1.dom` = **74 fails**, `generated.dom` = **130 fails**, both at ~machine-epsilon score; failures dominated by **`missing`-room stacking** (each missing room stacks critical + size + width + adjacency + level, §6). **Smoking gun:** `operators.mutate_divide` (operators.py:71) types each new leaf **at random** from `programme-codes + C + O`. Nothing makes the required programme spaces a constructive invariant, so on a large programme required rooms simply go missing → catastrophic `0.5^n` stacking, and the search is a random walk over type assignments with a flat-and-catastrophic gradient in the high-fail regime. **Causal frame for the fixes.** The base-floor tree is the *master* genome; upper storeys are divide/undivide deltas (`Below`-inheritance); the programme partitions rooms by required level (harbor: **10 on L0, 4 on L1, 2 free**). So construction and search should follow the genome's dependency order — credible base floor first, upper floors as deltas, with each floor's required-room set known from the programme. **Do not hard-freeze the base** when adding floors: that recreates the §4.2 partial-objective trap at the topology level (a base optimised purely as a ground floor can be a bad *substrate* — the vertical core must stay aligned and load-bearing walls must stack). ### 11.1 Premise experiment: single-storey harbor (`homemaker-py-c4c.1`) — DONE Built `examples/harbor-house-l0/` from harbor by retaining only the 10 space codes explicitly marked `level: 0` (cr1, ef1, da1, k1, ws1, m×3, la1, st1, me1, of×2 → 13 room instances), pruning adjacencies to the retained codes, and setting single-storey constraints (`storey_minimum: 1`, `storey_limit: 1`). The straddling anonymous spaces `n`/`t` (no explicit level key) were dropped so the set is an unambiguous single floor. Seeded from the bare plot (`init.dom`). - *Expectation / decision rule:* near-zero fails ⇒ bottleneck is multi-storey *coupling* (staging is the lever); still stalls (esp. `missing`) ⇒ per-floor *construction* itself is the bottleneck (§11.2 required first). - *Command (reproduce):* ```bash URB_NO_OCCLUSION=1 python3 experiments/run_search_scaled.py \ examples/harbor-house-l0 20000 0 \ examples/harbor-house-l0/init.dom examples/harbor-house-l0/generated.dom ``` - *Result:* 20000 native evals across 250 topologies (234 s, 85 evals/s). Best **33 fails**, fitness 2.25e-12 — deep in the 0.5ⁿ high-fail penalty regime, with the whole 16-member population stuck at 33–35 fails. The smaller budget-300 smoke run sat at 40 fails; full budget only crept 40 → 33. **Not near zero.** Fail histogram of the best `generated.dom`: | count | category | |------:|----------| | 13 | **missing** (all 3 `m` meeting rooms never constructed: required/critical + per-instance size/width/adjacency sub-checks) | | 6 | adjacency (ws1→c, k1→da1, da1→c, da1→k1, me1→c, la1→c) | | 4 | access | | 4 | size | | 2 | edge too long | | 2 | crinkliness | | 1 | proportion | | 1 | too few stairs — single-storey artifact (`staircase_min` floored to 1 by the fitness `or 1` default; constant across runs) | | **33** | total | - *Verdict: per-floor CONSTRUCTION is the bottleneck, not multi-storey coupling.* Even on a single floor with only 13 rooms and zero delta/core-alignment complexity, the search cannot assemble the required room set: the dominant category (13/33 = 39 %) is `missing` — the counted anonymous space `m×3` is entirely absent — and the remaining fails are downstream adjacency/access/size consequences of a room set the mutation operators never managed to construct. This matches the §11.0 prediction's "still stalls (esp. `missing`)" branch: **§11.2 programme-aware construction + missing-room repair is the prerequisite, and staging alone (§11.3) will not rescue it.** §11.3 stays blocked on §11.2. ### 11.2 Programme-aware construction + missing-room repair (`homemaker-py-c4c.2`) — DONE Two changes (`operators.py`, wired in `driver.py`): 1. **`constructive_topology`** — bootstrap seeder that makes the required room set a *constructive invariant*. It sizes each storey to its required rooms (partitioning by `level`; level-free rooms distributed round-robin over a shuffled order), plus one circulation `C` and one outside `O` per storey, grows the slicing tree to that leaf count, and assigns the types. Stochastic (random splits/rotations, shuffled type→leaf assignment) so a bootstrap batch is still a diverse population. Replaces the random `random_topology` bootstrap whenever the programme has required spaces. 2. **`mutate_place_missing`** — repair operator. Detects a required-but-absent space (`graph.check_space_counts`) and inserts one by dividing a host leaf into `[room | remainder]`. Lex-safe host ranking (cf. §4.10): generic `O` leaves first (unbounded, nothing displaced), then other non-required leaves, circulation/stairs only as last resort; a required room is never displaced. Forced onto the room's required storey when the programme constrains its level. Weight 2.0 in the mutation mix (noops cheaply once complete). - *Gate:* `missing`-type failures collapse to ~0; net-fail improvement vs the blank-slate baseline; no regression on the seeded programme-house 1-fail optimum (§4.10). - *Commands (reproduce):* ```bash # A/B at identical budget+seed (old = git HEAD before this change): URB_NO_OCCLUSION=1 python3 experiments/run_search_scaled.py \ examples/harbor-house 20000 0 examples/harbor-house/init.dom out.dom # §4.10 regression: warmstart-2f4 seed, 50000 evals, pop 8, 4 workers ``` - *Result (harbor-house, 20000 native evals, seed 0, identical config):* | metric | OLD (random bootstrap) | NEW (constructive) | |--------|-----------------------:|-------------------:| | seed best fails | 163 | 139 | | final total fails | 133 | **105** | | `missing` fails | **103** (77 %) | **12** (11 %) | | missing-records | 22 | 2 | | dominant remaining | `missing` | crinkliness 27, size 23, access 13, edge 12 | Constructive seeding alone gives a **24-fail head start at the seed** (163 → 139) and the run ends at **105 vs 133 (−21 %)**, with the `missing` stack collapsed **103 → 12**. **§4.10 regression: PASS** — the warmstart-2f4 seed still reaches a **1-fail** population (whole pop 1f at 50 040 evals; `place_missing` noops harmlessly when the set is complete). - *Verdict: construction works and is necessary, but reframes the bottleneck.* Making the required set a constructive invariant removes the catastrophic `missing`-room stacking that dominated the blank-slate baseline (77 % → 11 % of fails). But a *complete* 36-room harbor design then carries a large **quality-fail load** — crinkliness/size/access/edge-too-long packing of two fully-populated floors — that the current geometry inner loop + topology operators reduce only partway in 20k evals. So total fails improve but stay high. The dominant categories are now exactly what **§11.4 (graded objective, to navigate the dense quality-fail regime)** and **§11.3 (staging — build one credible floor at a time instead of cramming both)** target; §11.3 is unblocked by this result. A concrete next seeder refinement (filed): the type→leaf assignment is currently random, ignoring adjacency — clustering each room near its required `c`/neighbour at construction time should cut the adjacency (8) and downstream access (13) fails directly. *Note on the baseline:* DESIGN cited a "74-fail `out1.dom`", but the on-disk `out1.dom` is untracked and was overwritten by a prior experiment (it now re-scores to 37 fails; the committed `out1.dom.fails` of 74 lines belongs to the superseded `.dom`). The honest, reproducible comparison is therefore the identical-config A/B against the pre-change code (133 fails), not the stale `out1.dom` number. ### 11.3 Staged per-floor search (`homemaker-py-c4c.3`) — DONE Searches the genome in causal dependency order (`driver.search_staged`), two stages composed from the existing `driver.search`: 1. **Stage 1 — base floor** (40 % of budget). A single-storey programme is auto-derived to a tempdir (`programme.write_stage1_programme`): the full `patterns.config` filtered to the storey-0 room set (`programme.partition_rooms_by_storey`), `level:` keys dropped, adjacencies pruned to surviving refs, `storey_limit/staircase` forced to 1. The base is searched on that reduced programme but **ranked** with a substrate-readiness bonus — key `(-n_fails, fitness·(1 + W·readiness))`, `W=1` — so it is selected as a good *substrate*, not merely a good ground floor (anti-§4.2). `graph.substrate_readiness` = `core_factor · capacity`: full credit for a reserved `C` leaf ≥ `STAIR_MIN_AREA` (vertically-alignable core), times `min(1, usable_base_area / required_upper_area)` (enough divisible footprint for the upper set). 2. **Stage 2 — upper floors as deltas** (remaining budget). The best base is lifted (`operators.lift_base_to_storeys`) into a full multi-storey design that **preserves the base storey and its inherited core** and instantiates each upper storey's required room set by construction (the Stage-2 analog of §11.2 seeding). Deltas are searched with the base kept **mutable at low probability** (`base_p=0.15`, threaded through the exploratory ops; `place_missing`/`core_*` stay unbiased — repair and core-maintenance must reach the base). - *Gate:* staged beats single-stage on harbor at equal budget; reserved-core + readiness prevent the bungalow trap (stage 2 does not carve a core from scratch); no programme-house regression. - *Commands (reproduce, `URB_NO_OCCLUSION=1`, 20000 evals, seed 0):* ```bash python3 experiments/run_search_scaled.py examples/harbor-house 20000 0 \ examples/harbor-house/init.dom scratch/ab_single.dom # single-stage python3 experiments/run_staged_search.py examples/harbor-house 20000 0 \ examples/harbor-house/init.dom scratch/ab_staged.dom # staged ``` - *Result (harbor-house, 20000 native evals, seed 0, identical config):* | metric | single-stage | **staged** | |--------|-------------:|-----------:| | total fails | 105 | **95** | | crinkliness | 27 | 18 | | edge too long | 12 | 8 | | proportion | 6 | 4 | | width | 4 | 2 | | size | 25 | 26 | | access | 13 | 18 | | missing | 8 | 8 | | adjacency | 2 | 2 | Single-stage reproduces the §11.2 baseline **exactly (105 fails)**; staged ends at **95 (−10, −9.5 %)**. The gain is concentrated in the packing fails staging targets — crinkliness 27→18 and edge-too-long 12→8 — at a small cost in access (+5). **Anti-bungalow: confirmed.** Every `core_divide`/`core_undivide` in the Stage-2 winning lineage is a *noop* — the core is inherited from Stage 1 and is never carved from scratch. **Programme-house regression: PASS** — single-storey programmes fall through to plain `search`; the warmstart-2f4 seed (50000 evals, pop 8, 4 workers) still reaches a whole-population **1-fail** optimum (§4.10). - *Verdict: staging helps, modestly, and is the right structural frame.* Building one credible, substrate-ready floor first — then upper floors as constructed deltas with an inherited core — beats cramming both floors simultaneously (95 vs 105) without touching the inner loop. The remaining load is the dense quality-fail regime (size/access/crinkliness on two fully-populated floors) that **§11.4 (graded objective)** targets: with `missing` already collapsed (§11.2) and the floors now assembled in dependency order, the lever left is navigation *within* the high-fail plateau, where lex-by-count gives near-zero gradient. ### 11.4 Graded high-fail objective (`homemaker-py-c4c.4`) — DONE (negative) Premise (from Phase 4, §4.9): lexicographic-by-total-count `(-n_fails, fitness)` gives ~zero selection signal in the high-fail regime because the `0.5^n` cliff flattens fitness to ~machine-epsilon, so neighbours at ~49–105 fails look indistinguishable. Proposed fix: a continuous proximity key *beneath* fail-count and *above* fitness — `(-n_fails, grade, fitness)`. **Implementation (kept, default-off).** `fitness._leaf_grade` reads each *failing* per-leaf quality factor (perpendicular/proportion/size/width/crinkliness/access) as proximity-to-satisfaction `f / FAIL_THRESHOLD ∈ [0,1)` and sums it; `Fitness.score_with_grade` returns it alongside score/fails. The scalar fitness and the fail count are **untouched**, so the inner-loop `0.5^n` cliff (§5.4) is unaffected — **inner-loop 0/9-regression check: PASS** (re-ran §4.9 part 1, `run_inner_loop_protection`, 0/9 regressions). The grade is read once per child off the already-optimised tree in `driver._evaluate` (one extra native eval, ~1/child_budget) and used **only** in the outer comparator key, behind `search(..., use_grade=True)` / `search_staged(..., use_grade=True)` (default `False`; threaded to Stage 2 only — Stage 1 keeps its readiness key, §11.3). Structural fails (missing/adjacency/edge-too-long/level/…) score 0 grade, so the missing-space hierarchy (§6) is preserved: grade can never reward dropping a room. - *Commands (reproduce, `URB_NO_OCCLUSION=1`, 20000 evals):* ```bash USE_GRADE=0 python3 experiments/run_staged_search.py examples/harbor-house 20000 \ examples/harbor-house/init.dom scratch/st_lex.dom # lex baseline USE_GRADE=1 python3 experiments/run_staged_search.py examples/harbor-house 20000 \ examples/harbor-house/init.dom scratch/st_grade.dom # lex + grade ``` - *Result (harbor-house, staged, 20000 native evals, total fails at budget):* | seed | staged `lex` | staged `lex+grade` | |-----:|-------------:|-------------------:| | 0 | **95** | 99 | | 1 | **96** | 98 | | 2 | 106 | **102** | | mean | **99.0** | 99.7 | Grade wins 1/3 seeds, loses 2/3, and is **slightly worse on the mean** — within seed-noise, **no escape** from the plateau. Single-stage seed 0 is a dead heat (105 = 105). Stage-1 is identical by construction (grade off there); the divergence is entirely in Stage 2, where the grade run **stalls early** (seed 0: last improvement at 13600/20000 evals, stuck at 99) while lex keeps reducing the count (99→95). - *Why it fails — the premise is falsified by measurement.* The cliff is constant *within* a fail-tier (`0.5^n`, `n` fixed), so within a tier reported fitness is `value/cost × const` and still spans **~6 orders of magnitude** (seed-0 Stage-2 history: 1.2e-37 → 4.6e-31 *all inside the same descending fail count*). The outer comparator only ever compares within a tier (−`n_fails` dominates across tiers), so lex's secondary `fitness` key already carries a strong, well-graded signal — exactly the gradient §11.4 assumed was missing. Inserting `grade` *above* `fitness` **displaces** that working signal: the population fills with high-grade (shallow-fail) incumbents and the fail-reducing restructurings — which transiently deepen other fails and so look worse on grade — are no longer selected. Placing `grade` *below* `fitness` instead would be near-inert (fitness ties are measure-zero in a continuous objective). Either way there is no lever: the high-fail plateau is a *topology* basin, not a comparator-resolution problem. - *Verdict: reject the graded objective; lexicographic `(-n_fails, fitness)` stands.* The §11.3 staged **95-fail** result remains the harbor best. The remaining load is genuinely structural (escaping topology basins), which is what **§11.5 (structural niching + restarts)** and the `9gp` canonical-encoding capstone target — not outer-comparator reshaping. The `use_grade` flag and `score_with_grade` are kept default-off for reproducibility and possible reuse (e.g. as a *diversity* signal under §11.5 rather than a selection key). ### 11.5 Topology diversity: structural niching + restarts (`homemaker-py-c4c.5`) — DONE (negative) Premise (epic diagnosis): the population dedups on the **fitness scalar** (`driver.admit`, `abs(fitness)` within `1e-9`) and so has no structural diversity preservation — proposed as the root cause of the blank-slate gap (§7 Phase 2: memetic 18 fails vs urb-evolve 6), a single mutation chain losing to urb-evolve's upfront random-population diversity. **Implementation (kept, default-off).** A cheap structural topology signature (`genome.signature`) string-encodes each storey's tree shape + cut orientations + leaf types, routed through `encode` so dead inherited fields canonicalise; it is **ratio-invariant** (same topology, different geometry → same signature). Two diversity mechanisms, both behind flags on `search`/`search_staged`: `niche_by_signature` holds at most one individual per signature in the population (structural niching, keeping the better of a collision) in place of the fitness-scalar guard; `restart_patience=` does a soft restart on stagnation (keep `restart_elite` incumbents, refill with fresh constructive/random seeds — urb-evolve's upfront diversity as a soft restart). `SearchResult` gained `n_distinct_signatures` / `diversity_history` / `n_restarts` to quantify diversity over time. - *Commands (reproduce, `URB_NO_OCCLUSION=1`, 20000 evals):* ```bash NICHE=0 python3 experiments/run_search_scaled.py examples/programme-house 20000 \ examples/programme-house/init.dom scratch/ph_before.dom # legacy dedup (before) NICHE=1 python3 experiments/run_search_scaled.py examples/programme-house 20000 \ examples/programme-house/init.dom scratch/ph_niche.dom # structural niching NICHE=1 RESTART_PATIENCE=2000 python3 experiments/run_search_scaled.py \ examples/programme-house 20000 examples/programme-house/init.dom scratch/ph_restart.dom # harbor (staged): swap run_staged_search.py, seed examples/harbor-house/init.dom ``` - *Diversity (the secondary criterion) — MET.* Niching takes the final population from ~**4–6 / 16** distinct topologies (legacy dedup) to **16 / 16**; restarts raise distinct topologies *seen* by ~30 % (≈105–138 → ≈164–186 on programme-house). The signature machinery works exactly as designed. - *Fail count (the gate) — NOT MET.* Blank-slate programme-house, total fails at budget (lower is better): | seed | before (legacy) | niche | niche + restart | |-----:|----------------:|------:|----------------:| | 0 | **11** | 14 | 12 | | 1 | **11** | 11 | 14 | | 2 | 15 | **13**| 13 | | mean | **12.3** | 12.7 | 13.0 | Harbor-house (staged, seed 0): legacy **95** (reproduces §11.3 exactly), niche **94**, niche+restart **108**. Across both programmes niching is a **tie within seed noise** and restarts are **strictly worse**; nothing approaches the ≤ 6 gate. - *Why it fails — the premise is falsified by measurement.* More *structural* population diversity does not buy lower fails: the legacy dedup already holds 14/16 distinct topologies on harbor (Stage-2 starts from lifted bootstraps), so it was never the diversity bottleneck the epic assumed. Maximal diversity (16/16) with the fixed tournament pressure just **diffuses** effort — the fitness-scalar dedup's smaller effective population exploits a basin slightly harder. Restarts throw away converging Stage-2 work and regress hardest. The high-fail plateau is a **reachability** problem (operators + encoding cannot reach the low-fail basins), not a population-management one — the same conclusion §11.4 reached from the comparator side. - *Verdict: reject niching/restarts as defaults; the legacy fitness-scalar dedup stands.* `niche_by_signature` / `restart_patience` are kept default-off for reproducibility and reuse, and `genome.signature` is the cheap stand-in that the canonical Polish encoding (**`homemaker-py-9gp`**) supersedes. With §11.3–§11.5 all landed, the residual load is genuinely structural: the principled lever is the canonical encoding (associativity collapse `(a|b)|c == a|(b|c)`) plus richer topology operators, not outer-loop selection/population reshaping. ### 11.6 Adjacency-aware constructive seeding (`homemaker-py-s44`) — DONE (positive) Premise (follow-up to §11.2): `constructive_topology` instantiated every required room but **typed the leaves at random**, so rooms landed stranded from circulation. On harbor the seed carried ~29 adjacency-to-`c` + ~27 per-leaf `access` + level-`inaccessible` fails (≈ 56 of the seeder-controllable load; the remaining size/width/proportion/crinkliness fails are geometry, the inner loop's job). The programme confirms the shape: of 16 harbor spaces all 16 require adjacency to `c`, so the dominant lever is *connect every room to circulation*. **Implementation (`operators._assign_adjacency_aware`, default-on).** A single circulation leaf cannot border a dozen rooms, and a slicing tree guarantees adjacency only between *siblings* — so adjacency must be read from the geometric leaf graph, not the tree. The seeder now spends ~one extra leaf per three rooms on circulation, builds the type-independent `geometry.leaf_graph`, and picks a **greedy connected dominating set** of circulation leaves (start at the highest-degree leaf, extend along the frontier by most-newly-dominated): every room leaf ends up bordering a *connected* circulation spine, so adjacency-to-`c` and access are satisfied by construction at the seed geometry. Rooms are placed on dominated leaves (constraint-hardest first), outside `O` on the most peripheral leaf; room order and tie-breaks stay stochastic so a bootstrap batch is diverse. Threaded through `driver.search(seed_adjacency_aware=True)`; `adjacency_aware` flag on `constructive_topology` (env `ADJ` in `run_search_scaled.py`) for the A/B. - *Commands (reproduce, `URB_NO_OCCLUSION=1`, 20000 evals, single-stage):* ```bash ADJ=0 python3 experiments/run_search_scaled.py examples/harbor-house 20000 \ examples/harbor-house/init.dom scratch/hh_adj0.dom # random assignment (before) ADJ=1 python3 experiments/run_search_scaled.py examples/harbor-house 20000 \ examples/harbor-house/init.dom scratch/hh_adj1.dom # adjacency-aware (after) ``` - *Seed quality (harbor, 10 seeds, raw seed before optimisation):* adjacency-to-`c` **29.2 → 12.2**, per-leaf access **26.6 → 8.3**, level-inaccessible 0.4 → 0.2 (≈ 56 → 21 seeder-controllable fails). Geometry fails rise at the raw 0.5-split seed (more, smaller leaves) but are recovered by the inner loop. - *End-to-end (total fails at budget, single-stage, lower is better):* | seed | harbor before | harbor after | prog-house before | prog-house after | |-----:|--------------:|-------------:|------------------:|-----------------:| | 0 | 105 | 100 | 11 | 10 | | 1 | 115 | **85** | 11 | **8** | | 2 | 110 | 87 | 15 | 10 | | mean | **110.0** | **90.7** | **12.3** | **9.3** | Harbor **−19.3 fails (−17.5 %)**, programme-house **−3.0 (−24 %)**. `ADJ=0` seed 0 reproduces the §11.2 single-stage **105** baseline exactly (clean control). Notably the adjacency-aware **single-stage** harbor (mean 90.7, best 85) now **beats the §11.3 staged best of 95** — the first Phase-6 fail-count reduction from *seeding* rather than search machinery. - *Verdict: keep adjacency-aware seeding as the default.* It is the first lever in Phase 6 to move the fail count on both programmes. The win is the dominant adjacency-to-`c` / access load; secondary adjacencies and the staged `lift_base_to_storeys` upper floors are picked up in §11.7 (`homemaker-py-ld5`). ### 11.7 Adjacency-aware lift + secondary adjacencies (`homemaker-py-ld5`) — DONE (positive) Two gaps left by §11.6: (a) `lift_base_to_storeys` — the staged Stage-2 seeder — still typed upper-floor leaves at random, so staged search did not get the adjacency win; (b) secondary adjacencies (`k1↔da1`, `da1↔o`, ~4 harbor rooms) were ignored. **Implementation.** `_assign_adjacency_aware` gained a `fixed_circ` parameter: the dominating-set search is *seeded from* given circulation leaves, so on an upper floor the spine grows off the **inherited vertical core** rather than from scratch (preserving the §11.3 anti-bungalow core-alignment invariant). Room placement is now constraint-ordered: codes with the most non-`c` adjacency requirements are placed first, each onto the open slot that satisfies the most of its requirements against already-typed neighbours (circulation + rooms placed so far), clustering `k1↔da1`, `da1↔o`, etc. `lift_base_to_storeys(reqs=…, adjacency_aware=True)` grows a per-floor circulation budget and calls it with the core as `fixed_circ`; threaded through `search_staged(seed_adjacency_aware=True)` (`ADJ` env in `run_staged_search.py`). - *Seed quality (harbor lift, 8 seeds, raw seed):* adjacency-to-`c` **16.1 → 7.6**, access **16.2 → 7.2** on the lifted upper floor. - *End-to-end (harbor, staged, 20000 evals, total fails at budget):* | seed | staged before (`ADJ=0`) | staged after (`ADJ=1`) | |-----:|------------------------:|-----------------------:| | 0 | 95 | 97 | | 1 | 96 | **78** | | 2 | 106 | 81 | | mean | **99.0** | **85.3** | `ADJ=0` reproduces the §11.4 staged lex baseline **exactly** (95/96/106, mean 99.0 — clean control). Staged adjacency-aware is **−13.7 fails (−14 %)** and is now the **best harbor configuration overall**: staged baseline 99.0 → single- stage adjacency-aware (§11.6) 90.7 → **staged + adjacency-aware lift 85.3** (best **78**, seed 1). Staging and adjacency-aware seeding compose: the credible Stage-1 base and the core-seeded upper spine each contribute. - *Verdict: keep adjacency-aware lift + secondary clustering as defaults.* Harbor is now ~85 fails, down from the 95/105 plateaus that opened Phase 6. The residual is geometry- and shape-bound (size/proportion/crinkliness on the denser, more-circulation layouts), which is the canonical-encoding / shape-feasibility territory of `homemaker-py-9gp`. ### 11.8 Topology diversity × selection pressure, co-tuned (`homemaker-py-6zy`) — DONE (negative) Premise (loose end from §11.5): structural niching was A/B'd against the legacy fitness-scalar dedup with selection pressure **held fixed** at a binary tournament (`driver._tournament`, `k=2`). §11.5's own mechanism note named the coupling as the reason for its null — "Maximal diversity (16/16) with the fixed tournament pressure just **diffuses** effort" — i.e. diversity and pressure are coupled but were varied as if independent: niching widens the population, but `k` was never **sharpened** to convert the extra exploration back into exploitation. This issue isolates that coupling — sweep tournament size jointly with niching to test whether sharper selection turns the 16/16 structural diversity into lower fails. The project had already pivoted to the canonical encoding (`homemaker-py-9gp`); this is a falsification check so the lever is not silently lost, not an expected win (§11.4/§11.5 both located the plateau in **reachability**). **Implementation (knob only; default-off behaviour unchanged).** Exposed `tournament_k: int = 2` on `search` / `search_staged`, threaded into both `_tournament` call sites (crossover pair + mutation parent) and all three internal `search()` calls of the staged path; reuses the §11.5 `genome.signature` / `niche_by_signature` machinery unchanged. The experiments harness reads `HOMEMAKER_TOURNAMENT_K` (mirrors `NICHE`) in `run_search_scaled.py` / `run_staged_search.py`; `experiments/run_6zy_ab.sh` runs the joint grid (RESUME-able). - *Commands (reproduce, `URB_NO_OCCLUSION=1`, 20000 evals; blank-slate seed `init.dom` to match §11.5):* ```bash # grid: NICHE ∈ {0,1} × HOMEMAKER_TOURNAMENT_K ∈ {2,3,4} NICHE=0 HOMEMAKER_TOURNAMENT_K=2 python3 experiments/run_search_scaled.py \ examples/programme-house 20000 examples/programme-house/init.dom scratch/out.dom # harbor (staged): run_staged_search.py, seed examples/harbor-house/init.dom bash experiments/run_6zy_ab.sh # full grid → scratch/6zy/summary.tsv ``` - *Diversity (mechanism check) — confirmed biting.* `niche=on` holds the final population at **16/16** distinct topologies at every `k`; `niche=off` sits at **4–11/16**. The pressure knob is genuinely varied (`k`=2,3,4). So both levers are live — the null below is not a machinery artefact. - *Fail count (the gate) — no cell beats the baseline.* Blank-slate programme-house, total fails at budget over **5 seeds** (0–4), mean (sd): | niche \ k | k=2 | k=3 | k=4 | |:---------:|:-----------:|:-----------:|:-----------:| | **off** | **4.80** (1.60) | 6.40 (2.50) | 6.00 (2.00) | | **on** | 6.20 (1.72) | 7.00 (1.41) | 6.60 (1.85) | The legacy `(off, k=2)` cell is the **best** of the six (4.80); every higher-pressure row and every `niche=on` row is equal-or-worse (6.0–7.0). All differences sit within ~1 sd at 5 seeds, so the grid is a wash — but the central tendency is unambiguous: sharpening `k` and adding niching both *slightly hurt*, the opposite of the rescue the premise hypothesised. Harbor-house (staged, seed 0) reinforces it: `niche=on` is uniformly worse than `off` at every `k` (k2 72→83, k3 77→82, k4 67→75); within the `niche=on` row higher `k` helps monotonically (83→82→75) but never catches the `niche=off` row, and the best cell overall (`off, k=4` = 67) is a single-seed wiggle within noise of the `off, k=2` = 72 baseline. - *Why it fails — the coupling is real but points the wrong way.* Sharper selection does **not** convert the extra structural diversity into lower fails; if anything the 16/16 niched population at high `k` over-commits the larger spread to a handful of basins and loses the occasional lucky low-fail draw the smaller fitness-scalar population stumbles into. §11.5's "diffuses effort" diagnosis survives co-tuning: the bottleneck is **reachability** (operators + encoding cannot reach the low-fail basins), so reshaping selection/population pressure cannot recover what the search space does not expose — the same conclusion §11.4 reached from the comparator side and §11.5 from the diversity side. - *Verdict: §11.5 null is robust to selection pressure — reject `k>2` and niching as defaults; binary tournament + fitness-scalar dedup stand.* `tournament_k` is kept (default-2) as a reusable knob alongside `niche_by_signature`. With §11.4/§11.5/§11.8 all negative on the outer loop, the residual is confirmed structural: the principled lever is the canonical encoding + richer topology operators (`homemaker-py-9gp`), not selection or population management. ## 12. Phase 7 — scaling validation & residual reduction (post-c4c) **Epic:** `homemaker-py-leu`. **Status:** opened 2026-06-19. Continuation of the closed Phase 6 (§11). Phase 6 evidence located the leverage in *construction / seed quality* (§11.6/§11.7 wins) rather than search machinery (§11.4/§11.5 both regressed); the harbor residual is now geometry/shape-bound at ~85 fails. This section is the experiment ledger for Phase 7, same discipline as §11: each subsection records the command, the numbers, and a one-line verdict. ### 12.1 Larger-than-house benchmark: `maple-court` (`homemaker-py-leu.1`) — DONE **Why.** Harbor (16 programme entries, 2 storeys) was the biggest real programme in `examples/`. `homemaker-py-9gp`'s headline claim is scaling **>16 rooms** and its acceptance criterion demands "a larger-than-house programme" to measure on — so a bigger benchmark is a prerequisite, not optional. Proportion-aware seeding (`leu.2`) and re-scoped 9gp are both measured against this baseline. **The benchmark.** `examples/maple-court/` — a three-storey assisted-living / co-housing facility: **26 distinct programme entries / 52 room instances** across **3 required storeys** (`storey_minimum: 3`), ~1015 m² target internal area on a ~790 m²/floor plot. It mirrors harbor's structure deliberately — a dominant adjacency-to-`c` load on nearly every room plus a handful of secondary adjacencies (`da1↔k1`, `da1↔o`, `lr1/ws1/lo1/gh1/gy1 ↔ o`), anonymous interchangeable room families (`m`×3, `t`×6, `n`×4, `r`×12, `em`×2, `py`×2, `tt`×4), and `staircase_min/max: 2`. Code letters avoid the generic `c`/`o`/`s` leading-letter trap (those are reserved in `fitness.py`/`graph.py` for circulation/outside/sahn): no *room* code starts with c/o/s, so harbor's quirk of typing Common Room / Storage / Office as quasi-generic (`cr1`/`st1`/`of`) is not reproduced. `init.dom` is a single `O` footprint; storeys are built by the search from `storey_minimum`, exactly as harbor. **Baseline (current default search: adjacency-aware seeding + staged, §11.7).** Reproduce (`URB_NO_OCCLUSION=1`, 20000 evals, staged, `ADJ=1` default): ```bash URB_NO_OCCLUSION=1 python3 experiments/run_staged_search.py \ examples/maple-court 20000 examples/maple-court/init.dom scratch/mc_s.dom ``` | seed | total fails | best lineage | |-----:|------------:|---------------------| | 0 | **145** | rotate 0/rrlr | | 1 | 158 | core_undivide noop | | 2 | 152 | swap 0/rrlllr | | mean | **151.7** | | Each run executed exactly 20000 native evals across 250 topologies (~36 min, ~9.1 evals/s) and re-scored native-consistent (`→ OK`). The best layout (seed 0, 145 fails) was saved as `examples/maple-court/generated.dom` with its `.fails` (superseded in §12.2 by the proportion-aware 126-fail layout). The single-stage harness (`run_search_scaled.py`) also accepts the programme unchanged. The score prints near-zero (`0.5^145` fail cliff) — the **fail count** is the yardstick. - *Verdict: benchmark established at mean 151.7 fails (best 145).* As expected for a programme ~3× harbor's room count, the absolute fail floor is well above harbor's ~85; this is the scaling yardstick `leu.2` (proportion-aware seeding) and the re-scoped `9gp` are measured against. The residual character is the same geometry/shape family flagged at the close of §11.7. ### 12.2 Proportion-aware constructive seeding (`homemaker-py-leu.2`) — DONE (positive) Premise (follow-up to §11.6/§11.7). The constructive seeders grow geometry with uniform `[0.5, 0.5]` cuts *before* types are assigned, so the raw seed is "more, smaller leaves" of equal area: a room with a large programme target comes out too small, a small room too big, and the inner loop must recover all of size/width/proportion from scratch. With the adjacency load now cut by seeding (§11.6/§11.7), this geometry residual is the dominant remaining term. Attacking it at the seed — in the proven *construction* direction — is far cheaper than the `9gp` encoding rewrite. **Implementation (`operators._size_divisions_from_targets`, flag `seed_proportion_aware`, env `PROP`, default-on per the A/B below).** After the adjacency-aware type assignment (§11.6/§11.7, left exactly as is), each leaf carries a target area — a sized room's programme `size`; circulation/outside absorb the plot slack (floored at `0.4 ×` mean room area so a circulation leaf never shrinks below door-width and undoes the §11.6 adjacency win). Because `division=[f, f]` cuts off left area-fraction `f` (rotation-independent — verified), bottom-up subtree-target sums compose multiplicatively to give every leaf area ∝ its target. **Area alone regressed the raw seed**, though: choosing only the cut *fraction* to hit a target *area* slices thin slivers with terrible aspect (proportion/width/edge-too-long fails swamp the size gain — measured below). So each cut also picks the **rotation** (the two distinct cut directions) that makes its two children squarest; rotation depends on realised parent geometry, so the pass runs top-down. Both ratio and rotation derive from the target dims; neither touches topology or type assignment. Threaded through `driver.search`/`search_staged(seed_proportion_aware=…)`. - *Raw-seed fails (10 seeds, single-stage constructive, before optimisation), area-only vs area+rotation:* | family | harbor before | area-only | area+rot | |-------------|--------------:|----------:|---------:| | geometry | 123.0 | 135.9 | **99.9** | | access/adj | 19.1 | 23.8 | 20.4 | | total | 144.1 | 162.1 | **123.7** | Area-only makes geometry *worse* (slivers); area+rotation drops the geometry family on every programme — harbor **123.0 → 99.9 (−19 %)**, programme-house **13.1 → 8.7 (−34 %)**, maple-court **200.5 → 164.1 (−18 %)**. Access/adjacency regresses slightly (rotation shifts the leaf graph the adjacency assignment was computed against): harbor +1.3, prog-house +2.4, maple +3.4 — far smaller than the geometry gain. The size family in particular falls as intended (harbor size 31.4 → 22.0), and proportion flips from a regression to a win (21.3 → 12.8) once rotation is co-chosen. - *End-to-end (total fails at budget, 20000 evals, 3 seeds, PROP=0 vs PROP=1; harbor & maple-court staged):* | seed | harbor PROP=0 | harbor PROP=1 | maple PROP=0 | maple PROP=1 | |-----:|--------------:|--------------:|-------------:|-------------:| | 0 | 97 | 72 | 145 | 126 | | 1 | 78 | 81 | 158 | 148 | | 2 | 81 | 69 | 152 | 134 | | mean | **85.3** | **74.0** | **151.7** | **136.0** | Harbor **−13 % (best 69, was 78)**, maple-court **−10 % (best 126, was 145)**. PROP=0 reproduces the §11.7 staged harbor (85.3) and §12.1 maple baseline (151.7) *exactly* — clean controls. Proportion-aware seeding is the first Phase-7 lever to move the fail count on the larger-than-house benchmark. - *A storey-count bug surfaced (`homemaker-py-cq1`).* programme-house has `storey_minimum: 2` but all rooms `level: 0`, and `n_storeys_required` only read `level:` keys — so the constructive seeder built a **1-storey** seed for a 2-storey programme and `search_staged` fell through to plain search. Fixed (`programme.storey_minimum`/`n_storeys_for`; `driver.search` passes `min_storeys` to the seeder; `search_staged` routes on `max(level-derived, storey_minimum)`). No-op for harbor/maple (level-derived already ≥ storey_minimum); independent win on programme-house (single-stage baseline **8.0 → 5.0** with a correct 2-storey seed). - *programme-house regresses, but it is a convergence-speed artifact, not a worse optimum.* On the 6-room programme proportion-aware seeding loses at 20000 evals on every path tested (single-stage 1-storey 8.0→11.7, single-stage 2-storey 5.0→8.3, staged 2-storey 4.3→6.0). The mechanism is a *deeper local optimum*: the equal-area PROP=0 seed has badly-proportioned leaves, so `undivide` moves — the route to programme-house's simpler optimum — are accepted as improvements; the well-fitted PROP=1 seed makes `undivide` an immediate fitness drop (merging two good leaves yields one bad one), walling off the restructuring path. A budget sweep (staged, storey-fixed) shows this is *reachability speed*, not an asymptotic trap: | budget | PROP=0 (s0/s1) | PROP=1 (s0/s1) | |-------:|---------------:|---------------:| | 20000 | 4 / 5 | 8 / 6 | | 60000 | 2 / 2 | 4 / 3 | | 150000 | 2 / 0 | **1** / 10 | PROP=1 reaches **1 fail** (seed 0, 150k — beating PROP=0's 2; best-known is 2), so it is not trapped; the gap narrows with budget and crosses over. (Staged splits budget by *fraction*, so runs at different budgets evolve different Stage-1 bases and are not nested — hence the high variance, e.g. PROP=1 seed 1 swinging 3→10.) The same "deeper basin" that *helps* where the constructed topology is roughly right (large programmes, scarce budget) *delays* convergence where the seed must be restructured (small programmes). - *Verdict: keep proportion-aware split sizing, default-on (`seed_proportion_aware` default `True`, env `PROP=1`).* It is a measured win on both larger programmes — harbor −13 %, the maple-court scaling benchmark −10 % — exactly the regime Phase 7 targets and the basis the re-scoped `9gp` is measured on. The only regression is a small-programme convergence-speed effect that washes out with budget (PROP=1 reaches the known floor), with no evidence of an asymptotic penalty, so default-on is not paid for by a worse optimum anywhere. The win is rotation-and-ratio sizing from target dims; the bare ratio is not enough (area-only regressed). Area sizing assumes total target ≈ plot area; choosing the cut *direction* for aspect is what makes it pay. ### 12.3 Re-scoped 9gp: shape feasibility + reachability moves (`homemaker-py-9gp`) Re-scoped capstone of the epic (2026-06-19): the original canonical-Polish- expression rewrite was justified partly by a niching *signature*, but §11.5 falsified niching and `genome.signature` already supplies the cheap stand-in. The two surviving, evidence-supported parts are landed here as operators on the existing decoded `Node` tree — **no** Polish-expression rewrite — each measured independently against the §12.2 leu.2 baseline (maple-court staged 136.0, harbor 74.0). A true canonical encoding is revisited only if the M3 measurement proves associativity valuable at scale. **9gp.1 — shape-feasibility pre-filter (scaling lever).** `operators. predicted_shape_fails(root, reqs, fit)` lays a topology out at its proportion- aware target geometry (reusing `_size_divisions_from_targets`, §12.2 — the squarest layout the inner loop warm-starts from) and counts the size/width/proportion/crinkliness fails the native fitness reports: a cheap lower-bound proxy for the best shape the topology can reach. `driver._evaluate` calls it *before* the inner loop and **prunes** (1 feasibility eval instead of ~80 inner-loop evals) when the predicted shape fails both exceed a tunable threshold *and* are ≥ the incumbent's total fails — the second guard makes the proxy safe (a topology whose shape floor is still below the incumbent is never discarded). Pruned individuals are tagged `pruned/…`, counted as explored topologies but never bred from or ranked, so budget flows to feasible topologies. Seed/bootstrap/restart batches are never filtered (construction invariants must survive). Threaded as `search(…, feasibility_filter, feasibility_max_shape_fails)` through `search_staged`; **default OFF** so the §12.2 controls reproduce exactly (`test_feasibility_filter_off_matches_baseline`). Env: `FEAS=1 MAXSHAPE=`. **9gp.2 — M3 Wong-Liu re-association move (reachability lever).** `operators. mutate_reassociate` adds the associativity move `(a|b)|c ↔ a|(b|c)` on two **same-orientation** live cuts (both directions, for reversibility): a pure- topology move that preserves the leaf set and types but reaches tree shapes the existing set cannot. M1 (operand swap) is `mutate_swap` and M2 (single-cut orientation complement) is `mutate_rotate`; associativity was the missing canonical-slicing move attacking the reachability bottleneck §11.4/§11.5 both fingered. Only live cuts (`below is None`, as `mutate_rotate`) are restructured, so dead inherited fields are untouched and `encode` re-anchors deltas; the two restructured cuts default to `0.5` and the inner loop recovers their ratios. Registered in `MUTATIONS`; **default OFF** via `enable_reassociate` (forces its mutation weight to 0 so the baseline is byte-identical). Env: `REASSOC=1`. - *Implementation status (this session):* both land with unit tests (`tests/test_operators.py`: reassociate preserves the leaf multiset, changes the signature, noops on perpendicular cuts, stays canonical on the harbor corpus; `predicted_shape_fails` is non-negative, pure, deterministic. `tests/test_driver.py`: filter-off reproduces the baseline trajectory; filter-on prunes at 1 eval/topology and never admits a pruned individual). Full suite green (211 passed). A short smoke run on maple-court confirms both paths execute under the real native fitness. - *Calibration (predicted shape-fail floor of the constructive seeds).* Over 8 proportion-aware constructive seeds, `predicted_shape_fails` is maple **121–163 (mean 135.6)** and harbor **72–90 (mean 84.6)** — essentially equal to the final *achieved* total fail counts (maple 126–148, harbor 69–81). So the shape floor at the best achievable geometry already accounts for almost the whole residual: independent confirmation of §11.7 that the Phase-7 residual is geometry/shape- bound. `MAXSHAPE` was set below the incumbent range (maple 100, harbor 55) so the `pred ≥ incumbent` safety guard is the dominant prune gate (`experiments/ run_9gp_ab.sh`). - *A/B sweep (DONE — negative). maple-court + harbor, seeds 0/1/2, 20000 evals, staged, total fails at budget:* | programme | seed | baseline | reassoc | feas | combined | |-------------|-----:|---------:|--------:|-----:|---------:| | maple-court | 0 | **126** | 131 | 129 | 131 | | maple-court | 1 | **148** | 141 | 151 | 142 | | maple-court | 2 | **134** | 146 | 140 | 144 | | maple-court | mean | **136.0**| 139.3 | 140.0| 139.0 | | harbor | 0 | **72** | 83 | 82 | 81 | | harbor | 1 | **81** | 81 | 80 | 81 | | harbor | 2 | **69** | 70 | 69 | 70 | | harbor | mean | **74.0** | 78.0 | 77.0 | 77.3 | The baseline controls reproduce the §12.2 leu.2 means **exactly** (maple 136.0, harbor 74.0) — a clean control, so the negative is real. Every variant is neutral-to-slightly-worse on every programme: reassoc +3.3/+4.0, feas +4.0/+3.0, combined +3.0/+3.3 (maple/harbor). The feasibility filter *did* prune and explore more topologies in several runs (maple s1/s2 combined 342/319, s2 feas 317 vs the baseline 250) — but the extra topologies did not lower the fail count, and M3 reassociate never produced a win despite reaching new tree shapes. - *Verdict: keep both default-OFF; the Phase-7 residual is NOT reachability- or feasibility-bound.* This is the third independent negative on **search machinery** (§11.4 graded objective, §11.5 niching+restarts, now §12.3 M3 moves + shape pruning), against four positives all from **construction/seed quality** (§11.2, §11.6, §11.7, §12.2). The associativity move reaches new topologies but they are not better; the shape filter saves budget on topologies whose shape floor already matches the incumbent, but — precisely because the floor ≈ the achieved total (calibration above) — there is no lower-fail basin for that saved budget to find. The geometry/shape residual is intrinsic to the *constructed* layouts, not a search-reachability deficit. A full canonical Polish-expression rewrite is **not** justified: its one measurable promise here (associativity reachability) was tested directly and did not pay. - *Residual diagnostic (where the shape fails actually live, maple-court, 6 constructive seeds).* A per-leaf breakdown — to test, not assume, what the next lever would be — overturns the obvious "shape-aware placement" guess: | signal | measured | reading | |---|---|---| | plot utilisation (target/plot area) | **0.44** (0.28–0.54) | NOT density/area-bound — ample slack | | failing leaves / total | **~68 / 73** | shape fails are *uniform*, not concentrated | | dominant factors | **crinkliness 346, size 242**, proportion 121, width 102 | perimeter/area + undersize, both granularity effects | Because nearly *every* leaf fails (not a few mismatched ones), the residual is **not** a room→leaf placement mismatch — there are no well-shaped leaves to place demanding rooms into. The mechanism is **over-granular construction**: 73 small leaves for 52 rooms at 44 % utilisation gives every leaf a high perimeter/area ratio (crinkliness) and rooms below their target area (size). So the measured candidate lever is construction **granularity / leaf shape** (fewer, larger leaves; merge or share leaves across same-class rooms; a coarser spine), NOT shape-aware placement and NOT more search machinery. This is a *hypothesis with a measured motivation*, filed as **`homemaker-py-c3g`** — it is unproven and must be A/B'd against the §12.2 baseline before adoption, same discipline as every lever above. It may also be that 52 distinct rooms simply cannot be well-shaped as 52 leaves at this density, i.e. the residual is the geometry floor of the slicing representation; the experiment is what decides. ### 12.4 Construction granularity A/B (`homemaker-py-c3g`) — DONE (null) + a noise finding The c3g hypothesis tested directly. The cheap **raw-seed probe** (circ-per-room divisor `circ_divisor`, env `CIRCDIV`, default 3) confirmed the mechanism but also its catch: a coarser spine lowers the **shape** floor (maple 135→110, harbor 83→66 as `div` 3→∞) yet raises **access/adjacency** by as much, leaving the raw **total** floor flat-to-worse (maple 198→210, harbor 121→134). `div=3` already sits near the total-floor minimum. Because §12.3 showed shape is the *hard* residual and access/adjacency are *cheap* to repair, the open question was whether that trade pays **end-to-end**. - *End-to-end A/B (20000 evals, staged, total fails at budget; div=3 reuses §12.3):* | programme | div=3 (baseline) | div=6 | div=8 | |-------------|-----------------:|-------------:|----------:| | maple-court | **136.0** | 137.0 | 134.3 | | harbor | **74.0** | 75.3 | — | Per-seed: maple div6 143/122/146, div8 132/138/133; harbor div6 65/76/85. **Every arm is within ±1.7 of baseline** — inside the noise floor (below) — with a huge per-seed spread (maple div6 122–146). *Null result:* coarsening the spine does not pay end-to-end. The raw-probe prediction held — the shape-floor gain is cancelled by access/adjacency damage that is *not* free to repair after all. - *A reproducibility finding surfaced en route (`homemaker-py-xcy`, P2 bug) — later RE-DIAGNOSED and FIXED (2026-06-22).* The `div=3` control gave **129** vs §12.3's **126** for the same maple seed 0. The first diagnosis blamed `operators._assign_adjacency_aware` iterating `id()`-ordered Python sets of `Node`s — **this was wrong.** That function already ends every `max`/`min` with a unique leaf-`idx` tiebreak, and its set unions are used only for membership, so order never leaks: `constructive_topology(seed=0)` is **byte-identical across processes** for every example programme (stable sha1, e.g. maple `e688f744326b`). The "sig hashes 4480 vs 16064" was a **measurement artifact** — Python's builtin `hash()` of a *string* is salted per process (`PYTHONHASHSEED`), so an *identical* signature hashes to different ints run-to-run (reproduced 51920/5342/59970 for one identical string). Use `genome.signature` equality or a stable hash, never builtin `hash()`, to compare topologies. The **real** cause was parallel-only: `driver._run_batch` admitted futures via `concurrent.futures.as_completed`, i.e. in **completion order**, and `admit()` is order-sensitive (accrues `n_evals` per result; keeps the *first* individual of an equal-key tie as `best`). A long parallel run diverged **167 vs 161 fails** (maple seed 0) — the true source of the ±3..6 "noise". **Fix:** iterate the futures in *submission* order (`for f in futs: f.result()`; all still run concurrently), reproducing the serial admission sequence. After the fix two `workers=4` runs are byte-identical (162 fails). Serial (`workers=1`) was already byte-for-byte reproducible. Implication for the §11/§12 ledger: per-seed numbers are reproducible **only at a fixed worker count**. Serial≠parallel is *expected* (children/iteration = 1 vs `n_workers` changes batch granularity, hence the search), not nondeterminism. Any A/B that compared runs at *different* worker counts — or any pre-fix parallel run — conflated this with a real effect; sub-±3 effects (the §12.3 +3-4 negatives, the §12.4 ±1.7) should be re-run at a single fixed worker count before being trusted as magnitudes. - *Verdict: keep `circ_divisor=3` default; the granularity lever is null.* Together with §12.3 this closes the residual-reduction question for now from both sides: neither search machinery (§12.3) nor construction granularity (§12.4) moves the maple/harbor geometry residual beyond noise. The weight of evidence is that the residual is the **geometry floor of the slicing representation** at this room density — 52 distinct rooms as 52 adjacency-connected leaves inherently incur ~135 shape+access fails. Further progress, if wanted, needs either the determinism fix (to even see sub-±3 effects) or a representational change beyond the slicing tree — not another seed/search tweak at this scale. - *§12.3 re-run at fixed worker count — CONFIRMED, no new run needed (`homemaker-py-h10`, 2026-07-30).* §12.4's own writeup flagged the §12.3 reassoc/feas negatives (+3.3/+4.0) as sub-±3-adjacent and asked for a re-run "at a single fixed worker count" post-fix, since they predate the completion-order determinism fix above. Checked before re-running the full 8.3-hour sweep: `experiments/run_9gp_ab.sh` invokes `run_staged_search.py`, which never threads a worker count through to `driver.search_staged` — every §12.3 arm therefore already ran at `n_workers=1` (serial), the one mode §12.4 itself already proved "was already byte-for-byte reproducible" even *before* the fix (the bug was in `ProcessPoolExecutor` `as_completed` ordering, parallel-only; serial has no futures to reorder). Confirmed empirically too: re-running one arm (harbor-house seed 0, baseline config, budget 300) twice back-to-back reproduced identical fail counts at every logged checkpoint. So the §12.3 table was already measured at a fixed (and the most reproducible available) worker count — the determinism fix changes nothing for it. **Verdict stands as CONFIRMED-NULL** without re-spending the ~8 core-hours a full re-run would cost; upgrades §12.3's negative from "should be re-run" to "already valid as measured." ## 13. Phase 8 — lowering the geometry/shape floor (`homemaker-py-erc`) Phase 8 runs DIAGNOSTICS FIRST to decide *which* floor-lowering lever to invest in, then the construction/inner-loop experiments in dependency order. §12.3/§12.4 established the floor is real (search machinery and circulation-granularity both null); the open question is *what about the floor* — per-leaf slicing tax, or fixable cuts — and *where the slack hides* (util 0.44 yet rooms undersize). ### 13.1 Diagnostic A: per-leaf shape-fail vs density/granularity (`homemaker-py-erc.1`) — DONE GATES leaf-sharing (`erc.3`) vs compactness-cuts (`erc.5`). Reads only; no A/B, no baseline reproduction. Builds the §12.2 constructive seed (adjacency- and proportion-aware), lays it out at the proportion-aware TARGET geometry — the squarest geometry the inner loop warm-starts from, exactly as `operators.predicted_shape_fails` — then counts size/width/proportion/crinkliness fails per leaf. Script: `experiments/diag_leaf_shapefail.py` (seeds 0/1/2). *View 1 — cross-programme density sweep* (per-leaf rate = shape-fails ÷ leaves): | programme | rooms | leaves | l/room | util | shape | /leaf | siz/lf | wid/lf | prp/lf | crk/lf | |------------------|------:|-------:|-------:|-----:|------:|------:|-------:|-------:|-------:|-------:| | programme-house | 6 | 9.0 | 1.50 | 0.83 | 8.0 | 0.889 | 0.000 | 0.519 | 0.222 | 0.148 | | harbor-house-l0 | 13 | 13.0 | 1.00 | 0.31 | 19.0 | 1.462 | 0.231 | 0.154 | 0.487 | 0.590 | | harbor-house | 37 | 45.0 | 1.22 | 0.50 | 87.3 | 1.941 | 0.519 | 0.378 | 0.296 | 0.748 | | maple-court | 52 | 73.0 | 1.40 | 0.54 | 134.3 | 1.840 | 0.562 | 0.224 | 0.251 | 0.804 | Per-leaf shape-fail SATURATES at ~1.8–1.9 once the programme is non-trivial: the tiny 6-room case is the only outlier (0.89, no size fails, high util 0.83), and the three larger programmes cluster at 1.46→1.94 with no dependence on leaves-per-room (which barely moves, 1.0–1.5). Cross-programme "density" here is confounded by plot/room-mix/util (util swings 0.31→0.83), so this view alone cannot separate "intrinsic per-leaf tax" from "more leaves, worse cuts". *View 2 — synthetic granularity sweep, maple-court, room set FIXED, leaf count varied via the c3g `circ_divisor` knob* (the controlled test): | circ_div | leaves | l/room | util | shape | /leaf | siz/lf | wid/lf | prp/lf | crk/lf | |---------:|-------:|-------:|-----:|------:|------:|-------:|-------:|-------:|-------:| | 2 | 81.0 | 1.56 | 0.46 | 139.0 | 1.716 | 0.477 | 0.169 | 0.226 | 0.844 | | 3 | 73.0 | 1.40 | 0.54 | 134.3 | 1.840 | 0.562 | 0.224 | 0.251 | 0.804 | | 4 | 68.0 | 1.31 | 0.44 | 126.7 | 1.863 | 0.495 | 0.294 | 0.289 | 0.784 | | 6 | 65.0 | 1.25 | 0.47 | 126.0 | 1.938 | 0.554 | 0.303 | 0.262 | 0.821 | | 9 | 63.0 | 1.21 | 0.50 | 116.3 | 1.847 | 0.481 | 0.280 | 0.339 | 0.746 | With the programme held fixed, the per-leaf shape-fail rate is **FLAT** as leaf count varies (1.72–1.94, no monotone trend; if anything a slight *rise* as you coarsen, since the survivors are bigger but still fail). Crucially **TOTAL shape fails track leaf count almost linearly** (139 → 116 as leaves 81 → 63), and crinkliness — the dominant factor (crk/lf ≈ 0.75–0.84) — is itself flat per leaf. Each leaf carries a roughly fixed ~1.8 shape-fail tax regardless of how finely the *same plot* is sliced. The target layout already picks the squarest-aspect cut direction (`_size_divisions_from_targets` chooses rotation for squarest children), so leaves are already near-optimally shaped and STILL fail at ~1.8/leaf — there is little compactness headroom left to recover at fixed leaf count. **VERDICT — per-leaf shape-fail is FLAT vs slicing density (controlled view 2) → the floor is INTRINSIC to per-leaf slicing, not to cut quality.** By the diagnostic's decision rule this *prioritises leaf-sharing* (`erc.3` — fewer leaves for the same rooms is the only lever that moves the floor) and *deprioritises compactness-aware cuts* (`erc.5` — cuts are already squarest and still pay the tax; little headroom at fixed count). Note this is *not* the §12.4 `circ_divisor` null: that lever removed CIRCULATION leaves and the shape gain was cancelled by access/adjacency damage; leaf-sharing removes ROOM-leaf count (multi-room leaves) without disturbing the circulation spine, so the access penalty that killed c3g need not apply. Recommendation: close/deprioritise `erc.5`, advance `erc.3`. ### 13.2 Diagnostic B: undersize-despite-slack localization (`homemaker-py-erc.2`) — DONE GATES plot-fill construction (`erc.4`) vs the inner-loop slack-expansion term (`erc.6`). The §12.3 paradox: plot utilisation ≈ 0.44 (over half the plot "empty") yet rooms are UNDERSIZE. Where is the slack stranded, and at which stage should it be spent? Reads only. Builds the §12.2 constructive seed (whose geometry already sits at the proportion-aware TARGET ratios — the inner-loop warm start, so it *is* the "before" state), measures per sized-room leaf achieved-vs- target area and a plot accounting, then runs `innerloop.optimise` (nm, budget 80 = the bootstrap child budget) and re-measures. Script: `experiments/diag_slack_localization.py` (harbor-house + maple-court, seeds 0/1/2). | programme | state | sizeF | util | tgtFill | ā/t | %und | %ovr | sized% | circ% | out% | |--------------|------------------|------:|-----:|--------:|----:|-----:|-----:|-------:|------:|-----:| | harbor-house | BEFORE (target) | 23.3 | 0.50 | 0.50 |1.43 | 43 | 12 | 50 | 46 | 4 | | harbor-house | AFTER (innerloop)| 21.7 | 0.49 | 0.50 |1.40 | 54 | 16 | 49 | 46 | 4 | | maple-court | BEFORE (target) | 41.0 | 0.54 | 0.44 |1.46 | 42 | 15 | 54 | 43 | 3 | | maple-court | AFTER (innerloop)| 37.3 | 0.53 | 0.44 |1.46 | 42 | 19 | 53 | 44 | 3 | (util = sized-room area ÷ plot; tgtFill = Σ room targets ÷ plot; ā/t = mean achieved/target over sized leaves; %und/%ovr = leaves below 0.9× / above 1.1× target.) **The "56 % empty plot" is a misreading.** Sized rooms already occupy ~50–54 % of the plot and hold **1.4–1.5× their aggregate target area** (util > tgtFill); the other ~46 % of the plot is **circulation**, not claimable void (out/uncovered is only 3–4 %). So rooms are *over*-provisioned in total — there is no unused plot to hand them. **The size fails are pure MALDISTRIBUTION, set by SLICING POSITION not by need.** The median room sits right at target (a/t ≈ 1.0), but a long undersize tail (p25 ≈ 0.35, min 0.05) starves while a few giant leaves balloon (max **6.8×** harbor, **14.7×** maple). Decisively, *the same room type with the same target lands at both extremes* — harbor `r` (target 10 m²) appears at 68 m² (6.8×) and 2.3 m² (0.23×); maple `n` (target 60 m²) appears near target and at 2.7 m² (0.05×). A leaf's area is dictated by its depth/position in the binary slicing tree (ratios multiply down the ancestry), essentially independent of its target; `_size_divisions_from_targets` sets each *local* cut proportionally but cannot defeat the multiplicative depth effect. This is the same root cause as §13.1 (the binary-slicing structure), now seen on the size axis. **The inner loop cannot repair it.** Over budget 80 the size fails move only −1.6 (harbor) / −3.7 (maple), util is flat-to-down, and %undersize is flat-to- *worse* (43→54 harbor). On a frozen topology the equal-offset ratio DOF cannot shrink a 14× leaf to feed a starved one without trading into shape fails (the 0.5ⁿ cliff, §4.5, blocks it), and the symmetric size Gaussian (`quality_size` is `gaussian(area, 1, target, σ)`) gives no net reward for redistribution. **VERDICT — the slack is depth-driven maldistribution inside the room set, not unclaimed plot, and the inner loop (frozen-topology ratios) provably cannot move it.** This *falsifies plot-fill construction* in the "claim the empty plot" sense (`erc.4` as scoped — rooms are already 1.4× over aggregate target; the empty- looking plot is circulation) and *deprioritises the inner-loop slack-expansion term* (`erc.6` — wrong DOF: ratios on a frozen tree cannot undo a depth-set 14× leaf, and the blocker is position not a missing expansion reward). The fix must live UPSTREAM of the inner loop, where leaf area is actually decided: construction that balances tree DEPTH so equal-target rooms land at comparable depth / caps giant leaves (re-scope `erc.4` from "plot-fill" to **depth-balanced / giant- splitting construction**), reinforcing §13.1's call to advance leaf-sharing (`erc.3`) for the starved tail. Recommendation: re-scope `erc.4`, deprioritise `erc.6`. ### 13.3 Experiment: leaf-sharing / multi-room leaves (`homemaker-py-erc.3`) — DONE The lever §13.1 named as the *only* one that moves the floor: collapse same-code rooms into fewer, larger **shared** leaves so the per-leaf ~1.8 shape tax is paid once per group instead of once per room. Unlike c3g (§12.4) this removes ROOM-leaf count, not circulation, so the access/adjacency penalty that sank c3g need not apply. **Mechanism — explicit, type-guarded per-leaf multiplicity.** A construction stamps `leaf.share = k` and `leaf.share_type = code` on each shared leaf (`operators._share_rooms` groups a sized, multi-instance code into runs of ≤ `N` = `leaf_share_factor`; `_leaf_mult_from_plan` stamps the survivors and `_size_divisions_from_targets` sizes them to `k × target`). The fitness honours `k` only while `leaf.type == leaf.share_type` (`graph.leaf_share`), so any retype/undivide silently invalidates a stale share — the mutation operators need no resets, and a small leaf can never *retype* its way into claiming rooms it does not provide. Two scoring sites, both gated by a default-OFF `leaf_sharing` key (controls reproduce the §12.2 baseline exactly — 214 tests pass with it off): - `graph.check_space_counts` counts **coverage** (Σ per-leaf `k`) against `req.count`, so one shared leaf satisfies several same-code rooms with no missing fail; - `fitness.quality_size` centres the size Gaussian on `k × target` (σ scaled by `k`). `quality_proportion`/`quality_width` need no change — a proportionally-scaled leaf keeps its aspect and only gets wider. *Design history:* the first cut recovered `k` from area (`round(area/target)`) to avoid genome state, but the §13.2 depth maldistribution left shared leaves below `k × target`, so `round` undercounted and **17–44 missing fails leaked back** (harbor `share3`+il: 87.3 total, 16.7 missing; the inner loop could not close it — frozen-topology ratios, §13.2). Switching to **explicit** `share` (an undersize shared leaf is *present* → a light size fail, not a heavy missing fail) closes the leak. Because the phenotype tree is never rebuilt from the genome in the hot path (`genome.decode` is unused; operators edit `dom.Node` trees in place), the two `Node` fields survive the whole search via deepcopy without threading through `GNode`/encode/decode; `.dom` serialisation emits `share` only on a live shared leaf. **Floor probe** (`experiments/diag_leaf_sharing.py`, harbor + maple, seeds 0/1/2) — build the §12.2 seed both ways, score at the seed geometry and again after `innerloop.optimise` (nm, budget 80) under the *same* objective. Averaged fails: | programme | mode | leaves | total | missing | size | crink | |-----------|-------------|-------:|------:|--------:|-----:|------:| | harbor | OFF +il | 45.0 | 120.3 | 0.0 | 21.7 | 33.7 | | harbor | share2 +il | 31.7 | 86.0 | 0.0 | 15.3 | 22.0 | | harbor | share3 +il | 25.7 | 73.3 | 0.0 | 12.7 | 17.7 | | maple | OFF +il | 73.0 | 194.7 | 0.0 | 37.3 | 58.3 | | maple | share2 +il | 52.0 | 145.7 | 0.0 | 25.7 | 41.3 | | maple | share3 +il | 47.0 | 133.0 | 0.0 | 21.0 | 39.3 | **The floor moves and the leak is closed** — `share3` cuts the achievable floor **−39 % harbor (120.3 → 73.3) / −32 % maple (194.7 → 133.0)** with **zero missing fails**, and the missing did *not* re-emerge as size fails (size still falls, 22→13 harbor / 37→21 maple). The drop is exactly where §13.1 predicted: shape factors fall with leaf count (harbor leaves 45→26, crinkliness 34→18). Larger `leaf_share_factor` helps monotonically here (share2 → share3), bounded by `leaf_share_max` (default 4). **Verdict — leaf-sharing is the floor-mover §13.1/§13.2 called for: −32…−39 % on the achievable floor, no missing-fail leak.** The flag is threaded through the staged driver (`driver.search`/`search_staged` → `constructive_topology` / `lift_base_to_storeys`) and exposed for the A/B via `LEAFSHARE`/`LEAFSHAREFAC` in `run_staged_search.py` (which injects the objective into the inner-loop and final-score fitness, both arms on one programme dir). Smoke-tested end-to-end (harbor, staged, leaf_sharing+factor 3: re-score OK). **End-to-end A/B** (`experiments/run_leafshare_ab.sh`, staged search, 20 000 native evals, seeds 0/1/2, `leaf_share_factor=3` vs the default-OFF baseline, final native re-score): | programme | baseline (s0/1/2) | mean | leaf-share f3 (s0/1/2) | mean | Δ | |-----------|-------------------|-----:|------------------------|-----:|------:| | maple-court | 129 / 148 / 134 | 137.0 | 78 / 89 / 92 | 86.3 | **−37 %** | | harbor-house | 72 / 81 / 69 | 74.0 | 50 / 52 / 49 | 50.3 | **−32 %** | **VERDICT — leaf-sharing is the first lever to move the Phase-8 floor, and it moves it decisively: −37 % maple / −32 % harbor end-to-end.** The default-OFF baseline arm reproduces §12.2 exactly (maple 137.0 vs 136.0, harbor 74.0 vs 74.0), so the gap is the lever, not drift; and the separation is total — *every* share run beats *every* baseline run on the same programme (maple worst-share 92 < best-baseline 129; harbor 52 < 69). Fewer leaves also make each eval cheaper, so the share arm runs ~35 % faster at equal budget. This is the §13.1/§13.2 prediction realised: the per-leaf ~1.8 shape tax is intrinsic, so collapsing 52→47 / 45→26 room-leaves is what lowers the floor — and the explicit type-guarded multiplicity (vs the area-derived first cut) is what lets the gain survive without a missing-fail leak. Scoreboard update: this is the **5th** win from construction/seed quality and the first floor-mover of Phase 8; it confirms §12.3's thesis that only lowering the geometry floor (not search machinery) can help. Follow-ups: surface `leaf_sharing` on the `homemaker-evolve` CLI / as a `patterns.config` key for production use, sweep `leaf_share_factor`/`max_share`, and test the `erc.4` depth-balancing synergy (shared leaves at correct absolute area) now that the leak is closed. ### 13.4 Experiment: depth-balanced construction (`homemaker-py-erc.4`) — DONE (modest) The lever Diagnostic B (§13.2) called for. B localized the size fails to depth-driven **maldistribution**: a leaf's area is the product of cut fractions down its ancestry in the binary slicing tree, so the same-target room lands at 0.05× and 14.7× by *slicing position*, and the inner loop (frozen topology) provably cannot move it. The fix must live in construction, where leaf area is decided. **Mechanism — depth-balanced tree growth.** `_grow_leaves` grew the tree by splitting a *random* leaf each step → a random caterpillar whose leaves sit at wildly different depths. The `depth_balanced` flag instead always splits a *shallowest* current leaf (`operators._leaves_with_depth`), growing a near-complete binary tree so all leaves land at comparable depth. The proportion-aware sizing pass (`_size_divisions_from_targets`) then hits each target with cut fractions near their proportional value instead of compounding `fmin`/`fmax` clamp error down a deep spine. Type-agnostic and topology-only — it changes *which* leaf is split, not the type assignment or the proportional sizing — so it composes with adjacency-aware seeding and leaf-sharing unchanged. Default OFF (214 tests pass with it off); threaded through `constructive_topology` / `lift_base_to_storeys` → `driver.search`/`search_staged`, exposed via `DEPTHBAL` in `run_staged_search.py`. **Floor probe** (`experiments/diag_depth_balance.py`, harbor + maple, seeds 0/1/2) — build the §12.2 seed OFF vs balanced (vs balanced+share3 as the `erc.7` preview), score at the seed geometry and after `innerloop.optimise` (nm, budget 80). `dDep` = leaf-depth spread (max−min); `maxR`/`minR` = max/min achieved/target over sized leaves; `%und` = fraction below 0.9×target. Averaged: | programme | mode | leaves | total | size | crink | %und | maxR | minR | dDep | |-----------|-------------|-------:|------:|-----:|------:|-----:|-----:|-----:|-----:| | harbor | OFF +il | 45.0 | 120.3 | 21.7 | 33.7 | 54.2 | 12.0 | 0.1 | 7.0 | | harbor | bal +il | 45.0 | 106.0 | 21.0 | 31.3 | 25.0 | 8.3 | 0.2 | 1.0 | | harbor | bal+sh3 +il | 25.7 | 65.3 | 11.7 | 17.3 | 29.0 | 4.1 | 0.3 | 1.0 | | maple | OFF +il | 73.0 | 194.7 | 37.3 | 58.3 | 42.3 | 16.4 | 0.0 | 6.7 | | maple | bal +il | 73.0 | 173.0 | 37.3 | 61.7 | 22.4 | 6.2 | 0.2 | 1.0 | | maple | bal+sh3 +il | 47.0 | 113.7 | 22.3 | 38.7 | 17.7 | 7.9 | 0.4 | 2.0 | **The depth spread collapses (7→1) and the giant leaf is tamed** — maxR 12.0→8.3 harbor / 16.4→6.2 maple, %undersize 54→25 / 42→22 — at **equal leaf count** (45 / 73, no rooms removed). The achievable floor drops **−12 % harbor (120.3→106.0) / −11 % maple (194.7→173.0)** purely from tree *shape*, with zero missing-fail leak. Most of the total drop is in width/proportion (the giants were the wide, wrong-aspect leaves), not the soft size Gaussian (size barely moves). Crucially it is **additive with leaf-sharing**: `bal+sh3` beats §13.3's `share3`-alone floor (harbor 65.3 vs 73.3, maple 113.7 vs 133.0) — balancing places the *survivors* of sharing at correct absolute area, exactly the synergy `erc.7` was filed for. **End-to-end A/B** (`experiments/run_depthbal_ab.sh`, staged search, 20 000 native evals, seeds 0/1/2, `DEPTHBAL=1` vs default-OFF baseline, leaf-sharing OFF in both arms, final native re-score): | programme | baseline (s0/1/2) | mean | depth-bal (s0/1/2) | mean | Δ | |-----------|-------------------|-----:|--------------------|-----:|------:| | maple-court | 129 / 148 / 134 | 137.0 | 142 / 126 / 119 | 129.0 | **−5.8 %** | | harbor-house | 72 / 81 / 69 | 74.0 | 67 / 77 / 71 | 71.7 | **−3.2 %** | **VERDICT — depth-balancing is a real but MODEST standalone lever: −5.8 % maple / −3.2 % harbor, much smaller than the −11/−12 % the seed-floor probe predicted, and the arms OVERLAP** (maple balanced worst 142 > baseline best 129; harbor balanced 77 > baseline 69) — *not* the total separation leaf-sharing showed (§13.3, every share run beat every baseline). The default-OFF baseline reproduces §12.2 exactly (maple 137.0 vs 136.0, harbor 74.0 vs 74.0), so the comparison is clean and the small gap is the lever, not drift. **The 20k search erodes most of the seed-floor advantage**: the random-caterpillar arm partly catches up via divide/undivide mutations over the budget, so an 11 % lower *seed* floor realises only ~5 % at convergence. This is the mirror image of the §12.3/§11 thesis — seed quality helps, but here the search recovers enough of the gap that depth-balance *alone* is marginal, unlike the structural leaf-count cut of §13.3 which the search cannot undo (you cannot mutate 26 leaves back up to 45 cheaply). Its real promise is the **additive floor with leaf-sharing**: the probe showed `bal+sh3` beats `share3`-alone by a wide margin (harbor 65.3 vs 73.3, maple 113.7 vs 133.0) because balancing places the *survivors* of sharing at correct absolute area. The decisive end-to-end test is therefore `erc.7` (depth-balance × leaf-sharing synergy + factor sweep), not depth-balance in isolation. Recommendation: keep `depth_balanced` (default OFF, no test/runtime cost, same leaf count), advance `erc.7` to test whether the additive seed floor survives to convergence when stacked on the share lever that the search *cannot* erode. Scoreboard: a 6th construction/seed lever, but the first Phase-8 lever whose end-to-end gain is *materially* smaller than its seed-floor gain — a useful calibration of how much seed-floor reduction the staged search actually banks. ### 13.5 Experiment: leaf-sharing × depth-balancing synergy (`homemaker-py-erc.7`) — DONE (synergy confirmed) The decisive test the §13.4 floor probe set up. Depth-balancing was only MODEST standalone (§13.4: −5.8 % maple / −3.2 % harbor, overlapping arms) because the 20k search erodes a tree-shape seed advantage via divide/undivide. But the probe showed `bal+sh3` beats `share3`-alone at **equal leaf count** (harbor 65.3 vs 73.3, maple 113.7 vs 133.0) — additive on the leaf-COUNT cut the search *cannot* erode (you cannot mutate 26 leaves back up to 45 cheaply). Question: does that additive seed-floor advantage survive to convergence once stacked on the share lever that the search can't undo? **Setup** (`experiments/run_synergy_ab.sh`, staged search, 20 000 native evals, seeds 0/1/2, final native re-score). Both arms hold `LEAFSHARE=1` at factor 3 (the §13.3 winner). The control arm is share-alone (`DEPTHBAL=0`) and must reproduce §13.3; the experiment arm adds `DEPTHBAL=1` (depth-balanced grow). One programme dir per programme — `run_staged_search.py` injects `leaf_sharing` into the whole pipeline so both arms score under the same relaxed objective. | programme | share-alone db0 (s0/1/2) | mean | bal+share db1 (s0/1/2) | mean | Δ | |-----------|--------------------------|-----:|------------------------|-----:|------:| | maple-court | 78 / 89 / 92 | 86.3 | 76 / 85 / 86 | 82.3 | **−4.6 %** | | harbor-house | 51 / 52 / 49 | 50.7 | 41 / 41 / 38 | 40.0 | **−21.1 %** | The control arm reproduces §13.3 exactly (maple 86.3 = 86.3, harbor 50.7 ≈ 50.3), so the comparison is clean and the gap is the lever, not drift. **VERDICT — the synergy is REAL and SURVIVES to convergence, unlike depth-balance alone.** Harbor is **decisive**: −21 %, every seed improves by 10–11 fails, and the arms are **non-overlapping** (bal+share worst 41 < share-alone best 49) — the total separation §13.4-standalone never reached. Maple is **modest but uniform**: −4.6 %, every seed improves (−2 / −4 / −6), ranges overlapping. This is the mirror image of §13.4: there the seed-floor advantage washed out because the search could erode tree *shape*; here depth-balancing rides on top of the leaf-COUNT cut that the search cannot erode, so balancing the survivors of sharing onto their correct absolute k×target area banks. The probe prediction held — `bal+sh3` beats `share3`-alone end-to-end, not just at the seed. **Factor sweep** (`experiments/run_sharefactor_sweep.sh`, `leaf_share_factor` 2/4 under bal+share, seeds 0/1/2, vs the factor-3 bal+share above): | programme | factor 2 | factor 3 | factor 4 | |-----------|---------:|---------:|---------:| | maple-court | 92.7 | **82.3** | 83.3 | | harbor-house | 53.0 | 40.0 | **39.7** | **Factor 3 confirmed as the robust default once depth-balancing is stacked.** Factor 2 regresses on both (maple +10.4, harbor +13.0) — too little sharing leaves more, smaller rooms for the depth-balance to fix. Factor 3 and 4 are statistically tied (maple f3 wins by 1.0, harbor f4 wins by 0.3 — both inside seed noise, ranges overlap), so factor 4 buys nothing material and gives up maple while risking larger shared leaves. `leaf_share_max` (scoring cap, default 4) already credits every multiplicity at factor ≤4 with zero missing-fail leak (final re-score OK in all runs), so it needs no separate sweep at the chosen factor 3. Recommendation: make `depth_balanced` + `leaf_sharing` (factor 3) the default Phase-8 stack (both default OFF today, no test/runtime cost). Scoreboard: the first Phase-8 lever *combination* whose end-to-end gain (harbor −21 %) exceeds either lever alone (share −32 %→ this stacks a further −21 % on top; depth-balance −3 % alone), confirming the §13.4 thesis that levers the search cannot erode compound where shape levers do not. ### 13.6 Experiment: interior-O courtyard / light-well seeding (`homemaker-py-ld2`) — DONE (positive on dense floors) The construction lever aimed at the erc crinkliness residual directly. The adjacency-aware seeder placed ONE `O` on the most PERIPHERAL leaf — where the adjacent rooms already have plot facade, wasting the daylight source — while the landlocked rooms (no facade, no uncovered-`O` neighbour → `area_outside` ≈ 0 → crinkliness ≈ 0 → fail) get nothing. This arm instead seeds `O` as INTERIOR light wells (the most-landlocked leaves first, greedily spread so each illuminates a fresh room set) and scales their count with the room count. **Seed diagnostic first** (the epic mandate). Decomposing every crinkliness fail in the bal+share seed by side of the gaussian: **all** are UNDER-exposed (crink < 0.62, landlocked) — **zero** over-exposed slivers (crink > 21.7). So the residual is genuine under-daylighting, validating the premise (and correcting the epic's loose "high perimeter/area" wording — the *failing* leaves are starved, not over-walled). The naive default `outside_divisor=6` was **null** (too few/small wells; harbor seed 147→142, crinkliness even rose). Sweeping the divisor found `odiv=3` seed-optimal: harbor seed fails 147→129 (−18), maple 219→206 (−14), landlocked fails down — at the cost of more leaves (harbor +4, maple +8). Because it ADDS leaves it carries the §13.4 wash-out risk, so the convergence A/B decides. **Setup** (`experiments/run_interioro_ab.sh`, staged search, 20 000 native evals, seeds 0/1/2, final native re-score). Both arms hold the default stack `LEAFSHARE=1` (factor 3) + `DEPTHBAL=1`. Control is interior-OFF (peripheral `O`) — must reproduce §13.5 bal+share; experiment adds `INTERIORO=1` (odiv=3). | programme | peripheral off (s0/1/2) | mean | interior odiv=3 (s0/1/2) | mean | Δ | |-----------|-------------------------|-----:|--------------------------|-----:|------:| | maple-court | 77 / 85 / 86 | 82.7 | 74 / 78 / 89 | 80.3 | −2.8 % | | harbor-house | 41 / 43 / 38 | 40.7 | 28 / 39 / 35 | 34.0 | **−16.4 %** | The control reproduces §13.5 (maple 82.7 ≈ 82.3, harbor 40.7 ≈ 40.0), so the gap is the lever, not drift. **VERDICT — positive on the DENSE floor, marginal elsewhere.** Harbor is the win the issue targeted (it named "harbor-house ~19 rooms/floor" as where the single peripheral `O` is wasted): **−16.4 %**, every seed improves (−13 / −4 / −3), arms nearly non-overlapping (interior worst 39 ≈ control best 38). Maple is **−2.8 %**, within seed noise — two seeds improve, one regresses (+3), ranges overlap. This is the §13.4 pattern: the seed advantage (harbor −18, maple −14) survives roughly a THIRD on harbor but mostly washes out on maple, because a dense floor has enough landlocked rooms that the daylight gain outweighs the added-leaf tax, whereas on the sparser maple the +8 leaves nearly cancel it. Unlike depth-balance-alone (§13.4) which washed out *entirely*, interior-O holds on the dense floor. Recommendation: make `interior_outside` (odiv=3) a default-ON Phase-8 lever (default OFF today). Harbor is decisive and maple is net-neutral (mean still −2.8 %, no programme regresses on mean), so the flip is strictly ≥ on both means and matches the dense-programme target. Follow-up `homemaker-py-*` flips the default (mirroring `pll` after erc.7). `outside_divisor` left at 3 (seed-optimal joint); a finer odiv sweep under convergence is low-prior given maple's marginal response. ### §13.7 High-budget harbor floor probe — 71d go/no-go (homemaker-py-71d.1) The whole Phase-8 construction stack is now default-ON (leaf-sharing factor 3, depth-balanced, interior-O odiv=3, circ_divisor 3, proportion-aware). Cumulative floor vs the §12.2 leu.2 baseline (all under the §13.3 leaf-share-relaxed objective, staged, seeds 0/1/2): **maple 136.0 → 80.3 (−41 %), harbor 74.0 → 34.0 (−54 %)** — the entire drop from construction levers, zero from search machinery, exactly the epic's thesis. This probe decides **71d** (failure-directed topology-repair operator). 71d's premise: the pre-stack harbor 3M-eval plateau (`3m.dom`, re-scores to 27 fails) is dominated by **13 crinkliness** fails, characterised as **landlocked** rooms (`area_outside == 0` → `crink == 0` → `quality_uncrinkliness` hits the `if not crink: return 0.0` branch, fitness.py:355 → guaranteed fail for ALL ratios), repairable only by topology — *specifically interior O courtyards / facade access*. That fix has since shipped DEFAULT-ON (interior_outside, §13.6), so the premise needs re-measuring on the current stack. **Setup** (`experiments/probe_harbor_floor.py`, harbor-house, full default stack, seed 0, **500 000** native evals, staged, SERIAL — the leaf-share relaxed objective is injected by a parent-process `fitness.load_config` monkeypatch that does NOT reach `ProcessPoolExecutor` workers, so every §13.x floor run is serial; see homemaker-py-x3b for the production CLI wiring). The probe re-scores the best and splits each crinkliness fail into **landlocked** (`area_outside == 0`, 71d's ratio-invariant target) vs **under-exposed** (`0 < crink < target`, reachable by ratios/seeding). | metric | old 3M plateau (pre-stack) | full default stack, 500k | |--------|---------------------------:|-------------------------:| | total fails | 27 | **20** | | crinkliness | 13 | **4** | | landlocked crinkliness | ~13 | **2** | | top residual class | crinkliness | edge-too-long (6) | Final residual histogram (20 fails): 6 edge-too-long, 4 crinkliness, 4 size, 2 proportion, 2 width, 2 level-not-connected. Re-score OK (relaxed config consistent end-to-end). **VERDICT — NO-GO on 71d as scoped; interior-O already dissolved its target.** The landlocked-crinkliness block 71d was built to repair collapsed from ~13 to **2 of 20** — because interior-O seeding *is* 71d's named fix (interior O courtyards) and now does it by default. Crinkliness is no longer the dominant class; the residual is small and spread across edge-too-long / size / proportion / width / connected, with **no concentrated ratio-invariant block** for a targeted repair operator to attack. A deterministic repair operator remains a genuine new operator class (not refuted by the §11.4/§11.5/§12.3 search-machinery losses), but its expected value is now low: its highest-leverage target is gone, and what remains is diffuse. Recommendation: close 71d (and prerequisites 7u5/jrb/u8x) as superseded-by-construction; the floor 71d targeted was lowered by interior-O, not by search machinery — consistent with the epic scoreboard. The deprioritised P4 levers erc.5 (compactness cuts — Diag A: floor is leaf-count not cut-quality, and leaf-sharing over-delivered) and erc.6 (inner-loop slack — Diag B: wrong DOF) close wont-fix on unmet revisit conditions, completing the epic. Caveat (honest): single seed, 500k not 3M, relaxed config vs the old strict standalone 27 — so the 20-vs-27 *total* is not a clean apples-to-apples. The robust signal is the **composition collapse** (crinkliness 13→4, landlocked 13→2), which the §13.6 three-seed data corroborates (interior-O reliably cuts harbor landlocked fails). Follow-up observation, not part of this verdict: edge-too-long is now the single largest harbor class (6) — a candidate seed for any future floor work, distinct from the crinkliness regime Phase-8 addressed. ### 13.8 Experiment: share-aware edge-too-long cap (`homemaker-py-hph`) — DONE (positive, harmless) §13.7's follow-up observation (edge-too-long = harbor's top class, 6 fails) is the seed. **Dissection first** (`experiments/diag_edge_too_long.py` on the 500k probe best): the 6 fails are only **2 distinct locations**. (1) DOMINANT ~4/6: leaf `lllr` is a `share=3` leaf — one quad holding 3 rooms (247 m², edges 15–17 m, aspect 1.2, NEARLY SQUARE). Its walls exceed the flat 8 m cap purely because it *aggregates 3 rooms* — a leaf-sharing REPRESENTATION ARTIFACT, not a design flaw. §13.3 relaxed size/missing for shared leaves (`quality_size` centres on k×target) but `edge_cost` (fitness.py) and `outside_edge_cost` still used a flat 8 m regardless of `leaf.share` — the same §13.3 leak on a different measure. (2) ~2/6: leaf `llll`, a 1.2 m × 16.7 m sliver (aspect 14) — a REAL narrow-room pathology, already independently caught by width/proportion; its edge-too-long is the wall it shares with `lllr`. No corridors involved. **Fix.** New `Fitness._edge_cap(*leaves)` scales the 8 m cap by the largest type-guarded `leaf_share` (graph.leaf_share, §13.3's helper) among the adjoining leaves, mirroring `quality_size`'s k×target; non-shared leaves keep the flat cap. Used by both `edge_cost` (interior wall, max share of the two leaves) and `outside_edge_cost` (one leaf). Gated behind a new `share_edge_cap` config knob (`SHAREEDGE` env), default OFF, so the §13.x controls reproduce. On the probe best the lever clears all 6 edge-too-long (20→14 total fails); the `llll` sliver stays flagged via width/proportion. **Setup** (`experiments/run_shareedge_ab.sh`, full Phase-8 default stack LEAFSHARE=1/fac3 + DEPTHBAL=1 + INTERIORO=1/odiv3, staged, 20 000 native evals, seeds 0/1/2, final native re-score). Control SHAREEDGE=0 (flat cap) — must reproduce §13.6/§13.7; experiment SHAREEDGE=1. | programme | flat cap off (s0/1/2) | mean | share-aware on (s0/1/2) | mean | Δ | |-----------|-----------------------|-----:|-------------------------|-----:|------:| | maple-court | 74 / 78 / 89 | 80.3 | 73 / 78 / 71 | 74.0 | −7.9 % | | harbor-house | 28 / 41 / 35 | 34.7 | 27 / 39 / 27 | 31.0 | −10.6 % | The control reproduces §13.7 (maple 80.3 *exactly*, harbor 34.7 ≈ 34.0), so the gap is the lever, not drift. **VERDICT — positive and HARMLESS; recommend default-ON.** Both programmes improve on the mean with **zero regressions across all 6 seeds**: harbor every seed (−1/−2/−8), maple two flat/down + one −18 (seed2). The asymmetry of magnitude (maple's big seed2 swing) is search noise, but the *direction* is structural: the lever only ever *removes* a false-positive fail on an aggregate shared leaf — it cannot add one (non-shared leaves are untouched), so it is monotone-harmless on the objective. This is unlike the §13.4-family construction levers that trade leaves for fails; there is no tax to wash out. Recommendation: flip `share_edge_cap` default-ON for leaf-sharing runs (it is the §13.3 relaxation completed on the wall measure), mirroring the `pll`/`interior_outside` default flips. A follow-up issue flips the default + rebaselines the §13.x floor numbers (harbor 34.7→31.0, maple 80.3→74.0 become the new full-stack baseline). Repro: `experiments/diag_edge_too_long.py`, `experiments/run_shareedge_ab.sh`. ### 13.9 Flip `share_edge_cap` default-ON + rebaseline §13.x floor (`homemaker-py-rq2`) — DONE Acting on the §13.8 recommendation. `Fitness.__init__` now defaults the share-aware edge cap to `self._leaf_sharing` when `share_edge_cap` is unset: under leaf-sharing the cap is ON, mirroring the `pll` bal+share and §13.6 `interior_outside` default flips. An explicit `share_edge_cap=False` still reproduces the pre-flip control arm, so the §13.8 A/B and any §13.x control stay reproducible (`run_staged_search.py` now pins `conf["share_edge_cap"] = share_edge` explicitly in both arms; the `SHAREEDGE` override is preserved). Non-sharing runs (every example `patterns.config`, where `leaf_sharing` is absent) are untouched — a control re-score of `programme-house` reproduces bit-for-bit. **New §13.x full-stack floor** (Phase-8 default stack, staged, 20 000 evals, seeds 0/1/2): **maple-court 80.3 → 74.0, harbor-house 34.7 → 31.0** — the share-aware arm from §13.8 becomes the baseline. `test_edge_cap_flat_when_lever_off_even_with_sharing` now pins `share_edge_cap=False`; `test_edge_cap_defaults_on_under_leaf_sharing` guards the flip. 222 tests pass. ### 13.10 Productionise leaf-sharing: per-code `share` + CLI wiring (`homemaker-py-x3b`) — DONE Make the §13.3 lever a first-class, programme-author-controllable feature instead of an experiment-only env var + monkeypatch. Three pieces: **1. Per-code grain (`SpaceReq.share`).** `patterns.config` spaces accept an optional `share: N` → `SpaceReq.share` (int, default 1 = not shareable; a `has_share` flag distinguishes an explicit `share: 1` from the default). `operators._share_grain(req, leaf_share_factor)` resolves each code's grain from the global selector: - `leaf_share_factor == 0` — **per-code opt-in**: a code shares iff it sets `share: N≥2`; this is the safe default-on philosophy (sharing off unless the author asks, per space). - `leaf_share_factor ≥ 2` — **global mode**: every sized code shares at the factor, with an explicit `share` overriding (`share: 1` opts a code OUT, `share: N` sets that code's grain to N). Reproduces the §13.3 experiment with **no edits to example programmes** (so §13.3/§13.9 baselines stay reproducible). Only sized codes are ever shareable (an unsized c/o/s absorbs slack — no target to centre `k` rooms on). `_share_rooms` now groups per resolved grain. **2. End-to-end conf injection.** The §13.3 scoring sites gate on a `leaf_sharing` conf key, but example `patterns.config` files don't set it — the experiment harness monkeypatched `fitness.load_config` to inject it. Productionised cleanly: `load_config(dir, overrides=None)` merges run-level keys last, and `driver.search` / `innerloop.optimise` / `NativeEvaluator` / `_fitness_for` thread `conf_overrides={"leaf_sharing": True}` through both the inner-loop scorer and the off-tree grade/feasibility scorer when sharing is on. So the whole pipeline scores under the relaxed objective the shared seed targets, with no monkeypatch and no on-disk edits. (`share_edge_cap`'s §13.9 default-ON-under-sharing derivation in `Fitness.__init__` rides along automatically.) **3. CLI.** `homemaker-evolve` gains `--leaf-sharing/--no-leaf-sharing` (default ON, `HOMEMAKER_LEAF_SHARING`) and `--leaf-share-factor N` (default 3, `HOMEMAKER_LEAF_SHARE_FACTOR`), threaded to `driver.search`. Default-OFF parity holds: `overrides=None` leaves `load_config` byte-identical and `_share_rooms` is never reached. Smoke-checked end-to-end on harbor-house (sharing on 37 fails vs `--no-leaf-sharing` 95 at budget 160). 233 tests pass. ### 13.11 Residual diagnostic on the current full default construction stack (`homemaker-py-91f`) — DONE The §13.1/§13.2 (`erc.1`/`erc.2`) per-leaf diagnostics predate the depth-balanced + leaf-sharing synergy flip (`erc.7`) and the share-aware edge cap flip (`rq2`/ `x3b`) — the current §13.9 floor (harbor 31.0, maple 74.0) had never been decomposed by fail category. Unlike `erc.1` (which scores a single constructed seed at target geometry, a cheap proxy), this reads the actual best individual from a REAL `driver.search_staged` run — budget 20000, seeds 0/1/2, harbor-house and maple-court, the full default stack (`leaf_sharing`/`leaf_share_factor=3`, `depth_balanced`, `interior_outside`/`outside_divisor=3`, `share_edge_cap` default-on under sharing) — the actual reported floor, not a proxy. **Methodology note — a scoring pitfall found along the way.** The obvious approach (dump each run's best to `.dom`, reload, rescore with matching conf) gives a WRONG, but stable and easy-to-miss, fail count once `collapse_insearch` is doing real relabelling work: on harbor-house seed 0 the search itself reported 37 fails, and `copy.deepcopy(r.best.root)` rescored immediately in-process reproduces 37 exactly, but `dom.dump` + `dom.load` + rescore of the *same* topology gives a stable 53 — 15 extra `missing`/`adjacency`/`level` fails for a level-0 `count: 3` code that collapse-relabelling satisfies in the live tree but that is not present as a literal leaf type once round-tripped. Root cause not yet found (hash-seed randomness and float round-trip loss are both ruled out); filed as `homemaker-py-iio` (P2). A narrower, separate bug — `run_staged_search.py`'s own final sanity rescore omits the `collapse_insearch` override entirely, so its own "MISMATCH" line cannot be trusted whenever leaf-sharing is on — is filed as `homemaker-py-7ua` (P3). This diagnostic sidesteps both: `experiments/run_and_capture_91f.py` scores `copy.deepcopy(r.best.root)` immediately after `search_staged` returns, and writes the fails list to a `*.fails.json` sidecar (verified `rescore_match` on all 6 runs); `experiments/diag_residual_91f.py` tallies fail categories from those sidecars, never rescoring a `.dom` from disk. **Result (mean fails/seed; category % of all fails, combined):** | programme | seeds (fails) | mean | vs §13.9 cited floor | |---|---|---|---| | harbor-house | 37, 33, 30 | 33.3 | 31.0 | | maple-court | 82, 84, 78 | 81.3 | 74.0 | (Both a bit above the cited floor, as expected — a single staged run per seed here vs. whatever selection produced the cited numbers; same order of magnitude, good sanity check that the stack is wired correctly.) | category | combined n | % | |---|---|---| | **crinkliness** | 165 | **48.0%** | | **size** | 71 | **20.6%** | | adjacency (not adjacent) | 20 | 5.8% | | proportion | 13 | 3.8% | | access | 12 | 3.5% | | edge too long (outside) | 12 | 3.5% | | missing (adjacency/level/vertical cascade) | 12 | 3.5% | | circulation not connected | 9 | 2.6% | | edge too long (wall) | 9 | 2.6% | | missing required space | 6 | 1.7% | | (remaining: too-many-spaces, covered-outside, stairs, width, public-access) | 12 | 3.4% | Per-programme shares are consistent (crinkliness 43%/size 21% on harbor-house, crinkliness 50%/size 20.5% on maple-court) — this is not an artefact of one programme. **VERDICT — shape-intrinsic fails (crinkliness + size ≈ 69% of the residual) now completely dominate; construction-completeness fails (missing space, adjacency, level, vertical connectivity — the failure modes the §11–§13 series of construction levers targeted) are now a small tail, ≤6% each.** This revises the `erc.1` recommendation. `erc.1` (§13.1) found per-leaf crinkliness FLAT vs. slicing density and concluded the floor was intrinsic to leaf COUNT, prioritising leaf-sharing (`erc.3`) over compactness-aware cuts (`erc.5`, deprioritised: "cuts are already squarest ... little headroom at fixed count"). Leaf-sharing (plus depth-balancing, interior-O, and the edge cap) is now fully deployed as the default stack, and crinkliness is not just still present but *more* dominant proportionally than in any earlier per-category breakdown in this document (cf. §7's 27/85 and §9's 346/939-ish shares) — the "reduce leaf count" avenue has been substantially exploited by the current stack, yet the per-leaf shape tax persists and is now, by a wide margin, the single largest lever available. **Recommendation: reopen `erc.5`-style compactness-aware cutting (or a crinkliness-targeted construction/mutation lever specifically, since crinkliness outweighs size ~2.3:1) as the next concrete construction lever** — the same diagnostic-first logic that turned §13.7's edge-too-long finding directly into `hph`. ## 14. Island model: multi-run recombination (`homemaker-py-psk`) — DONE (null) **Lever (user-proposed).** Perl Urb ran the search many times and kept the best, because independent runs settle into different local minima. The Python tool is deterministic per `--seed`, so the analog is an *island model with synchronous migration*: run N independent seeds to convergence (Phase A), then PRIME a fresh population with those N converged elites and run a second, crossover-heavy phase (Phase B) to recombine basins. Distinct from §11.5 (`c4c.5`), which injected **fresh** random/constructive seeds for raw diversity and landed null — here the migrants are **fully-converged elites**, high-quality building blocks, so the "diversity does not help" result does not directly refute it. The one untested sub-mechanism: can crossover *stack* wins across independent basins (run A solved cluster X, run B solved cluster Y, child inherits both)? **Design (`experiments/run_island_ab.py`).** Three numbers per programme, all `leaf_sharing` OFF so controls track the §12.2 baselines (maple 136 / harbor 74), all on **equal actual eval budget** (the staged search has a hard ~`pop·child·2` bootstrap floor, so we account `r.n_evals`, never the request): - **`bestN@A`** — best-of-N over Phase A (the FREE reference; these N runs happen anyway — the legitimate descendant of Urb's multi-run habit). - **`island`** — Phase B result: a population primed from the N Phase-A elites via the existing `seed_factory`+`bootstrap` path (no new representation), evolved at `p_crossover=0.7`. Total budget = Phase A + migration. - **`bestN@T`** — best-of-N over N independent runs at the *same total* per seed (the "N+ longer independent runs" control). **THE BAR**: island must beat it. A default-off `child_probe` hook (`driver.search`) instruments the deciding mechanism: for every crossover child it records whether the spliced child beats `max`/`min(parent fails)`. Parent fails are appended to the child lineage as `|pf=a,b` (only when the probe is set) so the signal survives the `ProcessPoolExecutor` pickle round-trip an `id(root)` key cannot. **Result (N=4, master_seed 0, 28160 actual evals/arm, 4 workers):** | programme | bestN@A | island | **bestN@T** | verdict | crossover beat-min-parent | |-----------|--------:|-------:|------------:|---------|--------------------------:| | harbor | 73 | 68 | **67** | loses by 1 (within noise) | 1 / 65 | | maple | 134 | 124 | **116** | loses by 8 (decisive) | 3 / 63 | **Verdict: NULL / negative.** The island model does **not** beat best-of-N at equal total budget. On harbor it ties-to-loses inside the parallel noise band; on maple it loses clearly (124 vs 116) — a single *longer* independent run reached 116 while the migration phase, given the same budget, stalled at 124. The migration phase buys nothing a longer independent run does not. **The mechanistic probe explains why (the deciding diagnostic).** Crossover across independently-converged elites almost never synthesizes: of ~64 crossover children only **1/65 (harbor) and 3/63 (maple)** beat the *better* parent, with a best fail-drop of just 2 and 5. This confirms the issue's **alignment** hypothesis: `operators.crossover` is *area-matched* subtree exchange, but two independently evolved trees encode similar arrangements at different paths/areas (the encoding is non-canonical — `9gp` closed negative), so the splice is mostly disruptive, not combinatorial, and the inner loop re-solves ratios at the boundary (spliced quality not preserved). The null is therefore **mechanistic, not budget**. **Noise caveat (carry forward).** Phase A is unaffected by the probe, yet harbor seed 2 scored 71 then 73 on byte-identical re-runs — parallel/BLAS non-determinism, the same ±2-3 effect §12.4 flagged. Sub-±3 verdicts under `n_workers>1` are noise; both arms here ran at the same worker count so the *comparison* stays fair, and maple's −8 is safely outside the band. This is the third search-machinery null after §11.4 (graded objective) and §11.5 (niching+restarts) / §12.3 (M3 + shape filter), against four construction/seed wins (§11.6, §11.7, §12.2, §13.x). best-of-N at the Phase-A budget remains a free, worthwhile habit; a dedicated migration phase is not worth its budget. The residual stays geometry/shape-bound. NOT gated on canonical encoding (`9gp` closed); the `child_probe` hook is kept default-off for reuse. ## 15. Leaf-sharing output honesty: unfold + polish auto-finish (`homemaker-py-3l6`) — DONE **Bug.** Leaf-sharing (§13.3/§13.10, default ON) is a *fitness-evaluation* knob: a shared leaf of code X with `share=k` is credited as satisfying k programme entries, its size Gaussian re-centred on `k*target`. So the evolve inner objective rewards genomes that under-materialise the programme (fewer, larger rooms), but the winning `.dom` written to disk is that un-materialised genome. Re-scored by the canonical `homemaker-fitness` (sharing OFF), the un-materialised copies become *missing required space (critical)* fails. Measured on harbor-house (init.dom, 3M budget): internal best `1.03e-05` but **canonical `6.73e-29`, 90 fails (15 critical)**. The default silently optimised an objective the canonical scorer does not credit and wrote a catastrophically worse building than its reported internal fitness implied. **Investigation (`homemaker-py-yaa`).** Four fixes were scoped (make no-sharing the default; re-score-and-warn on write; materialise shared leaves on write; anneal the grain to 0 mid-run). yaa characterised the transferability of a sharing-phase solution to the honest objective and reached a **conclusive** result: - Naive warm-start from a raw sharing seed **stalls** (harbor 8.66e-08, 70 fails) — `place_missing`/`divide` cannot dig out the ~15-room count deficit fast enough. - Warm-start **+ unfold** (`operators.unfold_shared_leaves` at the transition) **catches the direct no-sharing route** (4.19e-06, 15 fails, 0 critical), matching the `--no-leaf-sharing` baseline (`nols-2` 4.19e-06). Bruno's key idea confirmed: the sharing phase's transferable value is the **adjacency/topology skeleton**, and the sole blocker to reusing it is the **materialisation (count) deficit** — not the `k*target` sizing mismatch. Unfold pays that deficit down. **`operators.unfold_shared_leaves(root)`.** Replaces every live shared leaf (`share>1`, `share_type==type`) with a balanced binary subtree of k same-code leaves splitting its footprint, sizes each for squarest proportion, and clears the share stamps. Footprint (plot area) is preserved; the adjacency skeleton is otherwise untouched. Returns the number of extra leaves created. **Fix — auto-finish before write (`driver.polish_finish`).** Rather than unfold on write alone (honest room *count* but un-polished proportion/width/size on the fresh children), the finish runs yaa's proven unfold-**then-polish** as an automatic terminal phase. When a run used `--leaf-sharing`, before write: 1. deep-copy the best, `unfold_shared_leaves` it (materialise the deficit); 2. warm-start a `leaf_sharing=False` search (`bootstrap=False`) from the unfolded genome for `--polish-budget` evals — local search under the *honest* objective cleans up the newly materialised rooms. The returned `best.fitness` is then the canonical score (sharing OFF ⇒ internal == canonical), and eval/topology/history accounting is stitched onto the sharing run with the two phases tagged `share:`/`polish:` (the objectives are not comparable, so the histories are concatenated, not merged). `--polish-budget` (env `HOMEMAKER_POLISH_BUDGET`): `-1` = auto = `budget//2`, `0` = unfold + single rescore only (no search). An **interrupt** forces `polish_budget=0` so a stopped run still writes an honest output without triggering a long extra phase. The default stays `--leaf-sharing` ON: its ~35 %-faster topology search (§13.3) is retained, and the output is made honest by the finish instead of by disabling the lever. Option 1 (no-sharing default) and option 2 (warn-only) from the bug were therefore not needed; the annealing option is its own follow-up (Schedule B, `homemaker-py-kpu`) — the single-transition finish here is its proven precursor, and `unfold_shared_leaves` is the primitive it will reuse at each grain step. **Verification.** harbor-house, budget 3000 + polish 1500: the reported polish fitness `4.79788e-27` **matches the canonical `homemaker-fitness` byte-for-byte**, **0 critical fails** (the missing-room criticals are gone — the 15 shared-leaf copies are materialised). Small budget so absolute quality is low, but the honesty — the point of the bug — is restored. Tests: `driver.polish_finish` ×3 (unfold+rescore stitching, polish-search accounting, no-best noop); 254 pass. ## 16. In-run leaf-share grain annealing — Schedule B (`homemaker-py-kpu`) — DONE (negative) **Premise.** §15's finish crosses the sharing→off objective cliff in a *single* hard transition (unfold every shared leaf at once, then polish). Schedule B (yaa's still-open option) instead **ramps the grain down within one continuous run** — e.g. `4 → 3 → 2 → off` — carrying the whole population across each step. Graduated non-convexity: the coarse early grain fixes gross topology/adjacency on a small effective problem (few, large rooms); each step materialises a little more and refines per-room size/proportion/width; no single fitness cliff is crossed at once. The question (kpu): **does a graduated ramp beat the single hard unfold transition** (yaa's warm-chain 4.19e-06) and the direct no-sharing baseline (5.14e-06)? **8iv settled the unfold primitive first (NEGATIVE).** kpu originally "wanted" the circulation-aware unfold from `homemaker-py-8iv` (route the materialised subtree's access through interior children). 8iv built and A/B-tested it and it **lost** to the plain balanced-grid `unfold_shared_leaves` (slice 41 fails vs grid 25 at 150k evals, grid leading throughout). So Schedule B reuses the **existing grid unfold** at every grain step — no slicing reintroduced; access is left to local search on the squarer grid seed (which yaa showed reaches 4.19e-06). **Mechanism (`driver.search_annealed`).** One phase per descending grain in `grain_ladder` (default `(4, 3, 2)`), then a de-share polish: 1. **Phase 0** (`grain = ladder[0]`): a normal `search` — constructs the population at `leaf_share_factor = cap` with the evaluator's `leaf_share_max` capped to `cap` (new `max_share` override, threaded through `_overrides_for`/`_fitness_for`/ `_evaluate`). 2. **Each grain step** (`cap` lowered): before resuming, unfold every population leaf whose `share` *exceeds* the new cap — `operators.unfold_shared_leaves(root, above=cap)` — so the leaves the lower cap would under-credit become real rooms instead of fresh missing fails; the rest stay collapsed for the next step. The whole population is then handed to the next `search` via the new `seed_pop` argument (each root re-optimised and re-scored under the lower cap), preserving topology/adjacency continuity rather than restarting from a single best. 3. **Finish** (`grain off`): unfold all remaining shared leaves (`above=1`) and run a `leaf_sharing=False` search (or a single rescore when `polish_budget <= 0` / on interrupt), so the returned `best.fitness` is the honest canonical score exactly as §15 guarantees (verified: annealed output re-scored by `homemaker-fitness` matches the reported best byte-for-byte). `budget` is split evenly across the sharing phases; `polish_budget` funds the finish. Phases are stitched with cumulative eval/topology accounting and a grain-tagged history (`g4:`/`g3:`/`g2:`/`polish:`) — objectives differ across grains so histories are concatenated, never merged. CLI: `homemaker-evolve --anneal-grain 4,3,2` (implies sharing; self-finishing, so the §15 finish is not applied on top). **Verification (plumbing).** harbor-house, budget 900 (300/phase) + polish 300, 4 workers: unfolds 33 → 22 → 9 leaf-copies across the ramp, population carried (`anneal-seed/*` lineages), honest share-free output whose reported best `3.36672e-29` **matches `homemaker-fitness` byte-for-byte**. (Fails rise at this toy budget — 64 leaves materialised with almost no recovery budget — so absolute quality is meaningless here; the head-to-head below runs at the baselines' budget.) Tests: `unfold_shared_leaves(above=)` grain-cap selectivity; `search(seed_pop=)` population seeding; `search_annealed` phase stitching / honest finish / degenerate-ladder fallback; 258 pass. **Head-to-head (DONE — NEGATIVE).** harbor-house, `init.dom`, seed 0, pop 16, child 80, grain `4,3,2`, budget 1.5M (500k/phase) + polish 1.5M = **3M total** (workers 4, ~22h), matched to the yaa baselines. Result: **1.26e-08, 23 fails** (canonical `homemaker-fitness` byte-for-byte). Both targets **beat it decisively**: | route | fitness | fails | |-------|---------|-------| | direct `--no-leaf-sharing` | 5.14e-06 | 15 | | yaa warm-chain (single hard unfold) | 4.19e-06 | 15 | | **Schedule B (graduated 4→3→2→off)** | **1.26e-08** | **23** | ~400× worse fitness, +8 fails. **Verdict: the graduated grain ramp loses to the single hard sharing→off transition (§15).** The trajectory shows why — each grain step spikes the fail count as its unfolded leaves acquire independent shape fails (phase-end fails 19 → 21 → 27, then the final de-share unfold 27 → 36), and the per-phase budget re-polishes a partially-materialised state that the *next* step materialises further, so the coarse-grain gains (19 fails at grain 4) do not carry forward. Splitting the budget across three intermediate materialisations left the polish phase starting from a deeper hole (36 fails) than the warm chain's single clean transition, and 1.5M polish evals recovered only to 23 — short of the 15 both baselines reach. Graduated non-convexity is **falsified** for this materialisation cliff: the transferable value is the sharing-phase topology skeleton (yaa), and it is best cashed in **once**, at full grain, not annealed. (Caveat: this run used `workers=4` vs the baselines' `workers=1`; the ~400×/+8-fail gap is far larger than worker-count trajectory noise, so the direction is robust.) The machinery is retained (`search_annealed`, `--anneal-grain`, `unfold_shared_leaves( above=)`, `search(seed_pop=)`, the `max_share` evaluator override) — it is correct, tested, and honest, and the `seed_pop` / grain-cap primitives are reusable — but the default finish stays §15's single-transition unfold+polish. Tests: 258 pass. ## 17. Finish-time global cell→room collapse (`homemaker-py-94g`) — DONE (positive) **Motivation — label-relative fails.** A layout's leaf carries a room *type*, and many of a good layout's residual fails are **label-relative**: a cell fails `size` / `width` / `proportion` only because the room *assigned* to it wants dimensions it lacks — relabel that cell to a room it fits and the fail vanishes; a `wrong-level` fail is likewise a labelling error. On the harbor-house best layout (`evolved-3M-nols-3`, 15 fails) ~11 of 15 are label-relative. This is separable from the **geometry-intrinsic** fails §13 chased at the shape floor — long-thin useless cells (`width`/`proportion`/ `crinkliness`) and `not-connected` — which *no* relabelling can fix because the cell's geometry, not its label, is wrong. The collapse targets only the former. **Mechanism (`Fitness.collapse_global`).** A one-shot, finish-time pass that relabels the whole building's room cells in one optimal assignment — the 9o5 per-class collapse (interchange superposition) generalised from one equivalence class to a **global** N inside-leaves ↔ M required-rooms matching (`_best_assignment`: brute-force under the class cap, else Hungarian). SUPPLY = leaves whose type is an assignable room code; DEMAND = every such code expanded by its required count. Constraints, each landed after an empirical correction (below): - **c/o/s partition.** Assignable codes exclude any starting `c`/`o`/`s`. `check_space_counts` (graph.py) skips those as circulation/outside/sahn — *including room codes that collide with the convention* (`cr1` Common Room, `st1`/`st2` Storage). Those leaves are the circulation/structure skeleton and must never be relabelled; the collapse uses the same partition the scorer counts against. - **Hard level.** A leaf may take a room only if its storey matches the room's required level (a −1e12 forbid penalty), so the collapse never *adds* a wrong-level fail. - **Adjacency relaxation.** Geometry is fixed at finish time, so each leaf's graph neighbours are fixed and only labels move. Required adjacencies become a labelling relaxation: warm-started from the evolved labels, each pass is a linear assignment over the base value plus a bonus for each of a code's adjacencies satisfied by the *current* neighbour labels, iterated to a fixpoint (Jacobi/WFC-style). Only room↔room adjacencies can break — adjacencies to `c`/`o` are invariant since those leaves are never relabelled. - **Threshold objective.** The base per-cell value is either continuous fit (`sum(usage_quality*area)`, as 9o5) or — the default — the **count** of `size`/`width`/ `proportion` factors that pass (≥ `FAIL_THRESHOLD`), with continuous fit only as a tiebreak. A satisfied adjacency and a passing factor carry the same unit weight (`_COLLAPSE_FAIL_W`), so the collapse minimises (adjacency + size/width/proportion) fails *jointly*. - **Public-access pin.** The building-level "no outside public access" check is existential (∃ a public street-edge outside leaf with an l/c/k neighbour) — invisible to the per-leaf objective. When the sole provider is an l/k *room* neighbour (no circulation fallback), that leaf is pinned (kept, its demand slot decremented) so the collapse cannot drop the check. **Two corrections found by measurement.** A naive first cut (level-only, per-leaf, continuous fit) went **15→46 fails**. Diagnosis killed two hypotheses: (1) the count explosion was *not* a merge effect (`merge_divided` merges only outside/sahn siblings, never rooms) but the c/o/s partition bug above — pulling `cr1`/`st1`/`st2` into the assignment shredded the circulation skeleton; fixing the partition took +31→+1. (2) The residual +1 was the continuous objective *shuffling* a `size` fail from one leaf to another (pushing one just over the 0.1 threshold and another just under); the threshold objective optimises the fail count directly and removes it. **Keep-better + wiring.** `Fitness.collapse_finish` scores baseline and collapsed on throwaway copies (scoring merges in place) and keeps the collapse only if the fail count does not increase — a strictly monotone safety belt. `driver.collapse_best` applies it to a `SearchResult`'s best, canonically re-scoring and tagging lineage `+collapse`. `evolve.py` runs it after the §15 sharing finish behind `--collapse`/`--no-collapse` (**default ON**). Standalone `homemaker-collapse ` (`collapse_cmd.py`) applies it to an existing layout, writing `.collapsed.dom`. **Verification.** Sweep over 6 harbor-house evolved layouts (total fails, base 195): `adj_off/quality` 192, `adj_on/quality` 185, `adj_off/threshold` 181, `adj_on/threshold` **171**. The default (`adjacency=True, objective="threshold"`, public-access pin) is **monotone across all 6** (never worse than baseline; keep-better guard is a belt, not needed here) — best layout 15→12, and e.g. 32→26, 90→82. The residual on the best layout is geometry-/building-bound, not label slack: the collapse searches **labels only, never geometry**, so it cannot touch long-thin cells or `not-connected` — those are spun out to `homemaker-py-7fm` (shape reshape) and `homemaker-py-qi6` (circulation placement). Running the collapse *inside* search per-eval (rather than finish-time) is `homemaker-py-qpk`, gated on the 9o5 landscape-flattening risk (§13 / `homemaker-py-xi7`) and its own A/B. Tests: `tests/test_collapse_global.py` ×6 (demand-set relabel, level hard constraint, c/o/s exclusion, no-op safety, keep-better/unmerged); 267 pass. ## 18. Graded circulation-connectivity signal (`homemaker-py-qi6`) — DONE (negative) **Motivation — the binary fail is flat.** After the §17 collapse, the residual fails on the harbor-house set are dominated by `level N not connected` (2 of the best layout's 12; also on 5 of the 6 sweep layouts). That fail comes from `connected_circulation` (`graph.py`): remove every non-circulation vertex from a storey's adjacency graph and require the remaining circulation cells (`C` stairs plus the `cr`/`st` room-codes that collide with the c/s prefix) to form ONE connected component. On the evolved layouts they instead fragment into **4–7 components per storey**. **Why finish-time repair fails (measured, negative).** The obvious §17-style companion — a finish-time pass that re-types boundary cells to circulation to bridge the components, kept only if the fail count does not rise — was prototyped (Steiner-MST bridge set per disconnected storey, keep-better guard) and measured on the 6 layouts: **195 → 560 fails (+365)**. The `not connected` fail is *binary* (one fail per storey regardless of fragmentation), but each storey needs 3–7 bridge cells, and every needed-room→circulation conversion triggers a missing-room fail cascade (2–5 fails) that dwarfs the single connectivity fail it clears. Keep-better reverts every one → no-op. **Conclusion: connectivity cannot be bought at finish time when every cell is a needed room; it must come from the outer search allocating connected circulation topology.** But the binary fail gives the search *zero gradient* — a 7-component storey scores identically (both in fail count and in the `0.5^n` scalar) to a 2-component one — so the search cannot tell it is making progress. **Mechanism — a graded proximity on the same channel §11.4 built.** `graph.circulation_connectivity(G)` returns the fraction of circulation cells in the largest connected circulation component ∈ [0,1] (1.0 = a single connected spine, lower = more fragmented, 0.0 = no circulation), measured on the same circ subgraph the fail uses so the two agree at the connected endpoint. Summed over storeys it is the graded proximity scalar `Fitness.score_with_grade` already carries for the outer comparator, gated by the `conn_grade` conf flag: when on it *replaces* the §11.4 leaf quality-proximity on that channel (a distinct, better-motivated use — §11.4 was rejected because within a fail-tier the `0.5^n` scalar is NOT flat there and grade merely displaced a working signal; connectivity is the opposite case, genuinely flat under the binary fail). Like §11.4 it leaves the scalar fitness and fail count **byte-identical** (verified) — it is only the secondary key `(-n_fails, grade, fitness)` (driver `use_lex and use_grade`), strictly beneath fail-count so the §6 missing-space hierarchy and the §5.4 inner-loop cliff are untouched. Among equally-failing neighbours the search now prefers the one whose circulation is closer to one component, restoring the gradient toward connected topologies. **Wiring.** `conn_grade` threads through `_overrides_for`/`_fitness_for`/`_evaluate` and the `search` signature; enabling it implies the grade key. `evolve.py` exposes `--conn-grade` (env `HOMEMAKER_CONN_GRADE`, default OFF); the grade is read off the optimised tree, one extra native eval per child. **Build.** Signal, fitness wiring, CLI, and 9 tests landed (`tests/test_conn_grade.py`: pure-graph fraction contract, non-circ cells ignored, monotone under (dis)connection, and the score/fail- count-invariance of the flag). 276 tests pass. **A/B verdict (measured, 2026-07-22, qpk protocol, `experiments/run_qi6_ab.sh`) — NEGATIVE.** Equal-budget `conn_grade` ON vs OFF, both arms finished with the standard finish-time `--collapse` (94g), 4 workers, canonical `homemaker-fitness` re-score for the `.fails` breakdown: - **harbor-house** (`init.dom`, budget 2500, seeds 1–3): **byte-identical output** in every seed (dom, fail list, fitness all diff-clean ON vs OFF) — the secondary comparator key never fired, i.e. the search trajectory never actually hit a tie at fail-count that the grade could break. This is the programme §18 was motivated on (2 of 15 fails on the best layout are `not connected`), and the signal moved nothing. - **programme-house** (`init.dom`, budget 3000, seeds 1–5): 3/5 seeds tie exactly (byte-identical `.fails`); seeds 1 and 2 diverge to a **different topology** with one fewer total fail (8→7 each) — but the diff is entirely adjacency/crinkliness/width/access/size fails, not connectivity. In all 4 seed-arms across both programmes where a `not connected` fail was actually present (harbor 1&3, programme 3&4), the fail is **unchanged** in both arms — zero cases of the grade clearing one. - **Conclusion: the grade does not do what §18 designed it to do.** It occasionally perturbs tie-breaking among equal-fail-count neighbours (programme-house seeds 1/2), which can incidentally shift the total fail count, but that perturbation never targets circulation connectivity specifically — consistent with a comparator key that is either too weak relative to the primary `(-n_fails, fitness)` keys to steer topology choice, or whose grade values are rarely distinct enough between the actual neighbours the search compares to break a tie in the intended direction. **Status / next.** Kept default OFF (already was). Mechanism (b) (graded proximity as a tertiary key) is falsified by this A/B, not just unconfirmed — do not re-attempt without a different mechanism. The remaining candidate from the original issue is mechanism (a): an explicit insert/relocate-circulation mutation/repair operator, which does not depend on the search stumbling onto a fail-count tie to act. Not started; low priority per DISCOVERED-FROM epic `homemaker-py-94g`'s framing (fitness fidelity, not search capability). ## 19. Geometry/topology repair for shape-intrinsic fails (`homemaker-py-7fm`) — DONE (negative) **Motivation.** §17 established that ~12 of the harbor-house best layout's 15 residual fails survive the label-only collapse — long-thin cells (`width`/`proportion`/`crinkliness`) whose geometry, not room assignment, is wrong. `bd memory collapse-global-94g-and-any-label-usage- optimisation` spun this out as its own problem: a mechanism that moves *geometry*, evaluated for net fail-count effect on the same 6-layout sweep §17 used. **Diagnosis (rules out mechanism (a)).** Re-ran the full-fitness ratio inner loop (`innerloop.optimise`, Nelder-Mead, 1500 evals, warm-started from the evolved ratios — far above the ~80-200/child budget search actually spends) on the 12-fail collapsed best layout: **zero change**, byte-identical fail lines. These are not local optima of the ratio search reachable with more budget. Tracing two representative fails back through the tree found two distinct structural causes, neither fixable by re-solving ratios on the existing cuts: (1) **area starvation** — a leaf's *defining branch* (several levels up) was allocated too little total area for what it has to share with its siblings (a storage leaf wanting 18m² sat in a 6.4m² branch whose sibling got 52.8m² of outside space); (2) **orientation mismatch** — a leaf is the correctly-area-sized-but-thin remainder of a cut whose *rotation* runs parallel to its parent rectangle's long axis, so no ratio value on that axis avoids a sliver. **Mechanism (`operators.mutate_shape_rotate`, `operators.mutate_deslim`).** Two targeted repair operators addressing each cause, in the `mutate_level_fix` style (structural, not blind-random): `_shape_failing(leaf, fit)` identifies a named-room leaf whose width or proportion factor actually fails (`< FAIL_THRESHOLD` under `Fitness.quality_width`/`quality_proportion` — not a geometric proxy, which over-flags leaves the Gaussian tail still passes). `mutate_shape_rotate` re-orients the live cut that produced a failing leaf (targets cause 2); `mutate_deslim` merges a failing leaf into its sibling, undoing the division that starved it (targets cause 1), leaving the displaced room for `mutate_place_missing` (already in `MUTATIONS`) to re-insert elsewhere. Both are registered in `MUTATIONS`/`mutate()`, gated on a `fit` argument (a new `fit_ops` class alongside the existing `reqs_ops`) so they no-op — and are excluded from the outer search's `weights` — wherever a `Fitness` instance isn't threaded through, exactly as `place_missing` etc. gate on `reqs`. `driver.search`/`evolve.py` do **not** yet pass `fit` through (see Status below), so the operators exist but are currently unreachable from the GA — they were evaluated instead as a finish-time greedy hill-climb (below). **Verification (measured, negative).** A finish-time hill-climb applied both operators exhaustively — for every live cut driving a shape fail, all 3 alternate rotations were tried (not just `mutate_shape_rotate`'s single random draw) alongside a `deslim` + `place_missing` + ratio-resolve, keeping the best only if it did not increase the fail count — on the same 6 harbor-house evolved layouts as §17 (total fails 187): **0 improving moves found on any layout, on any candidate cut, under any of the 4 tried variants.** Manually inspecting the rejected candidates for the representative case (harbor-house evolved-3M-nols-3, leaf `0/rlrlr` "la1", the proportion fail traced above) shows why: every one of the 3 rotations and the deslim+ reinsert produced a **worse** layout — new `no outside public access`, `not adjacent to c`, `access`, or `edge too long` fails, in every trial. This is §4.2's core lesson (proxy/partial- objective repair of a co-evolved local optimum "is structurally unable to win" — every cut position is *simultaneously* a size/shape knob **and** an adjacency/access/circulation knob) now confirmed for structural topology repair, not just ratio-solving: on a tightly co-evolved layout, the cut that makes a leaf thin is *also* the cut providing some other leaf's public- access or adjacency, so straightening it elsewhere is not free. The residual geometry-intrinsic fails on the harbor-house best layout appear to be close to a genuine Pareto floor for this topology, not a repairable inefficiency — consistent with §17's own framing ("geometry-/ building-bound"). **Status / next.** `mutate_shape_rotate`/`mutate_deslim` land in `operators.py`, default-excluded from `mutate()` (no `fit` threaded through the outer search yet), with dedicated tests (`tests/test_operators.py`: fail detection, noop-without-`fit`, targeted-cut selection, merge + `place_missing` repairability) plus automatic coverage via the existing `test_mutations_yield_canonical_genomes` parametrisation. 282 tests pass. The finish-time hill-climb script is **not** productionised (unlike §17's `collapse_cmd.py`) because it never found an improving move to apply — there is nothing to wire up. Not tested: whether these operators help as *in-search* GA moves (mechanism (c)) — a full multi-generation run gives selection pressure and population diversity a chance to accept a locally-worse move that a later step or recombination completes, a fundamentally different regime from single-step greedy hill-climbing on an already-finished layout. That A/B (thread `fit` through `driver.search`, gate with an `enable_shape_repair`-style flag as §12.3 did for `reassociate`, run full-budget with/without) is the remaining open question and would need to be its own measured experiment before further code changes — this session's finding is that the *finish-time* half of the issue's candidate mechanisms is a dead end, not that geometry repair is impossible in general. **In-search follow-up (measured, 2026-07-22, `homemaker-py-161`) — also negative.** `driver.search`/`search_staged` gained `enable_shape_repair: bool = False`, threading a cached `Fitness` instance into `operators.mutate()` only when set (mirrors `enable_reassociate`'s clean- toggle pattern; default off reproduces prior runs byte-for-byte). Full A/B on harbor-house `init.dom` cold-start, budget=1,000,000, pop=16, child_budget=80, workers=4, seeds 0–3: fails `[14,15,12,17]` mean 14.50 (off) vs `[17,14,16,12]` mean 14.75 (on) — no improvement, and the 0.25 delta is far inside the 12–17 seed-to-seed spread in both arms. A smaller pilot (budget=20000, 3 seeds) matched: off mean 31.33, on mean 32.00. In-search selection pressure and population diversity do **not** rescue `shape_rotate`/`deslim` on harbor-house-scale programmes either — the residual fails look like a genuine floor for this representation on this programme, not an inefficiency reachable by richer local operators, in either regime. Code kept (not reverted) for reuse/reproducibility per the `enable_reassociate` precedent; test `test_enable_shape_repair_threads_fit_into_mutate` in `tests/test_driver.py`. Both halves of §19's candidate mechanism space (finish-time and in-search) are now closed negative. ## 20. In-search global collapse (`homemaker-py-qpk`) — DONE (positive, size-dependent) **Motivation.** §17 (`94g`) landed the FINISH-TIME global cell↔room collapse — a one-shot label search over the already-searched geometry, applied once to the best layout at the end (harbor-house best 15→12). The original 94g thrust was the PER-EVAL version: run the same collapse inside every fitness eval during search, so the outer GA optimises the collapsed (relabelled) objective directly instead of discovering it only at the end. Deferred behind its own A/B because 9o5 (§13/`xi7`) found the analogous per-class collapse-as-relaxation NULL/NEGATIVE (OFF beat ON on both example programmes) — the risk carried forward here, AMPLIFIED to global scope, is that `max`-over-labellings flattens the fitness landscape (many topologies collapse to similar scores) and removes the gradient the outer search climbs. **Mechanism (build).** `Fitness.collapse_global` (§17) is called inside `_evaluate_full`, gated by a new `collapse_insearch` conf flag (default OFF, bit-identical when off — same contract as `superpose`/ `conn_grade`), at the same point `collapse_superposition` (9o5) already runs: before any Phase-1 check, on the unmerged tree, so `check_space_counts`/adjacency/quality downstream see the collapsed labels. Two knobs, both conf-driven: `collapse_insearch_adjacency` (default True — the fixpoint Jacobi relaxation §17 describes) and `collapse_insearch_iters` (default 3, vs finish-time's 6 — a per-eval cost, not a one-shot polish; lower until profiling says otherwise). `preserve_public_access` is always on (never safe to drop silently mid-search). Plumbed through the same minimal path as `conn_grade` (`driver._overrides_for`/`_fitness_for`/`_evaluate`/`search`, `evolve.py --collapse-insearch` / `HOMEMAKER_COLLAPSE_INSEARCH`) — not threaded into `search_staged`/ `search_annealed`/`polish_finish`, matching `conn_grade`'s existing footprint. **Verified (build-time).** On `evolved-3M-nols-3.dom` (harbor-house, the §17 15→12 fixture), `collapse_insearch` reaches the byte-identical 12-fail collapsed state as the finish-time pass — expected, since it is the same `collapse_global` call moved earlier in the same pipeline on a fixed geometry. Flag off reproduces baseline score/fails exactly. `tests/test_collapse_insearch.py` (8): defaults, conf knobs, `_evaluate_full` wiring (mocked call-site assertion: fires with the right kwargs when on, never when off), and the end-to-end 15→12 cross-check. 290 tests pass. A 60-eval CLI smoke run (`--collapse-insearch`, programme-house) confirms the plumbing only, no crash — not a result (mirrors qi6's smoke-only checkpoint). **Cost (measured, `evolved-3M-nols-3.dom`, 20-eval average).** Baseline eval 106 ms; with `collapse_insearch` + adjacency 205 ms (**1.9×**); adjacency off 157 ms (1.5×). Per-eval cost is therefore real but not prohibitive at this building size — no incremental/cached variant was needed to make the experiment affordable, contrary to the issue's worst-case worry. A full-budget run will cost roughly 2× the wall-clock of an equal-budget baseline run. **A/B verdict (measured, 2026-07-19, xi7 protocol) — POSITIVE, and the OPPOSITE of the 9o5/xi7 prior.** Equal-budget `collapse_insearch` ON vs OFF, both arms finished with the standard finish-time `--collapse` (94g) so the comparison is apples-to-apples on the final COLLAPSED score, 4 workers: - **harbor-house** (`init.dom`, budget 2500, seeds 1–3): **ON wins 3/3**, mean fails 80.3 → 72.0 (s1 85→74, s2 76→65, s3 80→77) — a consistent ~10% fail reduction, no losses. - **programme-house** (`init.dom`, budget 3000, seeds 1–5): ON wins 3/5, mean fails 8.4 → 7.8 (s1 8→5, s2 8→7, s4 10→9 win; s3 8→9, s5 8→9 loss by one fail) — a weaker, noisier signal on this much smaller building, already closer to its geometry floor (§13/§19). - **Combined head-to-head: ON 6, OFF 2.** Unlike 9o5 (a per-CLASS relaxation over interchangeable-but-not-identical codes, where `max`-over- labellings blurred which topology was actually good), the global WFC-style matching here is the *same* mechanism §17 already proved monotone/positive at finish time — running it every eval just lets the outer search see the condensed objective instead of discovering it only once, and evidently that gradient is real, not flattening, at least at the scale tested. The effect scales WITH building size (more leaves → more relabelling headroom per eval), the opposite of what the 9o5 fear predicted. **Cost (wall-clock, matches the profiled 1.5–1.9× per-eval figure above).** harbor-house mean 102.6s (OFF) → 177.8s (ON), ~1.73×. programme-house mean 39.0s (OFF) → 43.7s (ON), ~1.12× (smaller building → collapse is a smaller fraction of total eval cost). **Status (2026-07-19).** Kept **default OFF** — the programme-house result is too mixed (2 losses in 5 seeds) to flip the default on a small sample, and 9o5/xi7 is a fresh enough scar to want a second, larger-budget confirmation before doing so. But this is a genuine, working, opt-in improvement for larger buildings: `--collapse-insearch` is documented and ready to use on harbor-house-scale (or bigger) programmes today. A natural follow-up (not filed, low priority) would be a larger-N seed sweep on programme-house alone to see whether the mixed result is just small-sample noise around a true small positive, or a genuine size threshold below which in-search collapse doesn't pay for its ~1.1–1.9× cost. **Larger-N confirmation (`homemaker-py-1ph`, 2026-07-24) — DEFAULT FLIPPED TO ON.** Re-ran the programme-house arm alone at 4× the sample: same protocol (`init.dom`, budget=3000, 4 workers, both arms finished with the standard finish-time `--collapse`), 20 fresh seeds (1–20) instead of 5, on the current codebase (post-qpk commits through `161`, none of which touch the default-off code path): - Mean fails: **7.95 (OFF) → 7.10 (ON)**, a ~10.7% reduction — consistent in direction and magnitude with the original 5-seed sample (8.4 → 7.8) and with harbor-house. - Head-to-head (excluding 3 ties): **11 wins / 6 losses** for ON (was 3/2 at N=5). - Paired t-test on the 20 per-seed diffs: mean diff 0.85 fails, t=2.38, df=19, two-tailed **p ≈ 0.028** — the mixed 3/5 result was small-sample noise around a true small positive, not a genuine programme-house-scale exception. - Cost: ON still ~1.2–1.3× OFF wall-clock at this size (20.9s mean OFF → 26.4s mean ON), same order as the original measurement. Confirms the qpk verdict holds at both example scales tested. `collapse_insearch` default flipped **OFF → ON** in `evolve.py` (`--collapse-insearch`/`--no-collapse-insearch`, `HOMEMAKER_COLLAPSE_INSEARCH`) and `driver.py` (`_overrides_for`, `_fitness_for`, `_evaluate`, `search`, `polish_finish`) — opt out per-run with `--no-collapse-insearch` if a specific programme needs the cheaper finish-time-only path. `fitness.Fitness` itself is unchanged (still defaults off when `collapse_insearch` is absent from conf — the default lives in the driver/CLI override layer, same contract as `leaf_sharing`). **Caveat added retroactively (`homemaker-py-iio`, 2026-08-02).** A stale-leaf-share bug (§35) meant every `collapse_insearch=ON` eval during this era's runs (and any leaf-sharing run's finish-time `--collapse`) could occasionally value one candidate cell of the collapse assignment using leftover `share`/`share_type` metadata from a code the leaf no longer held. §35's re-verification shows this is real per-seed noise (not a directional bias) that does not appear to overturn the ON-beats-OFF verdict above, but the exact historical per-seed numbers quoted in this section were not re-measured under the fix. See §35 for the mechanism and what was (and wasn't) re-confirmed. ## 21. Insert/relocate-circulation repair operator (`homemaker-py-8sh`) — DONE (mixed, kept off) **Motivation.** qi6's remaining candidate (§18): mechanism (a), an explicit search-time mutation/repair operator that inserts or relocates a circulation cell to bridge a disconnected circulation component directly, rather than relying on the outer GA to discover connectivity via a comparator-key gradient (mechanism (b)/(c), measured NEGATIVE — the grade never fired on harbor-house and never cleared a genuine `not connected` fail on programme-house). **Mechanism (build).** `operators.mutate_bridge_circulation`: for each storey, builds the leaf adjacency graph (`geometry.leaf_graph`) and the circulation sub-components (`dom.is_circulation` nodes only, mirroring `graph.connected_circulation`'s subgraph). When a storey has more than one component, finds the cheapest path between any pair via a weighted Dijkstra search — edge weight is the average of its endpoints' conversion cost (`0` for an already-circulation node or a generic outside `O` leaf — nothing displaced, same rationale as `place_missing`'s host ranking; `1` for any other non-required leaf; `5` for a leaf typed as a required programme room, crossed only if no cheaper route exists) — and retypes every intermediate leaf on the cheapest cross-component path to `C`. A displaced required room becomes a missing-space fail for the existing `place_missing` operator to re-insert elsewhere on a later step, the same division of labour `mutate_deslim` (§19) uses. Registered in `operators.MUTATIONS` as a "`reqs`-optional" op — unlike `level_fix`/ `place_missing` it is never zero-weighted for lacking `reqs` (it needs only the tree's own adjacency graph), so gating is done the `reassociate` way instead: `driver.search`'s new `enable_bridge_circulation` flag (default OFF) zeroes its `mutation_weights` entry rather than relying on an argument being `None`. Threaded through `search_staged` and exposed as `evolve.py --bridge-circulation` / `HOMEMAKER_BRIDGE_CIRCULATION`. 6 unit tests (`tests/test_operators.py`): noop when already connected, bridges a synthetic 3-leaf fragmented fixture via the free leaf, falls back to bridging through a required room when it is the only route, and prefers a free `O` leaf over a required room when both routes tie in hop length. 296 tests pass. **A/B verdict (measured, 2026-07-24, qi6/qpk protocol, `experiments/run_8sh_ab.sh`).** Equal-budget `enable_bridge_circulation` ON vs OFF, both arms finished with the standard finish-time `--collapse` (94g), 4 workers, canonical `homemaker-fitness` re-score for the `.fails` breakdown — harbor-house (`init.dom`, budget 2500, seeds 1–3), programme-house (`init.dom`, budget 3000, seeds 1–5): | programme | seed | fails OFF→ON | not-connected OFF→ON | |---|---|---|---| | harbor-house | 1 | 74→67 | 0→**2** | | harbor-house | 2 | 65→65 (byte-identical) | 1→1 | | harbor-house | 3 | 77→77 (byte-identical) | 1→1 | | programme-house | 1 | 5→5 (tie, fitness differs) | 0→0 | | programme-house | 2 | 7→7 (tie, fitness differs) | 0→0 | | programme-house | 3 | 9→7 | 1→1 | | programme-house | 4 | 9→8 | 1→**0** | | programme-house | 5 | 9→7 | 1→**0** | Total fails: harbor-house mean 72.0→69.7, programme-house mean 7.8→6.8 — **never worse** on any seed (4 wins, 4 ties, 0 losses on total fail count across both programmes). Of the 5 seed-arms whose OFF baseline actually had a `not connected` fail, **2/5 cleared it** (programme-house seeds 4 and 5) — a genuine improvement over qi6 mechanism (b)'s 0/4. But harbor-house seed 1 shows the flip side: its OFF baseline had *no* `not connected` fail (0), and ON introduces **two** — while simultaneously landing the sweep's single largest fail-count win (74→67, fitness 3.2e-26→5.0e-24, almost two orders of magnitude apart) via a visibly different topology, not a locally-adjusted one. `mutate_bridge_circulation` only ever converts a leaf *to* circulation, never away from it, so it cannot mechanically increase fragmentation itself — the regression is trajectory-divergence noise (adding any nonzero-weight entry to `operators.mutate`'s weighted draw perturbs the RNG mapping for *every* subsequent draw, not just the ones that select the new operator, exactly as observed for `enable_reassociate`/`enable_shape_repair`/homemaker-py-161 — the same-seed off/on comparison is two genuinely different searches from the same seed, not a controlled single-variable diff). Two of the three harbor-house seeds never diverged at all (byte-identical fitness to 6 significant figures) — at `_MUTATION_WEIGHTS`' default uniform weighting the operator is drawn roughly 1-in-17 times a mutation fires, and evidently often never lands on a fragmented storey within a 2500-budget run. **Status.** Directionally positive and clearly better-targeted than qi6's graded signal (which cleared zero `not connected` fails in its own measured protocol), but the N=3/N=5 sample is too small and too trajectory-noisy to separate a true small positive from chance, per the same caution `collapse_insearch` was held to before its `homemaker-py-1ph` larger-N confirmation. Kept **default OFF** (`enable_bridge_circulation=False` in `driver.search`/`search_staged`, `--no-bridge-circulation` in `evolve.py`). Candidate follow-ups, not yet filed: (a) a larger-N seed sweep (the `1ph` protocol) to resolve whether the mean improvement is real; (b) raising `bridge_circulation`'s `_MUTATION_WEIGHTS` entry above the uniform default (mirroring `place_missing`'s `2.0`) so it fires more often per budget, since a `not connected` fail is exactly as fatal to fitness as a missing space and the operator is currently drawn no more eagerly than cosmetic ops like `rotate`. See §22 for the larger-N confirmation of both follow-ups — **result: null, weight change reverted, default stays OFF.** ## 22. bridge_circulation larger-N + weight confirmation (`homemaker-py-lj3`/`homemaker-py-qjg`) — DONE (null) **Motivation.** §21's two identified follow-ups — (a) a `1ph`-style larger-N seed sweep to resolve whether 8sh's small positive mean-fail improvement was real or small-sample noise, and (b) raising `bridge_circulation`'s `_MUTATION_WEIGHTS` entry to `2.0` (matching `place_missing`) so it fires more often — were filed as separate beads (`lj3` for the weight, `qjg` for the sample size) but `lj3`'s own description flagged them as confounded if tested separately: weight and sample-size are different variables, and a real effect from raising the weight could get masked or amplified by the same small-N noise that made §21 inconclusive in the first place. Tested together in one sweep instead of two. **Protocol.** `driver._MUTATION_WEIGHTS["bridge_circulation"] = 2.0` (matching `place_missing`, still zeroed via `mutation_weights` unless `enable_bridge_circulation` is set — no behaviour change for the default-off path). Same qpk/1ph protocol as §20/§21: equal-budget ON vs OFF, both arms finished with the standard finish-time `--collapse` (94g), 4 workers, canonical `homemaker-fitness` re-score for the `.fails` breakdown, `experiments/run_lj3_qjg_ab.sh`. Matching 1ph's own 4× scale-up: programme-house (`init.dom`, budget 3000) 20 seeds (1–20, vs §21's 5), harbor-house (`init.dom`, budget 2500) 12 seeds (1–12, vs §21's 3). **A/B verdict (measured, 2026-07-25) — NULL, opposite of §21's directional signal.** - **programme-house (N=20):** mean fails 7.10 (OFF) → 6.95 (ON), mean per-seed diff 0.15 fails. 6 wins / 5 losses / 9 ties for ON. Paired t-test on the 20 diffs: t=0.38, df=19, two-tailed **p≈0.71** — indistinguishable from zero. - **harbor-house (N=12):** mean fails 71.8 (OFF) → 72.3 (ON), mean per-seed diff **−0.5** fails (ON slightly worse on average). 4 wins / 4 losses / 4 ties. Paired t-test: t=−0.41, df=11, two-tailed **p≈0.69** — also indistinguishable from zero. - **Connectivity-specific effect, and the concerning part:** of the 10 programme-house seeds whose OFF baseline had a genuine `not connected` fail, 4 cleared it on ON (seeds 3, 4, 5, 6) — but 3 *new* `not connected` fails appeared on seeds whose OFF baseline had none (seeds 1, 9, 13), a higher new-fail rate than §21's original uniform-weight sweep saw (0/5 programme-house seeds introduced a new not-connected fail at N=5; here 3/20 = 15% did at the raised weight). harbor-house cleared 1/10 and introduced 0 new, but its total-fail mean still went the wrong way. `mutate_bridge_circulation` still only ever converts a leaf *to* circulation, never away — the new fails are §21's trajectory-divergence mechanism (a nonzero-weight operator entry perturbs the RNG draw sequence for every subsequent mutation, not just its own draws), and raising the weight increases how often that perturbation-inducing draw happens, which plausibly explains why the new-fail rate went up rather than down. - **Cost:** essentially unchanged from §21 — programme-house 28.1s (OFF) → 28.1s (ON, 1.00×), harbor-house 130.0s (OFF) → 132.6s (ON, 1.02×). **Interpretation.** §21's 4-win/4-tie/0-loss, 2/5-not-connected-cleared result at N=3/N=5 was small- sample noise around a true near-zero effect, not a genuine small positive — the same question 1ph asked of `collapse_insearch` (§20), but here the larger-N answer goes the other way: not confirmed. Raising the mutation weight did not help and, if anything, correlates with a worse trajectory-noise profile (more new not-connected fails per seed) than leaving it at the uniform default, consistent with the weight bump increasing how often the RNG-perturbing draw fires. **Status.** `_MUTATION_WEIGHTS["bridge_circulation"] = 2.0` **reverted** — back to the implicit uniform weight (not present in `_MUTATION_WEIGHTS`), matching pre-`lj3` behaviour exactly. `enable_bridge_circulation` stays **default OFF**. No further weight/sample-size follow-up planned; `operators.mutate_bridge_circulation` remains available opt-in (`--bridge-circulation`/`HOMEMAKER_BRIDGE_CIRCULATION`) for anyone who wants the connectivity- targeting behaviour despite the neutral aggregate measurement, but is not a candidate for a default flip on the current evidence. ## 23. Ruin-and-recreate LNS: rebuild a wing with the adjacency-aware constructor (`homemaker-py-f1d`) — DONE (positive, size-dependent) **Motivation.** DESIGN.md's own experiment log by this point is one-sided: every "search machinery" change tried (§11.5 niching+restarts, §11.4 graded objective, §12.3 Wong-Liu reassociation + shape-feasibility, §12.4 granularity, §14 island model, §16 grain annealing, §18 graded connectivity, §19 shape repair, §21/§22 circulation-repair ops) has come back null-to-negative, while construction/seeding QUALITY (§11.6/§11.7 adjacency-aware seeding, §12.2 proportion-aware seeding) is the only lever that has ever moved the fail count. `operators._assign_adjacency_aware` — the constructor behind both `constructive_topology` and `lift_base_to_storeys` — currently only ever runs once, at seeding. The proposal: reuse it repeatedly DURING search as a large-neighbourhood- search (LNS) ruin-and-recreate move, betting that the one technique with a real track record generalises better than another new comparator-key or population-management idea. **Mechanism (build).** `operators.mutate_ruin_recreate`: pick a divided, live-cut subtree ("wing") of one storey holding a genuine partial neighbourhood of that storey's leaves (>=2, <= half — not a single-leaf relabel already covered by `retype`/`swap`, not a whole-floor rebuild already covered by the initial seed), un-divide it back to one leaf, then regrow and retype it with `_assign_adjacency_aware`, seeded (`fixed_circ`) from whichever already-typed circulation leaves border the wing — the same mechanism `lift_base_to_storeys` uses to grow an upper storey off an inherited core (§11.7), so the rebuilt interior spine reconnects to the surviving one instead of growing a disconnected island. The wing's required-space room-code budget is preserved exactly (same multiset); only its internal circulation/outside counts and split are rebuilt, at the same circ_divisor=3/outside_divisor=3 ratio the constructive seeders default to (not threaded from the run config — kept parameter-light, like `bridge_circulation`). `_assign_adjacency_aware` gained a new `scope` parameter (leaves eligible for retyping; `fixed_circ` may then name border leaves OUTSIDE `scope` as dominating-set seeds only, never retyped) so the wing rebuild can share the exact constructor code without touching the rest of the storey. `scope=None` (every existing caller) reproduces the prior unrestricted behaviour exactly — verified no other caller's output changed. Gated like `reassociate`/`bridge_circulation`: zero mutation weight unless `enable_ruin_recreate=True` (`driver.search`/`search_staged`, `evolve.py --ruin-recreate`/`HOMEMAKER_RUIN_RECREATE`, default off). **Verified (build-time).** 200 applications of `mutate_ruin_recreate` chained onto fresh `constructive_topology` harbor-house seeds (40 seeds × 5 steps): zero missing-space regressions (`graph.check_space_counts`), every child a canonical genome (`encode(decode(encode(x))) == encode(x)`). 297 existing tests pass unchanged (the new op is exercised by the existing `test_mutations_yield_canonical_genomes` parametrization, which calls it with `reqs=None` and gets the documented noop). A `child_probe`-instrumented `driver.search` run confirmed the operator is actually selected by `mutate()` at its configured weight (not dead code). **Initial A/B (measured, 2026-07-25/26, qpk protocol) — NULL, but underpowered.** Equal-budget `enable_ruin_recreate` ON (implicit uniform mutation weight, ~7.5% draw probability among ~13 active ops) vs OFF, both arms finished with the standard finish-time `--collapse` (94g), 4 workers: - **harbor-house** (budget 2500, seeds 1–3): 1 loss (74→81), 2 ties. - **programme-house** (budget 3000, seeds 1–5): 4 ties, 1 win (9→8). - **Combined: 1 win / 1 loss / 6 ties out of 8**, mean fails 31.9 (OFF) → 32.6 (ON) — indistinguishable from zero, in the same direction as most of this log's other null results. - A direct `child_probe` instrumentation of one of the tied harbor-house runs found `ruin_recreate` fired **once in 32 children** — the initial sample is dominated by trajectories where the operator simply never got a turn, not by turns it lost. Six of the eight exact ties (fitness scalar identical to 6 significant figures, not just fail count) are consistent with this: the op's rare draws mostly didn't survive tournament selection into the recorded lineage. **Weight follow-up (measured, 2026-07-26) — reran the ON arm only** with `_MUTATION_WEIGHTS["ruin_recreate"] = 3.0` (matching `place_missing`, mirroring the `lj3` weight-bump precedent) at the same seeds/budgets, directly comparable to the existing OFF baseline: - **programme-house** (seeds 1–5): **4 wins, 1 tie, 0 losses** — 7→1, 9→7, 9→8, 9→7, 5→5. A striking, one-sided result, including one seed dropping from 7 fails to 1 (verified deterministic on rerun). - **harbor-house** (seeds 1–3): 1 win (77→73), 1 loss (74→82), 1 tie — still mixed. **Larger-N confirmation (measured, 2026-07-26)** — extended both arms to 10 fresh programme-house seeds (6–15) and 5 fresh harbor-house seeds (4–8) at the same weight=3.0, same protocol: - **programme-house, all 15 seeds combined: 8 wins / 1 loss / 6 ties.** Mean fails **7.07 (OFF) → 6.00 (ON)**, a ~15% reduction. Wilcoxon signed-rank p≈0.041; sign-test p≈0.020 (one-sided) — holds up at conventional significance, not small-sample noise around zero (the 8sh/1ph/qi6/lj3 pattern this log warns about). - **harbor-house, all 8 seeds combined: 3 wins / 2 losses / 3 ties.** Mean fails **73.0 (OFF) → 74.5 (ON)** — no consistent effect, if anything a very slight negative lean, echoing §20's (`collapse_insearch`) opposite-direction size split but with the SMALLER building this time as the one that benefits. **Interpretation.** A rare case in this log where a search-machinery idea shows a real, statistically-supported effect — but only on the smaller/simpler example programme. Plausible reading: programme-house's smaller room count means a wing rebuild samples a much larger fraction of the whole floor's topology per move (higher effective locality-vs-scope ratio), so the constructor's proven adjacency-aware placement quality dominates; harbor-house's much larger room count means the same wing size is a small, noisier perturbation relative to the whole building, and correlates with the ~2× per-op cost of `_assign_adjacency_aware` (leaf-graph rebuild + dominating-set search) not translating into more useful search steps within the same eval budget on that scale. **Status (2026-07-26).** `enable_ruin_recreate` stays **default OFF** — harbor-house shows no benefit and the two example programmes disagree on direction, so flipping the global default is not supported by this evidence (same conservative bar §20 applied before its own larger-N confirmation). `_MUTATION_WEIGHTS["ruin_recreate"] = 3.0` is kept in the source (only takes effect when the flag is on) since it is the validated-effective setting. `--ruin-recreate`/`HOMEMAKER_RUIN_RECREATE` is documented and ready to use today on programme-house-scale (smaller/simpler) programmes; a natural follow-up (not filed, low priority) would be a third or fourth example programme at a size between the two tested here, to locate the size threshold this result implies rather than inferring it from just two data points. ## 24. Ruin-and-recreate size-threshold sweep (`homemaker-py-y51`) — INCONCLUSIVE, no clean threshold **Motivation.** §23's follow-up: locate the room-count threshold where `ruin_recreate` (weight=3.0) stops helping, rather than inferring it from programme-house (6 rooms, win) vs harbor-house (37 room instances, null/slight-negative) alone. **No natural third example exists.** `programme-house2` is the same 6-room size as programme-house (a geometry-fix variant, not a size variant); `maple-court` (26 space *types*, many with `count`, ~more room instances than harbor-house) is bigger than harbor-house, not between the two. So this used option (b) from §23: a synthetic room-count sweep on programme-house's own `patterns.config`, scaling the `b1`/`t1`/`b2`/`t2` bedroom+ensuite module count by an integer factor (k=2..5 → 10/14/18/22 room instances), holding room-type mix, storey limits, ratios and adjacency constant, with the footprint (`init.dom`) scaled in area to match (`examples/y51-sweep-{10,14,18,22}`). budget=3000 calibrated so every size leaves nontrivial residual fails at seed 1 (14/29/27/43), not saturated to 0. **Measured (2026-07-26, `experiments/run_y51_sweep.sh` + `run_y51_sweep_ln.sh`)** — paired seeds, `--ruin-recreate` (weight=3.0) ON vs OFF, both arms finish with the default `--collapse` (94g), 4 workers. Initial pass: 5 seeds at every size. Larger-N confirmation: 5 more seeds (N=10 total) at the two sizes whose initial 5-seed read was most striking (n=14, the only size that initially *lost*; n=18, the strongest initial win) — mirroring this log's own larger-N-confirmation pattern. | n_rooms | N | W/L/T | mean fails OFF→ON | Δ% | Wilcoxon p | |---------|----|---------|--------------------|-------|-----------| | 10 | 5 | 3W/1L/1T | 17.80 → 16.40 | +7.9% | 0.625 | | 14 | 10 | 3W/5L/2T | 28.90 → 28.70 | +0.7% | 0.945 | | 18 | 10 | 7W/2L/1T | 36.50 → 33.10 | +9.3% | 0.098 | | 22 | 5 | 2W/3L/0T | 41.20 → 40.80 | +1.0% | 0.875 | **Interpretation.** This does **not** reproduce a clean monotonic decay of the effect as room count rises from programme-house's 6 to harbor-house's 37. n=14 came back a clean null after larger-N confirmation (the initial 5-seed 0W/4L read did not hold — noise, exactly the pattern this log repeatedly warns about). n=18 shows the strongest trend of the four synthetic sizes (a plausible-but- not-quite-significant ~9% mean improvement, p≈0.10) despite sitting *between* two much weaker/null sizes (14 and 22) — a non-monotonic bounce inconsistent with a simple "smaller wing-rebuild-to-floor ratio → bigger effect" threshold as a function of room count alone. Two readings, not mutually exclusive: 1. **Still underpowered.** §23's own programme-house confirmation needed N=15 seeds to reach p=0.041 for a similar-magnitude effect (~15% reduction); N=5/N=10 here is likely too little to resolve an effect this size cleanly at any of these sizes, so the bounce may just be sampling noise on top of a real but weak trend across 10-22 rooms. 2. **Methodological caveat: this sweep is not a clean proxy for "room count."** It scales room count by *duplicating already-anonymous, already-interchangeable* room codes (`count:` on b1/t1/b2/t2) — the same mechanism harbor-house itself uses "to reduce complexity" (its own patterns.config comment). Duplicating interchangeable codes may make placement systematically *easier* for `_assign_adjacency_aware` than harbor-house's mix of many genuinely distinct room types at the same instance count would be, so this sweep's room-count axis may not isolate the same "topology fraction sampled per wing move" variable that §23 hypothesised drives the effect. **Status (2026-07-26).** `enable_ruin_recreate` stays default OFF; no per-size default flip is supported by this evidence — the sweep did not locate a clean threshold. §23's practical guidance (safe to opt in on programme-house-scale, ~6-room programmes; not validated at harbor-house scale) stands unchanged. A real follow-up, if pursued, needs either (a) much larger N (~15+ seeds) at a smaller set of sizes to resolve whether the n=18 trend is real, or (b) a genuinely distinct third example programme (real room-type diversity at an intermediate room count, not a duplicated-code sweep on programme-house) to avoid the interchangeable-room confound above. ## 25. 2-opt local search past the collapse_global Jacobi plateau (`homemaker-py-9wi`) — DONE (positive, opt-in) **Motivation.** §17's `collapse_global` adjacency relaxation is a Jacobi/WFC-style loop: each round re-solves a *linear* assignment (`_best_assignment`) using an adjacency bonus computed from the *previous* round's neighbour labels. That is exact per round, but the true objective is quadratic — a satisfied adjacency depends on a **pair** of labels, not one — so synchronous Jacobi can plateau short of the joint optimum. Worked example (`test_two_opt_polish_escapes_jacobi_plateau`): a 4-cell chain `p1─q1─p2─q2` with two disjoint adjacency requirements (`p1<->p2`, `q1<->q2`) has a fully-satisfying relabelling (`p1─p2─q1─q2` or similar), but starting from the interleaved layout the Jacobi loop **2-cycles** between two labellings that each satisfy **zero** of the four requirements, and never escapes within `iters`. **Mechanism (`Fitness._two_opt_adjacency_polish`).** Runs once, after the Jacobi loop reaches its fixpoint (or exhausts `iters`). For every **same-level** pair of supply leaves, try swapping their current labels; keep the swap only if it **strictly** increases the total reward (own quality/threshold value + `fail_w` per satisfied adjacency) summed over the two leaves and every leaf adjacent to either — the only cells a label swap between `i` and `j` can change. Repeats to a fixpoint (or `local_search_passes`, default 20). Same-level-only pairing keeps the hard level constraint for free (both codes already matched their own leaf's level pre-swap, and the two leaves share a level, so the swap is valid on both sides). A swap is applied only on strict improvement, so this is **monotone by construction** — it can only reduce, never increase, the objective's implied fail count, same guarantee as the Hungarian solve it refines. `Fitness._collapse_value` factors the shared (leaf, code) → base-value computation out of the `collapse_global` assignment-matrix build so both the matrix and the polish score a pair identically. **Why 2-opt over CP-SAT/OR-Tools.** The issue proposed either a 2-opt local search or a CP-SAT (OR-Tools) encoding of the labelling QAP. Went with 2-opt: no new dependency (the project has no `ortools`), and it extends the existing Jacobi machinery directly rather than replacing it with a separate solver. QAP is NP-hard in general, so this is a local search, not an exact solve — but it strictly dominates the Jacobi-only result by construction, and `collapse_finish`'s keep-better wrapper is an additional safety net regardless. **Wiring.** `collapse_global(local_search=False, local_search_passes=20)` — the method-level default stays off (see §28: it's also called every fitness eval via `collapse_insearch`/`qpk`, a hot path this polish was never measured against). `homemaker-collapse --local-search`/`--no-local-search` and `evolve.py`'s `--collapse` now default it **on** at the one-shot finish-time call sites — see §28 (`homemaker-py-cdl`) for the broader sweep and wiring that flipped those defaults. **Verification.** Swept all 11 harbor-house `evolved-*`/`3m`/`materialised-3M` `.dom` files, comparing `collapse_global(local_search=False)` against `local_search=True`: 10/11 matched exactly (Jacobi was already at the 2-opt-local optimum on those layouts), **0 regressed**, 1 improved (`evolved-anneal-3M.dom` 21→19 fails — resolved a genuine mutual `da1<->k1` adjacency miss the Jacobi loop couldn't reach). Runtime <1s even on the largest file (`evolved-3M.dom`, 90 base fails). §28 extends this to a 46-file sweep and turns the finish-time default on. Tests: `tests/test_collapse_global.py` gains `test_two_opt_polish_escapes_jacobi_plateau` (7 total in that file); 298/298 pass project-wide. ## 26. Multi-use leaves / type superposition (`homemaker-py-9o5`/`xi7`/`b3v`) — DONE (negative), backfilled *Closed 2026-06-30 (`9o5`, `xi7`) / 2026-07-17 (`b3v`); written up retroactively — this section was missing when §17/§20 above were written, even though both reference its verdict directly ("mirrors 9o5", "the opposite of the 9o5/xi7 verdict"). Numbered at the end of the log rather than renumbering §14-§25 to preserve every existing cross-reference.* **Motivation.** A leaf that legitimately serves several DIFFERENT compatible programme codes at once (study+guest bedroom, kitchen+dining — Stewart Brand's "loose-fit" long-life rooms), distinct from §13.3 leaf-sharing which aggregates *k* instances of the *same* code. Two readings were scoped: (a) superposition as a SEARCH RELAXATION — carry an uncommitted set of candidate types per leaf during search, collapse (argmax re-type) to specific usages only at scoring time, for a smoother landscape; (b) multi-use as the permanent DESIGN GOAL, surviving into the output with no collapse. Path (a) was built and validated (below); path (b) was never started. **Mechanism (path a, built).** `programme.derive_interchange_classes`: codes form an equivalence class (connected component, size ≥ 2) under a symmetric `interchangeable()` relation — S1 both sized and non-generic (no `c`/`o`/`s`), S2 size/width/proportion targets within LOCKED ratio bounds (`R_SIZE=1.5`, `R_WIDTH=1.3`, `R_PROP=1.5`), S3 compatible level and service stack, S4 no direct required-adjacency edge between the two codes (adjacency pairs are coexisting rooms, not one substitutable leaf). Pure function of the parsed programme — classes are auto-derived, no hand-authored list needed on the happy path. `Fitness.collapse_superposition` re-types every superposed leaf to its best in-class usage each eval, before any check: per class, an optimal supply (leaves currently in the class) → demand (class codes × required count) matching, area-weighted usage quality as the objective (brute-force ≤`CLASS_CAP` = 4! permutations, else scipy Hungarian — the same `_best_assignment` §17/§25 later reuse at global scope). Runs on the UNMERGED tree, so counts/adjacency/quality downstream see the condensed types with **no changes needed** to `graph.py`/`dom.py`/`operators.py` — the key design realisation was that because collapse re-types at eval time, `Node` never needs a persisted class/serves field and no mutation operator needs a "retype within class" move; the genome can carry *any* in-class type and collapse fixes it. Gated behind `superpose` (default OFF, bit-identical when off — verified against 233 pre-existing tests). `tests/test_superposition.py` (20): derivation (service/adjacency/level guards, the real programme-house programme), assignment (brute force + Hungarian + surplus supply/demand), end-to-end collapse re-typing, veto-hatch behaviour. **A/B verdict (`xi7`, measured 2026-06-30) — NULL/NEGATIVE.** Equal-budget `--superpose` ON vs OFF, measuring the COLLAPSED (final) score: - **programme-house** (`init.dom`, budget 3000, 4 workers, seeds 1–5): OFF wins 4/5 (s1 8f>10f, s2 11f>12f, s3 10f>12f, s4 10f>10f-tied-fitness — all OFF strictly better or equal fails), ON wins only s5 (10f→8f). - **harbor-house** (`init.dom`, budget 2500, seeds 1–3): OFF wins 2/3 (s2 33f<38f, s3 43f<48f); ON wins s1 alone (50f<51f). - Superposition does **not** reach better layouts; in most seeds ON has ≥ OFF fails — the per-eval collapse re-typing perturbs counts/adjacency rather than smoothing the search, the same failure mode later sections would call "landscape flattening." **Relaxation-gap instrumentation (`xi7` §7.4) — ruled OUT as the cause.** Logged relaxed (unconstrained best-case usage-quality) vs collapsed value on the same matched leaves across the 5 programme-house ON runs: total `gap_ratio` 1.01–1.23 (per-class peaks up to 1.52) — small-to-moderate, not the large gap the original risk note feared. Because collapse is *per-eval*, there is no separate relaxed phase to diverge from — search already optimises the collapsed objective by construction. **Conclusion: path (a) underperforms not from a relaxation gap but because the geometry floor (§11–§13) dominates** — type labels are not the binding constraint on these programmes, so easing them buys nothing while the re-typing adds feasibility noise. This is the diagnosis §20 (`qpk`) later cites when arguing its own in-search collapse is a *different* mechanism (a hard-constraint-respecting global relabel, not a per-class relaxation over interchangeable-but-not-identical codes) and so isn't pre-falsified by this verdict. **Veto hatch (`b3v`, closed 2026-07-17) — the one real false-positive found.** Harbor-house's programme auto-derives a **transitive 8-code chain** `{da1,ef1,k1,la1,m,me1,n,ws1}` spanning a 6× size range (Meeting 10 m² .. Dining/Neighbourhood 60 m²) — semantically nonsensical (Meeting↔Dining↔Kitchen↔ Mechanical are not interchangeable) but sanctioned by the S1–S4 relation as written (each adjacent pair in the chain individually satisfies the ratio bounds; connectivity is transitive). `xi7`'s harbor-house losses show ON *adding* fails in both loss seeds (38→33 became 38 vs 33; 48→43 became 48 vs 43) — consistent with this misgroup actively hurting. Fix: `SpaceReq.interchange` (default `True`), settable `interchange: false` per code in `patterns.config`, honoured by `interchangeable()`'s S0 check — an architect veto for one code without disabling superposition globally. `superpose` itself stays default OFF regardless (the `xi7` verdict was null/negative overall), so the hatch only matters if/when superposition is deliberately enabled on a real config. **Status.** `--superpose` stays default OFF; path (a) is not recommended without a fundamentally different mechanism (the geometry floor, not the labelling relaxation, is what needs to move — the same conclusion §11–§13's construction-quality work and §19's negative geometry-repair result both reach from other directions). Path (b) (multi-use as a permanent design goal, no collapse) was never attempted — remains open if revisited, but low priority given (a)'s outcome and the project's broader 0-for-several record on search-machinery/fitness-shaping bets vs construction-quality bets (see `mi7`, §27, for the same pattern one experiment later). ## 27. 3D bubble-diagram adjacency fitness signal (`homemaker-py-mi7`) — DONE (negative) *Closed 2026-07-25, the session immediately before §25's `9wi`. `bubble.py` was left in the repo **uncommitted** as a documented reference per the original close note; committed alongside this write-up so the reference this section makes to it is actually resolvable.* **Motivation.** `graph.py`'s adjacency checks are binary (is X adjacent to Y, yes/no) and, like §18's connectivity fail, give the search no gradient toward a better overall spatial *arrangement* — only toward satisfying each declared pair. Idea: build the programme's required-space adjacency as a graph, relax it into a 3D "bubble diagram" (a spring/repulsion physics simulation, architecture's traditional adjacency-diagramming technique), then score a candidate layout by how well its real room-to-room distances correlate with a relaxed target's distances — an additional graded fitness term / search- guidance signal, in the spirit of §18's graded connectivity but for general spatial layout rather than circulation topology specifically. **Mechanism (`bubble.py`, prototype only, never wired into `fitness.py`).** `requirement_graph`: one node per required room instance (`code`, or `code#i` for `count>1`), generic `c`/`o`/`s` adjacency targets collapsed to one shared hub node per code (per whole building, not per storey — a known simplification), edges to a multi-count code fan out to all its instances at reduced weight (satisfying adjacency needs only *one* matching neighbour). `generate_targets`: relax the requirement graph from `n_restarts` random 3D starts with a spring force (ideal edge length = sum of target-area-equivalent circle radii) plus overlap-only repulsion plus a level-height pull on the z axis; because relaxation is non-convex and multi-modal (different starts settle on e.g. opposite-handed but equally valid arrangements), keep up to `keep` distinct low-energy solutions (pairwise-distance-vector correlation ≥ `dedup_corr` = duplicate) rather than one canonical target. `similarity`: weighted Pearson correlation between an actual Dom layout's real weighted shortest-path distances and a target bubble's Euclidean distances, over matched non-generic room instances, weighted `1/hop_distance` in the requirement graph so the many hub-mediated "just wants to be near circulation" pairs (weak positional evidence) don't drown out the few directly-declared adjacencies (strong evidence). `best_similarity` takes the max across the kept alternative targets. `matched_leaves` maps anonymous multi-count codes to actual leaves by a fixed centroid-order rule — flagged in the module docstring as a known simplification, not a real assignment solver. `topological_similarity` is a cheaper no-embedding alternative: hop-distance correlation directly on graph topology (real multi-cell circulation network on both sides), skipping the physics simulation and multi-restart dedup entirely. **Validation (measured 2026-07-25) — NULL on both formulations, both programmes.** Correlated each similarity metric against real evolved trajectories (not static examples) via `driver.search`: - **programme-house** (n=100 recorded individuals): `embedding` ρ≈0.05, `topological` ρ≈−0.06 — flat. This is the cleanest data point: programme-house has **zero** multi-count anonymous codes, so `matched_leaves`' fixed centroid-order heuristic cannot be confounding the result, and it's still flat. - **harbor-house** (budget 6000, n=75, fitness 3e-28→3.9e-17, fails 83→51 over the trajectory): `similarity()` (embedding) spearman=0.164, p=0.16 (n.s.); `topological_similarity()` spearman=−0.160, p=0.17 (n.s.) — noisier than programme-house (heavy anonymous-count codes: `n`×5, `m`×3, `t`×6, `r`×10, `of`×2, a real uncontrolled confound for the centroid-order matching there) but tells the same story. - **No statistically significant correlation anywhere**, across 2 independent formulations (spatial embedding vs pure topology) × 2 programmes, with real search trajectories rather than canned batches. **Status.** Do not pursue graph-relaxation-derived or pure-topological adjacency-matching as a fitness signal for this project without a fundamentally different formulation. If revisited, the harbor-house anonymous-code confound would need a real assignment solver (Hungarian/brute-force, mirroring `9o5`'s `CLASS_CAP` pattern) before drawing any programme-specific conclusion there — but programme-house's clean, confound-free null already argues against the core idea regardless. `bubble.py` stays in the repo as a working, documented reference, not wired into `fitness.py`. Consistent with the project's broader pattern (§11.4/11.5, §12.3/12.4, §14, §16, §21, §22, §26 above): search-machinery / fitness-shaping changes have been null-to-negative essentially every time they've been tried; only construction/seeding quality and representation-relaxation changes (leaf-sharing §13.3, global collapse §17/§25) have moved the needle. This is another data point for that pattern, not an exception. ## 28. Default the `9wi` 2-opt polish on for finish-time collapse (`homemaker-py-cdl`) — DONE (positive) **Motivation.** §25 (`homemaker-py-9wi`) validated the 2-opt adjacency polish on harbor-house alone (11 files, 1 improvement, 0 regressions) and left it opt-in pending a broader, non-synthetic sweep and the `evolve.py`/`driver.collapse_best` wiring to expose it outside the standalone `homemaker-collapse` CLI. This closes that follow-up. **Broader sweep.** Extended the harbor-house comparison to programme-house's 34 `.dom` files (real evolved candidates, not synthetic), 46 files total across both example sets. Compared `collapse_finish(local_search=False)` against `local_search=True` (both keep-better against the uncollapsed base, per §17): **0 regressions**, 2 improvements — the known harbor-house `evolved-anneal-3M.dom` (21→19 fails) plus a new one on programme-house, `a82f07068e4408fdd0d5e3dc469a8dee.dom` (3→2 fails); every other file matched exactly. Confirms the finding generalises past the single synthetic dataset §25 was validated on. **Where the default did NOT change.** `collapse_global`'s own `local_search=False` default (§25) was left untouched. `collapse_global` runs twice in this codebase: once as a one-shot finish-time pass (`collapse_finish`, `homemaker-collapse`, `driver.collapse_best`) and once **per fitness eval** inside `_evaluate_full` when `collapse_insearch`/`qpk` (§20) is on — the latter is the hot path of the entire evolutionary search, run thousands of times per run, and the 46-file sweep only measured the one-shot cost (<1s even on the largest file). Flipping the method-level default would have silently turned the 2-opt pass on inside that hot loop too, an untested and likely-costly change out of scope for this issue. So the default stays `False` at the method level, and each one-shot call site turns it on explicitly instead. **Wiring.** `homemaker-collapse --local-search`/`--no-local-search` (`collapse_cmd.py`) now defaults `True` (was `False`). Added `homemaker-evolve --collapse-local-search`/`--no-collapse-local-search` (`evolve.py`), default `True`, passed through to `driver.collapse_best(..., local_search=...)` — which already forwarded arbitrary `**collapse_kw` to `fit.collapse_finish`, so no signature change was needed there. The new flag is a no-op under `--no-collapse` (nothing to polish if the finish-time collapse itself is skipped). **Verification.** 298/298 tests pass (no test changes needed — `test_collapse_global.py`'s explicit `local_search=True`/`False` cases already covered both method-level defaults). Re-ran `homemaker-collapse` standalone on `evolved-anneal-3M.dom` with no flags to confirm the new CLI default reproduces the 19-fail result end-to-end. ## 29. Beam/best-first search over adjacency-aware room placement (`homemaker-py-c94`) — DONE (inconclusive, mixed on harbor-house, null on programme-house) **Motivation.** Construction/seeding quality is the one lever with a consistent positive track record (§11.6/§11.7 adjacency-aware seeding, §12.2 proportion-aware seeding, §23 `f1d`'s reuse of the same constructor mid-search). `operators._assign_adjacency_aware` places rooms onto the circulation-dominated leaf set with a single greedy pass: hardest-constrained code first, each dropped onto whichever open slot currently satisfies the most of its declared secondary adjacency (beyond `c`) against *already-placed* neighbours. Because the pass never revisits a placement, an early code with no already-typed neighbours to match against (every mutual pair's first-placed half, e.g. harbor-house's `k1`↔`da1`) picks blind — any open slot scores identically at that step — and an unlucky tie-break could strand it from a partner that would only be placed several steps later. The proposal: explore the same per-room slot decisions with a width-K beam/best-first search instead of one irrevocable pass, scored by a cheap proxy (no geometry/fitness calls), and measure whether it ever finds a genuinely better seed before considering investing further (e.g. wiring it into the outer search config). **Mechanism (build).** `_assign_adjacency_aware` gained a `beam_width: int = 1` parameter (`operators.py`); `beam_width<=1` (default) is byte-identical to the prior greedy code path — verified by `test_construction_beam_width_default_matches_greedy` and by the full 298-test suite passing unchanged before any beam-specific test was added (302/302 after adding four new beam-specific tests). `beam_width>1` instead routes room placement through the new `_beam_place_rooms`: keeps up to `beam_width` partial placements alive, each step branching every surviving state into its top-`beam_width` candidate slots for the current code (same ranking greedy uses), scored by the running total of secondary-adjacency matches satisfied so far. This is genuinely cheap — no geometry or fitness calls, since circulation/outside are already fixed before room placement starts and the leaf-adjacency graph (`_nbrs`, `deg`, `idx`, `dominated`) is shared read-only across every branch — then prunes back to `beam_width` states before the next code, returning the highest-scoring complete placement. Threaded through as `construction_beam_width` in `constructive_topology`, `lift_base_to_storeys`, `driver.search`, and `driver.search_staged` (all default `1`, matching the project's existing knob-threading convention for `circ_divisor`/`depth_balanced`/etc. — no CLI flag added, consistent with those other construction-only knobs). Not threaded into `mutate_ruin_recreate` (kept parameter-light, like `bridge_circulation`/`ruin_recreate`'s own circ/outside ratios, §23). **Verified functioning (synthetic, not a no-op).** A hand-built adversarial 4-slot graph (two disjoint adjacent pairs, codes `a`↔`b` mutually required plus a filler `x` placed between them) confirms the mechanism is real: `beam_width=1` places `a` by an arbitrary tie-break, `x` then greedily grabs `a`'s only neighbour before `b` gets a turn, stranding the pair (`a-b adjacent=False`); `beam_width>=2` recovers the correct joint placement (`a-b adjacent=True`) by keeping `a`'s alternate slot choice alive long enough for `b`'s later score to reward it. This is exactly the "no lookahead" failure mode `_assign_adjacency_aware`'s one-shot pass is structurally prone to, and confirms the beam can and does out-score greedy when the graph offers a genuine trade-off. **Raw-seed check (2026-07-27) — misleadingly byte-identical, later shown insufficient.** Before running any search, a cheap diagnostic scored `constructive_topology`'s raw output directly (no GA, one `score_with_fails` call per seed): `beam_width` 1/4/8, 15 rng trials each, on programme-house and harbor-house — fail counts and adjacency/access fail counts identical to the last digit across all three widths, every trial. Extended to `lift_base_to_storeys` (the Stage-2 seeder) at widths 1/4/8/20, 10 trials: again byte-identical at every width, including `beam_width=20` (near-exhaustive for the ~15-17 codes per storey these programmes carry). A step-by-step trace of a real harbor-house construction (`da1`→`k1`→`ws1`) confirmed the beam *does* explore physically distinct slot branches, but every branch reached the same cumulative score every time — harbor-house's circulation-spine geometry usually offers several equally-good neighbours per code, so a lone raw-seed sample rarely hits a real trade-off. **This was wrongly taken as proof an end-to-end run would also be byte-identical** (a single root's construction never diverging was treated as sufficient to conclude the full bootstrap population never would either) — see the correction below. **End-to-end correction (2026-07-28, prompted by user question "should the default be 1? can we find out by running the two example programmes from a clean start?") — the raw-seed argument was wrong.** Ran `driver.search` from a clean bootstrap (`init.dom`, `n_workers=1` for reproducibility, budget 1500) at `construction_beam_width` 1 vs 4, same seed both arms, 5 seeds each programme: | programme | seed | bw=1 fails | bw=4 fails | result | |---|---|---|---|---| | harbor-house | 1 | 60 | 58 | bw4 win | | harbor-house | 2 | 67 | 52 | bw4 win (large) | | harbor-house | 3 | 53 | 53 | tie | | harbor-house | 4 | 52 | 52 | tie | | harbor-house | 5 | 52 | 62 | bw4 **loss** | | programme-house | 1–5 | (9,6,11,11,9) | identical | tie, all 5 | harbor-house: **2 wins / 1 loss / 2 ties**, mean fails 56.8 (bw1) → 55.4 (bw4) — a small mean improvement pulled mostly by seed 2's outlier, with a real loss on seed 5. programme-house: 5/5 ties, matching the raw-seed prediction exactly. The harbor-house divergence itself confirms the raw-seed reasoning's flaw: `driver.search`'s bootstrap builds `pop_size` individuals, each consuming a different slice of the RNG stream (unlike the single-root raw-seed check), and once even one population member's construction hits a genuine beam-vs-greedy tie-break divergence, the GA's subsequent *structure*-dependent choices (which subtree a mutation targets, crossover points) cascade into a different trajectory from there — even though the raw RNG numbers drawn are bit-identical between arms. "The one seed I checked never diverged" does not imply "no seed in a population of many ever will." **Interpretation.** The mechanism works (§ above, verified on a synthetic graph built to need it), and does occasionally get real traction on harbor-house's larger, more room-dense programme — but the 5-seed result is the same small-N, mixed-direction shape this log has repeatedly warned produces false signal (§23 `f1d`'s initial 8-run sweep, explicitly flagged there as "the 8sh/1ph/qi6/lj3 pattern"): a genuine loss (seed 5) sits alongside the two wins, and N=5 is far short of what `f1d`'s own larger-N confirmation needed (N=15/8) to separate a real effect from noise. programme-house shows no effect at any N tested, consistent with both the raw-seed check and its smaller, simpler room graph. **Status.** `construction_beam_width` stays default `1` — the *direct answer* to "should the default be 1": yes, current evidence does not clear this project's bar for flipping a default (cf. §20/§23's own "only after larger-N confirmation" standard), though harbor-house's mixed result (unlike programme-house's clean tie) means this is genuinely unresolved rather than a confident null. The code and tests stay in the tree as a working, verified-functioning building block (`operators._beam_place_rooms`), consistent with keeping validated-but-inconclusive mechanisms available rather than reverting them (cf. `bubble.py`, §27). A natural follow-up — not filed, low priority, matching `f1d`'s own unfiled size-threshold follow-up (§23/§24) — would be a larger-N harbor-house-only sweep (N=15+, matching `f1d`'s and `y51`'s bar) to determine whether the mean-improvement lean is real or an artefact of seed 2's outlier. ## 30. `c94` beam-width larger-N confirmation (`homemaker-py-e01`) — DONE (confirmed null) **Motivation.** §29's own filed follow-up: the 5-seed harbor-house end-to-end result (2W/1L/2T, mean fails 56.8→55.4) was flagged as the same small-N, mixed-direction shape that has repeatedly produced false signal in this log (`8sh`/`1ph`/`qi6`/`lj3`, §23's initial `f1d` sweep) — the mean was pulled mostly by seed 2's outlier (67→52), and N=5 falls well short of the N=15/8 bar `f1d`'s own larger-N confirmation needed to separate a real effect from noise. **Measured (2026-07-29, `experiments/run_e01_sweep.py`)** — identical protocol to §29: `driver.search` from a clean bootstrap (`init.dom`), `n_workers=1`, `budget=1500`, same seed both arms, `construction_beam_width` 1 vs 4, harbor-house only (programme-house showed zero effect at any N in §29 and was not re-checked). Extended seeds 1-5 (reproduced byte-identical to the §29 table, confirming the protocol) up to N=15: | seed | bw=1 fails | bw=4 fails | result | |---|---|---|---| | 1 | 60 | 58 | win | | 2 | 67 | 52 | win (large, the outlier) | | 3 | 53 | 53 | tie | | 4 | 52 | 52 | tie | | 5 | 52 | 62 | loss | | 6 | 65 | 65 | tie | | 7 | 50 | 50 | tie | | 8 | 68 | 64 | win | | 9 | 49 | 55 | loss | | 10 | 63 | 59 | win | | 11 | 63 | 62 | win | | 12 | 61 | 61 | tie | | 13 | 52 | 53 | loss | | 14 | 47 | 45 | win | | 15 | 53 | 58 | loss | N=15: 6 wins / 4 losses / 5 ties, mean fails 57.0 (bw=1) → 56.6 (bw=4), Wilcoxon signed-rank p=0.84 — no signal by any conventional threshold. Confirming the §29 suspicion directly: excluding seed 2's outlier, the mean *flips slightly negative* (56.3 → 56.9, bw=4 marginally worse), i.e. the entire 5-seed "mean improvement" that motivated this follow-up was that one outlier — the other 14 seeds average to a null-to-negative effect. **Interpretation.** The beam mechanism remains verified-functioning on its adversarial synthetic case (§29) but confirmed to find no reliable real-world traction on either example programme at any N tested. This resolves §29's "genuinely unresolved" status to a clean null, matching programme-house's result and consistent with `y51`'s own experience (§24) that small-N mixed-direction results in this codebase are usually noise rather than an early real signal. **Status.** `construction_beam_width` stays default `1`, now on confirmed (not just precautionary) grounds. Code and tests stay in the tree as a working, verified-functioning building block (`operators._beam_place_rooms`), consistent with keeping validated-but-null mechanisms available rather than reverting them (cf. `bubble.py` §27, `mi7`). ## 31. `y51` n=18 larger-N confirmation (`homemaker-py-xyu`) — INCONCLUSIVE, weak but not evaporated **Motivation.** §24's own filed follow-up (a): of `y51`'s four synthetic room-count sizes (10/14/18/22), n=18 showed the strongest trend at N=10 (7W/2L/1T, +9.3% mean fails, Wilcoxon p=0.098) despite sitting non-monotonically between two much weaker sizes — consistent either with a real-but-weak effect that N=10 underpowered, or with n=18 simply being the noisiest extremum of four small-N estimates. Extends only this one size to N=15, matching the sample size that resolved a similar-magnitude effect for `f1d`'s own programme-house confirmation (§23, p=0.041 at N=15). **Measured (2026-07-29, `experiments/run_xyu_sweep.sh`)** — 5 fresh seeds (11-15) appended to `y51`'s existing n=18 seeds 1-10, same protocol (`--ruin-recreate` weight=3.0 ON vs OFF, budget=3000, 4 workers, finish-time `--collapse`): | seed | OFF fails | ON fails | diff (OFF-ON) | |---|---|---|---| | 1 | 27 | 25 | +2 | | 2 | 33 | 29 | +4 | | 3 | 28 | 26 | +2 | | 4 | 30 | 30 | 0 | | 5 | 34 | 28 | +6 | | 6 | 45 | 35 | +10 | | 7 | 33 | 34 | -1 | | 8 | 46 | 42 | +4 | | 9 | 52 | 37 | +15 | | 10 | 37 | 45 | -8 | | 11 | 36 | 35 | +1 | | 12 | 29 | 28 | +1 | | 13 | 40 | 40 | 0 | | 14 | 44 | 44 | 0 | | 15 | 35 | 36 | -1 | N=15 combined: **9W/3L/3T**, mean fails **36.60 (OFF) → 34.27 (ON)**, Δ≈**6.4%** (down from N=10's 9.3%). Wilcoxon signed-rank two-sided **p≈0.059** (just misses conventional significance), one-sided (directional, matching the effect's own sign) **p≈0.029**; sign test on the 12 non-tied seeds is weaker, one-sided **p≈0.073**. The 5 new seeds alone were 2W/1L/2T — same direction as the original 10, weaker than them, but not reversed. **Interpretation.** Extending N=10→15 at the size that was *itself selected* for follow-up because it had the strongest of four initial signals is a scenario primed for regression to the mean, and that partly happened — the effect size shrank from 9.3% to 6.4% and the two-sided p moved from 0.098 to 0.059, i.e. still on the "not quite" side of both conventional thresholds. But the trend did not evaporate or flip the way §22's `lj3` weight bump or §24's own n=14 size did on their larger-N passes — it stayed directionally consistent across all 15 seeds' aggregate and crossed p<0.05 on the one-sided directional test. This is a genuinely ambiguous middle case: not the clean confirmation `f1d` got at the same N, not the clean reversal-to-null `lj3`/n=14 got either. **Status.** `enable_ruin_recreate` stays default **OFF** — this result alone does not clear the bar for a default flip even at n≈18-room scale, and harbor-house (37 room instances) remains null-to-negative (§23). §24's methodological caveat (the synthetic sweep scales room count by duplicating already-interchangeable codes, the same mechanism harbor-house itself uses, so it may not isolate the same "topology fraction sampled per wing move" variable the `f1d` hypothesis needs) is **not** addressed by this larger-N pass — only option (a) of §24's two follow-ups was run here. Option (b), a genuinely distinct third example programme (real room-type diversity at an intermediate room count, not a duplicated-code scale-up), remains the more likely route to a clean answer and is refiled as a fresh follow-up rather than closed out by this inconclusive N=15 read. ## 32. `health-centre` non-synthetic third example (`homemaker-py-9yx`) — CLEAN NULL **Motivation.** §31's own filed follow-up (option b): `y51`'s n=10/14/18/22 sweep scales room count by duplicating already-interchangeable `programme-house` codes (`b1`/`t1`/`b2`/`t2`/`l1`) via `count:` — the same mechanism `harbor-house` itself uses "to reduce complexity". `harbor-house` has real room-type diversity (16 distinct codes) but sits out of the tested range at 37 room instances, and its own result was already null-to-negative (§23) — so it cannot distinguish "the effect needs more real rooms than harbor-house has" from "the effect never existed outside the duplicated-code mechanism". A genuinely distinct programme at an intermediate, non-duplicated room count was needed to isolate room count as the variable. **Programme.** `examples/health-centre`: a small primary-care health centre, a building type unlike either `programme-house` (a house) or `harbor-house`/`maple-court` (dormitory-style co-housing). 19 distinct, individually-sized room codes, n=20 room instances (matching `xyu`'s own n=18 test point closely, without leaning on `count:` as the scaling knob — the only duplication is a realistic pair of public WCs). A first draft's room sizes formed a single transitive interchange class spanning all 19 codes — `9o5`'s auto-derived interchange relation chains through any sequence of pairwise-close-enough neighbours, so a smooth size gradient from a 3 m² WC up to a 28 m² waiting room reconnects the whole building into one class regardless of the individual rooms being genuinely different types. This would have silently reintroduced the exact confound the issue exists to eliminate. Fixed by deliberately tiering room widths with >1.3x gaps at three boundaries (micro/utility, office/support, large clinical/public), which resolves to three bounded classes (sizes 6, 9, 4) instead of one whole-building chain — the same shape of result `harbor-house` itself gets from a real programme, and consistent with `9o5`/`b3v`'s own experience that this needs active management rather than resolving itself. **Measured (2026-07-30, `experiments/run_9yx_sweep.sh`)** — 15 fresh seeds (1-15), same protocol as `xyu` (`--ruin-recreate` weight=3.0 ON vs OFF, budget=3000, 4 workers, finish-time `--collapse` default): | seed | OFF fails | ON fails | diff (OFF-ON) | |---|---|---|---| | 1 | 42 | 42 | 0 | | 2 | 44 | 40 | +4 | | 3 | 53 | 46 | +7 | | 4 | 47 | 46 | +1 | | 5 | 46 | 43 | +3 | | 6 | 43 | 47 | -4 | | 7 | 41 | 47 | -6 | | 8 | 51 | 43 | +8 | | 9 | 44 | 48 | -4 | | 10 | 50 | 54 | -4 | | 11 | 45 | 43 | +2 | | 12 | 44 | 44 | 0 | | 13 | 47 | 42 | +5 | | 14 | 45 | 39 | +6 | | 15 | 50 | 53 | -3 | N=15: **8W/5L/2T**, mean fails **46.13 (OFF) → 45.13 (ON)**, Δ≈**2.2%** — well below `xyu`'s already-weak 6.4% at the same scale. Wilcoxon signed-rank two-sided **p≈0.40**, one-sided (directional) **p≈0.20**; sign test on the 13 non-tied seeds one-sided **p≈0.29**. Nowhere near any conventional threshold, in either direction. **Interpretation.** At a real, diverse ~20-room programme, `ruin_recreate`'s effect is indistinguishable from noise — much weaker than even `xyu`'s own inconclusive N=15 reading (6.4%, p≈0.059) at essentially the same room count. This is the cleanest evidence yet that the `y51`/`xyu` signal was substantially (if not entirely) an artifact of the duplicated-interchangeable-code scaling mechanism itself — repeatedly placing several copies of the *same* small room set — rather than a genuine effect of room count/topology scale that would transfer to a building with that many *different* rooms. It converges with `harbor-house` (37 real instances, null-to-negative, §23) rather than with `y51`'s own synthetic n=18 reading, closing the gap that made §31 ambiguous. **Status.** `enable_ruin_recreate` stays default **OFF**, now on a broader evidence base: null-to-negative on every real (non-duplicated-code) programme tested at any scale from 6 rooms (`programme-house`) to 37 (`harbor-house`), and only ever weakly positive on the synthetic duplicated-code sweep that this result suggests was measuring the wrong thing. No further follow-up is filed — the room-count hypothesis from `f1d` (§23) is now addressed on the diversity axis `xyu` (§31) could not reach. ## 33. Multi-use leaves as a permanent design goal (`homemaker-py-1s3`, §26 path b) — DONE (NULL, N=3 signal did not replicate) **Motivation.** §26 scoped two readings of "multi-use leaves" — a leaf legitimately serving several DIFFERENT compatible programme codes at once (study+guest bedroom, kitchen+dining, Stewart Brand's "loose-fit" long-life rooms). Path (a), superposition as a per-eval search relaxation, was built and measured NULL/NEGATIVE (§26): the geometry floor dominates, not the type-labelling relaxation gap. Path (b) — multi-use as the *permanent design goal*, surviving into the output with no collapse — was never attempted. The framing going in: path (b) is structurally the same lever as leaf-sharing (§13.3, `homemaker-py-x3b`) — the single biggest positive lever in the project (−32…−39% on the achievable fail floor) — extended from *same*-code multiplicity to *different*-but-compatible codes, with a materially larger addressable set on programmes with many small single-instance rooms (`health-centre`'s 19 distinct codes, §32). **Mechanism.** Explicit, architect-declared `co_locate: [code, ...]` per `SpaceReq` (unlike `interchange` classes, never auto-derived — fusing two codes onto one leaf is a much stronger commitment than a soft substitution class). `programme.derive_colocate_pairs` keeps a declared pair only if it *also* passes the existing `interchangeable()` S1-S4 relation (§26/`9o5`) — reusing the already-validated bounds instead of inventing a second relation — and returns pairs only, never folding them into connected components, so the `b3v` transitive-chain failure mode (§26) cannot arise by construction. `Node.co_type` (new field, sibling to `share`/`share_type`) records the second code a leaf serves; `graph.leaf_codes()` is the resolver every programme-check function (`check_space_counts`, `check_adjacency`, `check_level_constraints`, `check_vertical_connectivity`, `has_adjacency`, `has_vertical_connection`) now routes through instead of comparing `leaf.type` directly — returning `[type, co_type]` only while `multi_use` is on AND the pair is still a currently-valid declared co-location (a retype silently drops a stale `co_type`, the same self-healing type-guard `leaf_share` uses). `fitness.quality_size` combines a fused leaf's two codes **additively** (target and sigma both sum — the same operation as leaf-sharing's k×target, generalised from k identical terms to 2 different ones — area genuinely sums across two uses). Construction-time only (no mutation operator): `operators._colocate_rooms` greedily fuses available same-storey instances of a declared pair (before `_share_rooms`, so same-code sharing still groups whichever code is kept primary), `_leaf_colocate_from_plan` stamps the winning leaves, and `_size_divisions_from_targets` grows the fused leaf to the combined target. Gated behind `multi_use` (default OFF, bit-identical when off — 335/335 tests pass including 33 in `tests/test_multi_use.py`). Threaded end-to-end through `driver.py`/`evolve.py --multi-use`, mirroring `superpose`'s existing wiring. **Shape-combination sub-experiment — `quality_width`/`quality_proportion`.** Unlike area, a leaf's width and aspect are the SAME physical measurement serving two potentially-different codes' targets at once, so "additive" makes no sense — three combination strategies were tried, in this order, each triggered by review of the previous: 1. **Naive max-target/min-sigma ("stricter of both").** The first cut: pick whichever code's target is harder to satisfy. Simple, but ad hoc — it does not correspond to any principled combination of the two codes' evidence. 2. **Precision-weighted product (`fitness._gaussian_product`).** The product of two Gaussian curves evaluated at the same point is itself proportional to a Gaussian: precisions (`1/sigma^2`) ADD, and the combined target is the precision-weighted average — an INTERMEDIATE target (never simply the stricter one) with a NARROWER spread than either input. The standard way to combine two pieces of independent evidence about the same quantity. 3. **Mixture (`fitness._clipped_gaussian` + `max()`).** A different philosophy: the leaf need not compromise between the two codes' targets at all — score it against whichever target the realised geometry ends up closer to (a wide, bimodal tolerance), echoing this project's own per-leaf usage collapse (§26 path a) but applied within one leaf's shape terms instead of across its whole type. Appealing in principle (no forced compromise) but, per the A/B below, empirically the worst of the three. **Declared pairs.** Architect-authored in each programme's `patterns.config`, hand-picked from the pool of `interchangeable()`-eligible candidates on semantic grounds (not every eligible pair is a sensible fusion — e.g. `health-centre`'s public/staff WCs and sterilisation room pass the S1-S4 bounds but were deliberately left undeclared): `harbor-house` — foyer/meeting-room (`ef1`/`m`), laundry/plant-room (`la1`/`me1`); `health-centre` — admin/manager's office (`ao1`/`mo1`), admin/staff-room (`ao1`/`br1`), dental/minor-surgery (`de1`/`ms1`), storage/records (`dp1`/`re1`). **End-to-end A/B, all three shape-combination strategies** (`experiments/run_multiuse_ab.sh`, staged search, 20 000 native evals, seeds 0/1/2, 4 workers, final native re-score, mirrors §13.3's harness; each run verified single-process before launch — an early attempt let two runs overlap and contaminate the results, discarded entirely, see the bead's history): | combination | harbor-house (s0/1/2) | mean | Δ | health-centre (s0/1/2) | mean | Δ | |---|---|---:|---:|---|---:|---:| | baseline (no multi_use) | 95/101/103 | 99.7 | — | 63/82/71 | 72.0 | — | | 1. stricter-of-both | 92/101/94 | 95.7 | **−4.0%** | 81/111/77 | 89.7 | **+24.5%** | | baseline (re-measured) | 95/101/90 | 95.3 | — | 63/82/71 | 72.0 | — | | 2. precision-weighted | 82/117/83 | 94.0 | **−1.4%** | 65/78/43 | 62.0 | **−13.9%** | | baseline (re-measured) | 95/102/97 | 98.0 | — | 63/82/71 | 72.0 | — | | 3. mixture | 81/110/81 | 90.7 | **−7.5%** | 91/92/77 | 86.7 | **+20.4%** | (Baseline drifts slightly run-to-run — the staged search's own within-seed run-to-run noise at this budget/worker-count, not a bug; each combination's Δ is against its own paired baseline row.) Among the three, the precision-weighted single-compromise-peak model was the only one to improve BOTH programmes at N=3, so it is the one landed in the shipped code (`_clipped_gaussian`/mixture kept in `fitness.py`, documented and unit-tested, as a recorded negative alternative). But per the confirmations below, this N=3 comparison — used to pick a combination strategy — turned out to be too small a sample to trust for the multi_use verdict itself. **Larger-N confirmation — the N=3 signal did not replicate.** N=3 is a thin sample (§31/§32's own standard is N=15), so the precision-weighted result was checked two ways before considering any default-flip recommendation: | test | conditions | harbor-house Δ | health-centre Δ | |---|---|---:|---:| | original | N=3, staged search, budget 20 000 | **−1.4%** (2W/1L) | **−13.9%** (2W/1L) | | confirm #1 | N=15, plain search, budget 3 000 (mirrors `xyu`/`9yx`'s own protocol exactly) | +6.1% worse (5W/10L, p=0.30) | +6.6% worse (3W/11L/1T, Wilcoxon p=**0.044**) | | confirm #2 | N=15, staged search, budget 20 000 (**same conditions as the original**, more seeds) | +6.6% worse (4W/11L, p=0.15) | +4.7% worse (6W/9L, p=0.48) | Confirm #1 uses a cheaper protocol (budget 3000, and for the multi-storey `harbor-house`, plain search rather than staged — `search_staged` only falls through to plain search on single-storey programmes) so it answers a related but distinct question. Confirm #2 is the true same-conditions replication — identical to the original A/B except 15 seeds instead of 3 — and it **also trends negative on both programmes**, though neither reaches significance at this N. Two of the three measurements, including the one that actually matches the original protocol, disagree with the original finding's *direction*. The honest read: the N=3 positive result was very likely sampling noise from an unlucky (or lucky) three-seed draw, not a real effect — `harbor-house`'s original 2W/1L was already a coin-flip-sized sample, and `health-centre`'s dramatic −13.9% at N=3 (driven substantially by one seed swinging from 71→43 fails) did not hold up at N=15 (mean Δ flipped to +4.7%, p=0.48 — indistinguishable from no effect). **Diagnosis.** Leaf-sharing's k×target scaling never changes the SHAPE constraint: k identical rooms share one identical width/proportion target, so a shared leaf is exactly as easy or hard to satisfy geometrically as any single instance of that code, just bigger. Multi-use fusion is different — the combined leaf's larger area target competes with every other room for the same limited plot area, and (whichever shape combination is used) the fused leaf's shape constraint is at best as forgiving as either code alone, never more so. The mechanism does not appear to reliably pay for this cost the way leaf-sharing's pure count relaxation does — consistent with the broader pattern in this log (§11.4/11.5, §14, §16, §21, §22, §26, §27, §30) that search-machinery/fitness-shaping-adjacent levers rarely move the needle, and that small-N results in this problem class need real confirmation before being trusted (the same lesson `y51`/`xyu`/`9yx`, §31/§32, already taught once). **Status.** `multi_use` stays default **OFF** and is not recommended even as a promising candidate — the larger-N evidence points toward NULL-to-mildly-negative rather than positive. The mechanism itself (declared `co_locate` pairs, `graph.leaf_codes()` resolver, precision-weighted shape combination, construction-time fusion) is complete, fully tested (335/335 passing, `tests/test_multi_use.py`), gated OFF by default and bit-identical when off, so it remains available if a future architect wants to opt a specific programme into it manually despite the null aggregate result — but no further investment (default flip, additional combination strategies, or a larger sweep) is planned. This closes out `homemaker-py-1s3` and, with it, both halves of §26's original multi-use-leaves question: path (a) (search relaxation) was NULL/NEGATIVE, path (b) (permanent fusion) is NULL after replication. ## 34. Spike: autodiff/gradient-based inner-loop ratio optimisation (`homemaker-py-2ax`) — DONE (negative, wall-clock) **Motivation.** `innerloop.py`'s default inner-loop optimiser (`nm_search`, multi-start Nelder-Mead) is derivative-free — a legacy of the Perl-subprocess oracle era when fitness was not differentiable. Fitness is now a native Python port (`fitness.py`) built from ordinary arithmetic (Heron's-formula areas, Gaussian quality terms), plausibly differentiable. Nobody had tried gradient-based optimisation since the port. Real risk flagged going in: the deliberately-preserved `0.5^n` failure-count penalty cliff (§4.5) is a sharp discontinuity by design, which could make raw gradients unreliable near failure boundaries. **What was actually built.** The full fitness pipeline (`_evaluate_full`, 1700+ lines) is not literally differentiable end-to-end regardless of the geometry — staircase fit truncates to integers (`_risers_number`/`_ideal_going`/`_*_turn`), physical adjacency is a `door_width` threshold on wall overlap, `access` is a categorical neighbour-type test, and `check_space_counts`/`check_adjacency`/etc. are graph algorithms over discrete labels. Porting all of that to an autodiff framework was out of scope for a spike and would still bottom out in the same non-smooth primitives. Built instead: `experiments/autodiff_spike.py`, a torch mirror of `geometry.py`'s coordinate recursion (`coordinate`/`coord_a`/`coord_b`/`area`/ `edge_length`/`angle`/`aspect`, exact port, tensors instead of floats) driving the 5 per-leaf quality factors that vary continuously with the ratios (perpendicular, proportion, size, width, crinkliness) plus the cost/value accumulation (leaf cost, edge cost, outside-edge cost). Every *structural* fact that doesn't vary continuously for a frozen topology — which leaves are adjacent, boundary ids, leaf types/params, which fails are structural (missing/adjacency/level/vertical/access/staircase/storey/edge-too-long) — is snapshotted ONCE from a real `fitness.py` evaluation at the start ratios (`TorchTopology._snapshot`) and held frozen; `building_factor` (programme area-ratio Gaussians, staircase volume, storey/public-access checks) is folded into one calibration constant rather than ported. The `0.5^n` cliff itself is relaxed to a steep sigmoid (`soft_fail`, steepness 60) on each continuous factor's `FAIL_THRESHOLD` test, so the proxy is smooth everywhere — this directly probes the flagged risk rather than assuming it away. `torch.optim.Adam` ascends the proxy; the true fitness (`NativeEvaluator`-equivalent) is re-checked and the topology re-snapshotted periodically, a trust-region-style refresh since the frozen adjacency set can in principle drift as ratios move. **Measured, two frozen topologies (CPU, no GPU in this environment):** | topology | DOF | nm_search (200 evals) | torch: 1 fwd+bwd step | ratio | |---|---:|---|---|---:| | `programme-house/candidate-002.dom` | 6 | 200 evals / **3.0 s**, fitness 0.0142 (2 fails) | 200 Adam steps (10 resnaps) / **106 s**, fitness 0.0041 (3 fails) — worse on both axes | **~35×** slower, worse result | | `harbor-house/3m.dom` | 36 | 200 evals / **14.6 s** | 1 step ≈ **2.1 s** (200 steps ⇒ ~420 s projected, before resnapshot overhead) | **~29×** slower per unit of search progress | The slowdown is per-op tensor dispatch overhead (each leaf/edge is a handful of scalar torch ops, no batching across leaves — nothing here is a large matmul torch is built to accelerate) plus the snapshot/ re-snapshot cost (a real `fitness.py` evaluation on a deep copy, same cost class as one `nm_search` eval, but paid on top of the gradient step rather than instead of it). A small-step gradient test (`lr` 0.01/0.03/0.1 from the same `x0`) confirmed the flagged cliff risk concretely: 0.03 improved true fitness, but 0.01 and 0.1 from the *same descent direction* both crossed into a new failure and scored worse than the start — gradient *direction* carries real local signal, but step size right next to the cliff is as fragile as the issue predicted, and nothing about autodiff removes that fragility (it only makes the direction cheaper to compute, and the wall-clock numbers show it isn't even cheaper here). **Verdict.** NULL/NEGATIVE — not recommended. Even setting aside the failure-cliff sensitivity, the autodiff path is decisively slower per unit of progress than `nm_search` at both scales tested, and does not reach a better fitness in the equal-"budget" comparison at the small scale. The theoretical case for autodiff (avoid the ~DOF-proportional cost of finite-difference-style multi-start search) does not survive contact with this problem's actual shape: very few, cheap-to-evaluate scalar dimensions per leaf, no batching opportunity, and a real per-step evaluation cost (snapshot refresh) comparable to a full oracle call anyway. `experiments/autodiff_spike.py` is kept as a reference/starting point (not wired into `innerloop.py`) should a future architect want to revisit this at a very different scale (e.g. thousands of DOF, where nm_search's `O(DOF)` per-iteration cost would start to dominate) — not worth further investment at current programme/topology sizes (6-40 DOF). ## 35. Stale leaf-share leak into `collapse_global`'s candidate valuation (`homemaker-py-iio`) — FIXED, retroactive impact partially assessed **Discovery.** Found while diagnosing `homemaker-py-91f`: rescoring a dumped `.dom` under the `leaf_sharing`+`collapse_insearch` stack did not reproduce `driver.search_staged`'s own reported `n_fails`. `copy.deepcopy(r.best.root)` rescored in-process matched the search's own number exactly (37 fails, harbor-house seed=0, budget=20000, full default stack); `dom.dump`+`dom.load` of the exact same tree, rescored identically, gave 64. Ruled out first: hash-seed randomness (stable across `PYTHONHASHSEED` 0-4), float-precision loss (`dom.dump`/`dom.load` round-trips a Python `float` exactly — `yaml`'s float representer uses `repr()`, which is round-trip-exact by construction — confirmed no `numpy.float64` leaks into `.division`, all such writes already go through `float(...)`), and below-link/geometry staleness (the leading hypothesis going in — `dom._link` is re-run after every structural mutation, so this turned out to be a dead end). **Root cause.** Not geometry at all — a metadata leak in `Fitness._collapse_value` and `Fitness._usage_quality` (`fitness.py`). Both temporarily overwrite `leaf.type` to probe a *hypothetical* candidate code (`_collapse_value` inside `collapse_global`'s Hungarian assignment build; `_usage_quality` inside `collapse_superposition`/9o5), call `quality_size`, and restore the original type in a `finally`. `quality_size` reads `graph.leaf_share(leaf, max_share)`, which returns the leaf-sharing multiplier `k` only when `leaf.share > 1 and leaf.share_type == leaf.type` — by design, this makes a share stamp "stale" (harmless) the moment a leaf is retyped away from the code it was stamped for (§13.3's own documented contract). But because the probe overwrites `leaf.type` to the *candidate*, not the leaf's real current type, `leaf_share`'s guard compares the stale `share_type` against the CANDIDATE code — so whenever a probed candidate happens to equal a leaf's old, stale `share_type`, the k× size-target credit spuriously reactivates for that one (leaf, candidate) cell, even though the leaf never actually committed to that code. This skews that one cell of the Hungarian matrix and can flip which leaf `collapse_global` assigns to which room. `dom._emit` only serialises `share` when `share_type == type` (the same live/stale guard, correctly applied to the leaf's REAL type) — so a stale `share`/`share_type` combo is silently dropped on `dom.dump`+`dom.load`. That is exactly why the live in-process tree (still carrying the stale metadata) and its dump/reload round trip (metadata gone) fed different values into the same `collapse_global` call and landed on different optimal assignments. Structural diff of the live vs. reloaded harbor-house tree that triggered this showed exactly two leaves differing, both in `share`/`share_type` only (e.g. `share=3, share_type='n'` live vs. `share=1, share_type=None` reloaded) — nothing else (no type, division, or below-link differences). **Fix** (`src/homemaker_layout/fitness.py`): in both `_collapse_value` and `_usage_quality`, temporarily clear `leaf.share_type` for the duration of the probe whenever the candidate differs from the leaf's real current type, restoring it in the `finally` block. The leaf's own real current type (the non-hypothetical, `code == orig` case — e.g. `_two_opt_adjacency_polish`'s `reward()`, which always evaluates a leaf's own current type, never a hypothetical one, and so was never exposed to this bug) still legitimately carries a live share. **Verification.** Two new regression tests in `tests/test_collapse_global.py` (`test_collapse_value_ignores_stale_share_for_hypothetical_code`, `test_collapse_global_dump_reload_agree_with_stale_share`), both confirmed to fail pre-fix and pass post-fix. Full suite 337/337. Re-ran the exact 91f repro (harbor-house seed=0, budget=20000): search / in-process rescore / dump-reload rescore now agree at 37/37/37 (previously 35/35/64). **Retroactive impact: who was exposed.** The bug requires `leaf_sharing=True` (default since §13.10 `x3b`) *and* `collapse_global` running on a tree carrying a stale share — either every eval (`collapse_insearch=True`, default since §20 `1ph`, 2026-07-24) or once at finish time (`--collapse`, 94g, §17, default on since before that). That describes essentially the whole "full default stack" used for every experiment from `x3b` onward, including the very studies that justified defaulting these features on (`94g`, `qpk`/`1ph`, `8sh`, and everything downstream). Two things are NOT exposed: the `9wi`/`cdl` 2-opt polish (`reward()` always probes a leaf's own current type — see above), and `9o5`/`superpose`-only runs (exposed via `_usage_quality`, but only when `superpose=True`, which has always defaulted off and — as far as this investigation went — was not cross-checked against whether `leaf_sharing` was also on in that specific historical A/B). **Re-verification performed (2026-08-02).** Re-ran the qpk protocol's harbor-house arm (`examples/harbor-house/init.dom`, budget 2500, seeds 1-3, 4 workers, `--collapse-insearch` ON/OFF, canonical `homemaker-fitness` re-score) on **today's codebase**, once with the `iio` fix in place and once with it reverted (`git show 929be5b~1:src/homemaker_layout/fitness.py` swapped in temporarily via the editable install, then restored — no commit was made with the bug reintroduced): | seed | collapse_insearch | fixed | pre-fix (buggy) | |---|---|---:|---:| | 1 | OFF | 85 | 85 | | 2 | OFF | 76 | 76 | | 3 | OFF | 80 | 80 | | 1 | ON | 82 | 74 | | 2 | ON | 65 | 65 | | 3 | ON | 72 | 77 | OFF is byte-identical between the two code versions on all 3 seeds — expected, since OFF never calls `collapse_global` during search, only once at finish time, and none of these three final trees happened to carry a triggering stale share at that point. ON diverges on 2 of 3 seeds, by a real margin (seed 1: 74 vs. 82, an 8-fail swing; seed 3: 77 vs. 72, a 5-fail swing) — and, critically, **not directionally**: the bug's noise landed better on seed 1 and worse on seed 3. This is consistent with the mechanism (a coincidental corruption of one assignment-matrix cell, not a systematic push in either direction). **What this does and doesn't establish.** It establishes the bug was not merely theoretical: it demonstrably perturbed real per-seed outcomes under `collapse_insearch=ON` on this exact protocol, by margins (5-10% of the fail count) that are not negligible next to the ~10% mean effect `qpk`/`1ph` reported. Because the perturbation is non-directional noise rather than a systematic bias, it's unlikely to have flipped `1ph`'s aggregate, statistically-tested verdict (N=20 programme-house seeds, paired t-test p≈0.028, consistent direction and magnitude with the original N=5 sample and with harbor-house) — random per-seed noise in both directions tends to average out rather than compound across a 20-seed sample. But this was NOT rigorously confirmed: the comparison above reran today's code (fix vs. no-fix), not the actual historical commit at the time `1ph`/`qpk` were measured, and used only 3 harbor-house seeds, not the original seed sets. Any specific historical per-seed number quoted in §17/§20/§21 (and elsewhere the full default stack was used) should be treated as carrying real, now-quantified uncertainty from this bug; the qualitative "leaf-sharing helps" / "in-search collapse helps" conclusions are probably still sound but were not independently re-proven against the fix. **Follow-up (not done here, low priority, filed as `homemaker-py-d86`):** a rigorous re-verification would check out the codebase near the `1ph` commit (2026-07-24), backport the `iio` fix there in an isolated worktree, and re-run the *actual* historical seed set (programme-house N=20, harbor-house N=3) to get a direct before/after comparison against the published numbers, rather than today's much-improved baseline (which, at these budgets, mostly saturates to 0 fails and so is no longer a useful testbed — see below). **Aside: today's baseline has moved far past the `qpk`-era regime.** An earlier pass at this re-verification (same protocol, same code) produced a systematic false "0 fails" for every arm/seed — traced to a bug in the *verification script*, not the product: it built `homemaker-fitness`'s target path as `realpath "../../$dom"` (copied from `experiments/run_8sh_ab.sh`, where the equivalent `$dom` is relative to the repo root) against an already-absolute scratch path, `realpath` failed, the error was swallowed by `>/dev/null 2>&1`, and the harness's `fails=0` fallback silently reported success instead of an error. Once the path bug was fixed, real (non-zero) numbers came back matching the historical scale. Two things worth remembering from this: (1) `programme-house` at the `1ph` budget (3000) now reaches 0 fails on every seed/arm tried under today's full default stack — a large improvement since `1ph` from the many subsequent Phase-8/9+ landings — so it is no longer a useful regression testbed for this particular question at that budget; harbor-house (budget 2500, still 65-85 fails) still has real headroom and is what the table above uses. (2) a silent-failure-shaped "suspiciously good" result is a smell — a fallback default that never reports "ERR" loudly is worth distrusting on sight (the harness now sets `fails=ERR` on a missing `.fails` file instead of `0`, kept in `qpk_verify_ab.sh`/`qpk_verify_hh_ab.sh` in scratch, not committed). ## 36. Expert review of the numeric/scoring path (`homemaker-py-zrx`) — DONE, 3 confirmed bugs filed Motivated by §35: the `iio` stale-share leak survived unnoticed because it corrupted scores without crashing anything. This review read the whole numeric path end-to-end — `fitness.py`, `solver.py`, `collapse_cmd.py`, the `collapse_insearch` path through `innerloop.py`/`driver.py`, plus the `geometry.py`/`graph.py`/`dom.py` substrate and `evolve.py` plumbing — hunting specifically for that bug class (stale shared state, valuation/accounting mismatches, parallel non-determinism). Three confirmed bugs and one hygiene task, each verified with a runnable probe before filing: - **`homemaker-py-r5a` (P2) — stale-share *resurrection* through the collapse commit.** The `iio` fix guards the *probes*, but when `collapse_global` (or a 2-opt swap) commits a leaf back to its stale `share_type`, the k× credit reactivates — a credit the Hungarian matrix just valued at 1× — and the resurrected stamp then serialises (`type == share_type` again), so it persists. Minimal repro diverges live vs dump/reload evals of the *same tree* 12 vs 19 fails (scores 7.4e-08 vs 7.3e-11): the §35/91f divergence class, reopened through the commit door. Recommended fix: canonicalise stale stamps at `_evaluate_full` entry, mirroring `dom._emit`'s guard. - **`homemaker-py-cvw` (P2) — parallel staged runs read stale geometry through `id()` reuse.** With `n_workers>1`, `search_staged` stage 1 computes `substrate_readiness` in the *parent* process, which never scores and so never clears `geometry._cache`; evicted individuals' id-keyed entries alias freshly unpickled children. Churn probe: 24/300 readiness values corrupted (worst error ~1.0 on a [0,1] signal), cache growing unboundedly. Address-dependent stage-1 selection bias — a concrete non-BLAS candidate for part of `b8g`'s irreproducibility. Serial runs are safe. - **`homemaker-py-sd3` (P3) — `collapse_best`'s keep-better guard is vacuous.** Its evaluator is built with `_fitness_for`'s default `collapse_insearch=True` (the run flag cannot be threaded through), so `base_fails` is measured on a copy that *re-collapses in-eval*: base == collapsed on 5/5 probed files (logs "12 → 12" where the canonical evaluator shows 15 → 12). The 94g safety property is not actually checked against the true base, and a `--no-collapse-insearch` run's finish evaluator contradicts its own objective (the deterministic `7ua` mechanism, in the product). - **`homemaker-py-pek` (P3)** — `fitness.py` carries two `process_storey` definitions; the first is dead code silently shadowed by the second, a silent-bug vector for future edits. Reviewed clean: the gaussian/truncated-e ports, `_gaussian_product`, count/adjacency/level checks and missing-id suppression, `collapse_global`'s pin/slot/forbid accounting and Jacobi update, the `xcy` submission-order determinism fix, `NativeEvaluator` deepcopy hygiene (the per-eval `geometry.clear_cache()` at `_evaluate_full` entry protects the whole in-eval path), and `merge_divided` (o/s-only, so no share-stamp interaction). `solver.py` is experiments-only — nothing on the search path calls it. `collapse_finish`'s cand-deepcopy id-reuse hazard was probed 0/6 (Node trees are reference cycles, so the dead copy outlives the reuse window); a defensive clear at `collapse_global` entry is folded into `cvw`. Verdict on the method: the §35 hypothesis held — all three confirmed bugs are silent, non-crashing, and invisible to the test suite (337/337 green throughout), and two of them sit exactly on the leaf-share/collapse seam `iio` came from. ## 37. Phase 9 plan: ground truth, exact evaluation, solver-directed search (`homemaker-py-2g7`) **Epic:** `homemaker-py-2g7`. **Status:** scoped 2026-08-02, pre-implementation. Strategic review of §11–§13 + the 3M-eval runs (`examples/harbor-house/evolve-3M*.log`: 1.7 M evals / 2.4 days inside one 15-fail tier, hard structural fails — level connectivity, wrong-level — surviving > 1 M evals despite dedicated repair operators). The scoreboard is lopsided: every fail-count win of Phases 6–8 was a construction/objective-honesty lever; every search-machinery lever (§11.4 grade, §11.5 niching/restarts, §11.8 tournament-k, §14 islands, §16 annealing, §29/§30 beam, §27 bubble, §34 autodiff) was null or negative. Three root causes, three tracks: 1. **No ground truth.** Every non-empty `.dom` in the repo is evolution output — there are *no human-generated plans in the corpus*, so nobody has ever measured what a known-good design scores; "the examples are solvable" is currently unfalsifiable, and the residual taxonomy (crinkliness = 48 %, §13.11) may be miscalibrated rather than unmet. → `2g7.1` plan→dom composer + traced human solutions (guillotine-cut extraction from rectangular partitions; non-slicible input is itself a representability finding) → `2g7.2` objective calibration against them → `2g7.3` hard/soft fail tiering ("solved" = 0 hard fails; guards: §4.5/§4.9 inner-loop cliff protection must survive) — **DONE, PASS, see §37.1**. 2. **Evaluation is ~10²–10³× too expensive.** The 80-eval NM inner loop answers a question the classic Otten/Stockmeyer slicing-floorplan shape-curve DP answers exactly in one bottom-up pass (feasibility + optimal ratios for the size/width/proportion family). → `2g7.4` (prototype on harbor-house-l0, rectangular-plot approximation, DP as pre-filter + NM warm start), unlocking `2g7.9` parallel best-of-N + racing (blocked by `cvw`/`b8g`; §14 showed best-of-N ≥ islands; the box has 4 cores and 3M runs used 1–2 workers) and `2g7.10` MAP-Elites (elite-per-niche archive — mechanically distinct from the failed §11.5/§11.8 diversity-under-one-selection). 3. **Evolution used as a constraint solver.** Discrete subproblems have exact methods: `2g7.5` CP-SAT type assignment for a fixed tree (the optimal big brother of the §11.6/§11.7 greedy assignment — the biggest Phase-6 win); `2g7.6` spike on graph-first construction (rectangular dualization / adjacency-realizing slicing trees); `2g7.7` LLM repair operator at stagnation (generalising the §4.10 compound-operator lesson: fails are semantic and localized, so an LLM proposes the valley-crossing multi-edit; native fitness disposes; plateau-only for cost) and later `2g7.8` AlphaEvolve-style operator synthesis against the existing A/B harness. Prerequisite hygiene: the open scoring-path bugs (`cvw`, `r5a`, `7ua`, `sd3`, `pek`) land first so Phase-9 A/Bs measure a sound objective. Recommended opening moves: `2g7.1`+`2g7.2` (days, and they redefine the target for everything else) in parallel with `2g7.4` (the compute multiplier). ### 37.1 `homemaker-py-2g7.3` hard/soft fail tiering — measured 2026-08-02 **Implementation.** `fitness.classify_fail_tier`/`tier_counts` (fitness.py) classify every fail string emitted across `fitness.py` and `graph.py` into two tiers, raising `ValueError` on anything unrecognised (no silent default) so a new fail-emission site must declare a tier: - **HARD** — no amount of ratio-only optimisation within the current topology can fix it; needs a topology mutation (add/remove/retype/reconnect a node): missing/excess required space (and its "would need … check" cascade placeholders), wrong/required level, level circulation connectivity ("level N not connected", "N inaccessible usable space"), vertical/stair connectivity, adjacency ("not adjacent to"), stairs count, covered-outside support, storey limit/minimum, no outside public access. - **SOFT** — a continuous per-leaf/edge shape or quality metric the inner-loop ratio solve can improve without changing the tree: perpendicular, proportion, size, width, crinkliness, access (grouped with the shape family, not with `graph.py`'s structural adjacency checks, because `evaluate_leaf` computes it identically to proportion/crinkliness — a per-leaf continuous factor thresholded against `FAIL_THRESHOLD` — and `_GRADED_FACTORS` already groups it there), edge-too-long, staircase volume. `driver.Individual` gained `n_hard`/`n_soft` (populated from `innerloop.Result.fail_lines`); `driver.search(use_tiers=True)` swaps the outer comparator from `(-n_fails, fitness)` to `(-n_hard, -n_soft, fitness)`. Default off (`evolve.py --use-tiers` / `HOMEMAKER_USE_TIERS`), so existing runs/reproductions are unaffected. **Guard 1 (§4.5/§4.9 inner-loop 0.5^n cliff protection).** Not re-measured empirically — the change touches neither `innerloop.py` nor the existing `value *= 0.5 ** len(failures)` line in `fitness.py`; tiering only adds pure functions that classify `driver.py`'s already-collected `r.fail_lines` after the fact. The cliff is unaffected by construction. **Guard 2 (§4.9 outer A/B — no scalar-pathology regression).** The tiered key is still a lexicographic tuple, not a blended scalar, so it structurally cannot reproduce the §4.8 pathology (a worse-tier design winning on raw fitness). Encoded as a regression test, `tests/test_driver.py::test_use_tiers_prefers_fewer_hard_over_fewer_total_fails`: constructs a seed (0 hard, 2 soft) vs. a mutated child with FEWER total fails and HIGHER raw fitness but 1 hard fail — the flat comparator picks the child, the tiered comparator keeps the seed. **Acceptance A/B** (`experiments/tier_ab_2g7_3.py`, `URB_NO_OCCLUSION=1`, harbor-house + maple-court, 3 seeds, budget 20 000 native evals/run, `leaf_sharing=True`, `n_workers=4`, ~2h53m wall): | programme | scheme | hard (mean) | soft (mean) | total (mean) | |---------------|--------|-------------|-------------|---------------| | harbor-house | flat | 11.67 | 29.00 | 40.67 | | harbor-house | tiered | **5.33** | 42.33 | 47.67 | | maple-court | flat | 19.33 | 71.33 | 90.67 | | maple-court | tiered | **14.00** | 87.67 | 101.67 | Hard-fail mean strictly improves on both programmes (harbor 11.67→5.33, maple 19.33→14.00) at the cost of more soft fails and a higher raw total — exactly the intended trade: budget stops being spent polishing shape fails while structural fails remain. **ACCEPTANCE: PASS.** Full per-seed log: `scratch/tier_ab_2g7_3/log.txt` (not checked in — regenerate via the script). **Not yet done** (follow-on, not blocking this bead's acceptance criteria): `2g7.2`-style calibration of whether tiered search reaches 0 hard fails faster in wall-clock/eval terms than flat lex at the SAME budget (this A/B measured fail composition at fixed budget, not convergence speed); an apples-to-apples "evals to 0 hard fails" race is a natural follow-up once `2g7.1`/`2g7.2` ground truth lands. ### 37.2 `homemaker-py-2g7.4` shape-curve DP prototype — measured 2026-08-02, ACCEPTANCE: PASS **What was built.** `experiments/shapecurve_spike.py` + `experiments/ validate_shapecurve.py`: an Otten/Stockmeyer-style shape-curve DP answering "does some equal-offset ratio assignment clear the size/width/proportion FAIL_THRESHOLD for every leaf" in one bottom-up pass, for a frozen topology on harbor-house-l0. Each leaf's feasible (width, height) region is bounded by an area hyperbola, a min-width line, and an aspect-ratio wedge — closed-form FAIL_THRESHOLD inversions of `quality_size`/`quality_width`/ `quality_proportion` (`leaf_constraints`, verified against the real Gaussian formulas by construction, not reimplemented magic numbers: same `conf`/ `get_space_params` lookups `fitness.py` uses, including the "any type code starting with 'c' or 's'/'o' hits the circulation/outside branch, not its own programme params" quirk — confirmed this is existing product behaviour, not a bug, by reading `get_space_params`/`quality_size` together). Regions compose bottom-up through the slicing tree: a node's cut ALWAYS sums its two children's contributions into the node's own "w" (`edge0+edge2`) dimension, with "h" (`edge1+edge3`) the shared/cross dimension — a fixed convention of `geometry.py`'s division formula (`coord_a`/`coord_b` always interpolate between edge(0,1) and edge(3,2)), not a per-node choice. The only variable is which of a CHILD's own (w, h) plays which role relative to its parent, an EXACT function of that child's `rotation` parity (`_child_contrib` — see the correction below). Composition runs on a shared log-spaced grid (interval-sum + a numpy-vectorised inversion, `_invert`); leaf curves themselves are exact closed forms, so all discretisation error is confined to internal-node composition. A top-down `realise()` back-substitution converts a feasible root point into actual `division` ratios, so the DP's output is a real, scoreable `.dom` tree, not just a yes/no. **Explicit scope (per the plan's own caveats).** Only size/width/proportion is modelled — crinkliness/adjacency/access/level connectivity are graph terms, out of scope by design. Every quad is approximated by a rectangle with edge-length-derived (w, h) — exact only for a true rectangle/parallelogram (see the rotation-invariance correction below for why this is edge lengths, not a bounding box). `leaf_sharing`/`co_type` target-adjustment is not modelled (harbor-house-l0's programme doesn't exercise either). **Correction 1 (caught in review): bounding-box (w, h) is not rotation-invariant.** The first version measured each quad's (w, h) from its axis-aligned bounding box in global x/y — silently correct only because harbor-house-l0's plot happens to be near-parallel to its own x/y axes (~7.5% bbox-area error, see below). Flagged in review: Urb's Perl ancestor (`Urb::Quad::Straighten`/ `Straighten_Root`) explicitly keeps internal walls mutually orthogonal but NEVER assumes them axis-aligned — `Straighten()` aligns a division parallel/ perpendicular to its PARENT's own division line, not to global x/y, so a real building's walls can legitimately run at any angle (45° tried explicitly below) to the survey/CRS axes the plot's `node:` corners are recorded in. Confirmed by rotating harbor-house-l0's plot 45° about its centroid: bbox area error jumped from 7.5% to **102%** (a rotated square's bbox is up to 2x its true area). Fix: `_dims` measures (w, h) from `(edge0+edge2)/2` and `(edge1+edge3)/2` — the same pairing `geometry.aspect()` already uses — which depends only on the quad's own edge lengths, never on global coordinates. This port's equal-offset division convention already gives the local-orthogonality property Urb's `Straighten()` provides explicitly (no such pass exists or is needed in `operators.py`), so this is a safe substitution, not a new modelling assumption. **Correction 2 (caught in review, and this one REGRESSED accuracy before being fixed properly): which dimension sums is not a matter of degree.** Switching to edge-length (w, h) alone was not sufficient — a first attempt kept the "measure orientation empirically, per node" structure from the bbox version (comparing children's summed dims against the parent's under two hypotheses, picking whichever fit better) and this DROPPED agreement on the untouched harbor-house-l0 benchmark from 99.0% to **95.5%**, with a false negative appearing for the first time (previously zero). Root cause: `geometry.coordinate()` applies a node's OWN `rotation` field even when reading corners it inherited from its parent — a node with odd rotation has its local edge0/edge2 pair correspond to its PARENT's edge1/edge3 pair instead (rotation parity selects between a quad's two possible opposite-edge pairings; `operators.mutate_divide` randomises this on every newly-divided node, so it's common, not an edge case). This is not something to measure and approximate — it's an exact algebraic identity: verified numerically (float-exact, `29.533730484465025 == 29.533730484465025`) that `left.w + right.h == parent.w` whenever `left.rotation` is even and `right.rotation` is odd, independent of skew or global orientation. `_child_contrib(curve, rotation)` applies this directly (`curve.w_of_h` for even rotation, `curve.h_of_w` for odd) — no geometry measurement, no baseline-ratio pass, no heuristic threshold, and the empirical `_orientation`/ `annotate_orientations` machinery from both prior versions was deleted entirely (simpler code, not just more correct). **Validation** (`experiments/validate_shapecurve.py`, harbor-house-l0, 200 `driver.random_topology` topologies, 2-14 leaves, seed 12345): compared against NM search **minimising shape-fail count directly** (`ShapeFailEvaluator`, budget 100), not `innerloop.optimise`'s full aggregate objective — an earlier version of this harness used the full objective and found spurious "disagreements" where the DP's own realised point independently verified at **zero** shape fails but NM's full-objective search had wandered away from it, because on a topology missing most of its programme, the 0.5^n missing-space penalty swamps the objective and NM has no pressure to preserve shape-feasibility specifically. Minimising shape-fail count alone is the correct apples-to-apples comparison against what the DP claims to solve. | metric | harbor-house-l0 (unrotated) | harbor-house-l0 rotated 45° | |---|---|---| | agreement | 198/200 = **99.0%** (target >= 95%) | 100/100 = **100.0%** | | false positives (DP feasible, NM can't reach 0) | 2 | 0 | | false negatives (DP infeasible, NM reaches 0 anyway) | **0** | 0 | | speedup (grid_n=150, vs 100-eval NM) | **97.2x** (target >= 50x) | 97.1x | | plot-level (w,h)-approximation area error | **+7.5%** (bbox, pre-fix) / ~0.1% (edge-length, post-fix) | 102% (bbox, pre-fix) / ~0.1% (edge-length, post-fix) | The unrotated-plot numbers are BACK to matching the original (pre-Correction-2) 99.0%/0-false-negative result exactly — same 2 mismatches, same seeds (`623465425`/`1523713848`) — confirming Correction 2 fixed the regression it introduced without disturbing the genuine, separately-diagnosed residual error below. The 45°-rotated run (`python experiments/validate_shapecurve.py 100 100 150 45` -- same protocol, `n=100` for wall-clock, the plot's `node:` corners rotated 45° about their centroid into a scratch copy via `rotated_plot_dir`) is the direct, reproducible test of the concern that motivated Correction 1: 100% agreement, confirming the fix generalises and isn't overfit to harbor-house-l0's near-axis-aligned plot. Zero false negatives in both: the DP never wrongly rejects a topology NM finds feasible — the safe direction for a pre-filter (worst case it fails to prune, never wrongly prunes a viable topology). `_invert`'s pure-Python O(N²) double loop was ~70% of DP wall-clock before vectorising with numpy (profiled: 170ms → 40ms/topology at grid_n=300 from that change alone; grid_n=150 is the shipped default, no measured accuracy cost vs. 300 on this benchmark). **Remaining approximation error, root-caused (unchanged by Corrections 1/2 — a different, smaller error source).** Both unrotated false positives trace to the rectangle-vs-true-skewed-quad approximation itself (§37.2's plan-flagged "equal-offset skew-quad geometry" caveat), not to global rotation or to composition: the DP's own realised point for both cases had one leaf whose edge-length-approximated area was comfortably inside its feasible bound, but whose true `geometry.area` (a real, slightly non-parallelogram quad) fell just below the true lower bound — an ~8-12% approximation gap, the same magnitude as harbor-house-l0's own plot-level residual skew. This is a strictly smaller, already-anticipated error source, distinct from the two corrections above (which were about measuring w/h and composing them correctly, not about the rectangle-vs-skew-quad approximation itself). **ACCEPTANCE: PASS** — all three criteria cleared (agreement, speedup, quantified approximation error), on both the original and the rotated plot. **Not done in this session** (follow-on, new bead needed before this can replace `operators.predicted_shape_fails` in `driver.py`'s real pre-filter path): wiring the DP into `driver._evaluate`/`innerloop.optimise` as an actual pre-filter + NM warm-start, multi-storey (`below`-link) support, `leaf_sharing`/`co_type` modelling, and a true skew-quad (non-rectangle) leaf region to remove the remaining ~8-12% approximation-error source rather than just quantify it. `experiments/shapecurve_spike.py` is kept as a reference/ prototype (the §34 `autodiff_spike.py` precedent), not wired into `innerloop.py`.