Interior-O courtyard seeding option

_assign_adjacency_aware (operators.py:528) currently places the single O leaf on the MOST PERIPHERAL leaf, where adjacent rooms already have facade. For dense floors (harbor-house ~19 rooms/floor) this wastes the daylight source. Add an option to seed O INTERIOR (as a light well) and to scale O-leaf count with room count, so landlocked rooms get an adjacent uncovered-outside neighbour by construction -> fewer crinkliness fails in the seed. A/B against current peripheral placement.

Construction lever (high prior under erc), sibling of erc.3 (leaf-sharing) and erc.4 (plot-filling). Directly attacks the crinkliness residual (erc decomposition: crinkliness 346 on maple): landlocked rooms get an adjacent uncovered-O light well by construction. Follow erc shared protocol (A/B maple+harbor seeds 0/1/2, 20k evals, control reproduces 136.0/74.0, DESIGN.md §13.x).


Experiment: depth-balanced / giant-splitting construction (re-scoped by Diag B)

Attacks the #2 factor (size/undersize 242) via the §12.3 paradox: rooms are undersize while 56% of the plot is empty. The shape floor is computed at TARGET dims, so construction never spends the slack. Scale leaves up to consume available plot area (proportionally, preserving target aspect) so rooms reach/exceed target — bigger leaves are also easier to keep compact, so this may help crinkliness/width too.

Builds on leu.2 (proportion-aware splits sized FROM target dims) by adding a fill step that scales the whole layout (or per-region) to the plot envelope instead of leaving slack as empty plot. Implementation in operators construction / _size_divisions_from_targets.

NOTE: exact fix-site (construction vs inner loop) is decided by Diagnostic B — if B shows leaves park at target with unused plot, this construction lever is correct; if B shows the inner loop simply lacks an expansion gradient, prefer the inner-loop slack-expansion sibling instead. A/B vs §12.2 baseline, seeds 0/1/2, 20000 evals, staged, default-OFF. Record DESIGN.md §13.4.

RE-SCOPED by Diagnostic B (§13.2). Original premise (rooms parked at target, scale leaves up into 56%-empty plot) is FALSIFIED: sized rooms already hold 1.4-1.5x aggregate target area; the empty-looking plot is ~46% circulation, not claimable void. Real defect: MALDISTRIBUTION by slicing position — same type/target leaf lands 0.05x..14.7x by binary-tree depth; inner loop cannot fix (frozen topology). NEW SCOPE: construction that balances tree DEPTH so equal-target rooms land at comparable depth and/or splits/caps giant leaves so area tracks target. NOT a uniform scale-to-envelope (that would just inflate the giants further). A/B vs §12.2 baseline, seeds 0/1/2, 20000 evals, staged, default-OFF. Record DESIGN.md §13.4. Synergy with erc.3 (leaf-sharing for the starved tail).


Experiment: leaf-sharing / multi-room leaves in construction

Strongest untried construction lever. §12.3 named 'merge or share leaves across same-class rooms' but c3g never tested it — c3g only coarsened the circulation spine (circ_divisor), trading shape gains for equal access/adjacency damage (null). Leaf-sharing is DIFFERENT: it reduces leaf count by collapsing same-class rooms (e.g. several O/storage, or same-type repeated rooms) into a shared leaf, attacking crinkliness(346)+size(242) directly WITHOUT coarsening circulation — so it should dodge the access penalty that sank c3g.

Implementation sketch: in operators.constructive_topology (+ lift path), allow rooms of the same class/type (and compatible adjacency) to be instantiated as one larger leaf rather than one-leaf-per-room, lowering leaves-per-room from ~1.4 toward 1.0 or below. Honour storey_minimum and required-room presence (a shared leaf must still satisfy each merged room's presence/area in the fitness check, or the merge must be limited to rooms the fitness treats as fungible).

Tests the deepest open question: whether 52 rooms simply cannot be well-shaped as 52 leaves at this density. A/B vs §12.2 baseline (maple 136.0, harbor 74.0), seeds 0/1/2, 20000 evals, staged; default-OFF toggle so controls reproduce. Record DESIGN.md §13.3.

A/B DONE (§13.3): staged 20k, seeds 0/1/2, factor 3. maple 137.0→86.3 (−37%), harbor 74.0→50.3 (−32%). Baseline arm reproduces §12.2 exactly (maple 137 vs 136, harbor 74.0 vs 74.0). Total separation: every share run beats every baseline run same-programme. ~35% faster (fewer leaves). First Phase-8 floor-mover; 5th construction/seed win. Closing.


Diagnostic B: undersize-despite-slack localization (construction-target vs inner-loop-fill)

GATES the plot-fill-construction vs inner-loop-expansion decision. The paradox from §12.3: plot utilisation is 0.44 (56% empty) yet size fails are 242 (rooms UNDERSIZE). Where is the slack stranded, and at which stage should it be spent?

Measure, on constructive seeds for maple-court + harbor (seeds 0/1/2):
1. After CONSTRUCTION (before inner loop): per-leaf achieved area vs target area, and total occupied vs plot area. Are leaves parked at target with the slack left as unused plot, or is the slack distributed but mis-shaped?
2. After the INNER LOOP optimises ratios: did size fails drop — i.e. does the ratio solve already expand leaves into slack, or does it have no gradient/incentive to exceed target? Compare predicted_shape_fails (target geometry) vs achieved size fails (post-optimise).

The §12.3 calibration (floor at TARGET dims ≈ achieved) already hints the inner loop is NOT filling slack — confirm and quantify, and identify whether the gap is (a) construction targets too-small dims given the plot, or (b) the objective gives no reward for exceeding target area. Output: DESIGN.md §13.2.

DECISION RULE: if rooms are parked at target with unused plot → fix in CONSTRUCTION (plot-fill, erc child). If the inner loop has the room to expand but no objective gradient → fix in the INNER LOOP (slack-expansion term, erc child). Reads only; no behaviour change.

VERDICT (DESIGN.md §13.2): the '56% empty plot' is a misreading. Sized rooms already occupy ~50-54% of plot and hold 1.4-1.5x their aggregate target area (util>tgtFill); ~46% of plot is CIRCULATION, not claimable void (out only 3-4%). Size fails are pure MALDISTRIBUTION set by SLICING POSITION: median room at target (a/t~1.0) but long undersize tail (p25~0.35, min 0.05) starves while a few giants balloon (max 6.8x harbor, 14.7x maple). Same type/target lands at BOTH extremes (harbor r t=10: 68m2 & 2.3m2; maple n t=60: ~target & 2.7m2) => area dictated by binary-tree depth, not target. Inner loop CANNOT repair it: budget-80 size fails move only -1.6/-3.7, %undersize flat-to-worse; frozen-topology ratio DOF + 0.5^n cliff + symmetric size gaussian. => FALSIFIES plot-fill-as-claim-void (re-scope erc.4 to depth-balanced/giant-splitting construction), DEPRIORITISE erc.6 (wrong DOF). Reinforces erc.3 leaf-sharing for the starved tail. Script: experiments/diag_slack_localization.py


Diagnostic A: per-leaf shape-fail vs density/granularity profile

GATES the leaf-sharing vs compactness-cuts decision. The open question from §12.3: is the shape floor intrinsic to slicing at this leaf density (→ fewer leaves is the only lever), or fixable by better-shaped cuts at the same leaf count?

Measure: per-leaf shape-fail rate (crinkliness/size/proportion/width, broken out) as a function of leaves-per-room and plot utilisation, across the existing programmes spanning density — harbor (16 rooms) vs maple-court (52 rooms) — and, if cheap, a synthetic sweep that holds the programme fixed while varying leaf count (e.g. reuse the circ_divisor / construction granularity knob already in place to generate coarser vs finer constructive seeds and score predicted_shape_fails per leaf).

Reads, does not change behaviour: use operators.predicted_shape_fails + the per-leaf factor breakdown already in fitness.py (the §12.3 residual table was produced this way). Output: a table of per-leaf shape-fail vs density, written into DESIGN.md §13.1.

DECISION RULE (write it into the verdict): if per-leaf shape-fail is FLAT across densities → floor is intrinsic to slicing density → prioritise leaf-sharing (erc child), deprioritise/close compactness-cuts. If it RISES with density → better cuts can pay → keep compactness-cuts. This is a measurement, not an experiment; no A/B, no baseline reproduction needed.

VERDICT (DESIGN.md §13.1): per-leaf shape-fail is FLAT vs slicing density in the controlled synthetic sweep (maple-court, room set fixed, circ_divisor 2->9: leaves 81->63, per-leaf rate 1.72-1.94 with no trend; TOTAL shape fails track leaf count ~linearly 139->116). Crinkliness dominates (~0.8/leaf) and is flat. Cuts already squarest (_size_divisions_from_targets) yet still ~1.8 fails/leaf => little compactness headroom at fixed count. Floor is INTRINSIC to per-leaf slicing. => prioritise leaf-sharing (erc.3), deprioritise compactness-cuts (erc.5). NOT the c3g null: that removed circulation leaves (access damage cancelled gain); leaf-sharing removes ROOM-leaf count without touching the spine. Script: experiments/diag_leaf_shapefail.py


Phase 8: lower the geometry/shape floor — construction & inner-loop levers

Continuation of Phase 7 (leu, closed). Phase 7's decisive finding (§12.3 calibration): predicted_shape_fails at the best achievable geometry ≈ the achieved total fail count (maple floor 121-163 vs achieved 126-148). Therefore SEARCH MACHINERY CANNOT HELP — there is no lower-fail basin for the constructed topologies to reach; the floor IS the result. Scoreboard: 4/4 wins from construction/seed quality (§11.2, §11.6, §11.7, §12.2), 0/3 from search machinery (§11.4, §11.5, §12.3). The only way to lower fails is to lower the geometry FLOOR.

Residual decomposition (maple-court, 6 constructive seeds, §12.3): crinkliness 346 + size 242 (undersize) + proportion 121 + width 102, with plot utilisation only 0.44 (56% of plot empty) yet rooms UNDERSIZE. Diagnosed mechanism: over-granular construction — 73 leaves for 52 rooms — every leaf high perimeter/area (crinkliness) and below target area (size). c3g tested ONE granularity lever (circulation-spine coarsening via circ_divisor) → null (shape gain cancelled by equal access/adjacency damage). The other named levers were never tested.

This epic runs DIAGNOSTICS FIRST to decide which floor-lowering lever to invest in, then the construction/inner-loop experiments in dependency order. Tier-3 search-machinery bets (island model psk, tournament pressure 6zy) are tracked but LOW prior — do not invest there until something moves the floor.

Shared protocol (every experiment): A/B on maple-court + harbor, seeds 0/1/2, 20000 evals, staged; controls MUST reproduce the §12.2 baseline (maple 136.0, harbor 74.0); record verdict in DESIGN.md (new §13.x). Same discipline as every lever in §11-§12.


Larger-than-house benchmark programme (>16 rooms) + baseline

PREREQUISITE for the whole epic. Harbor (16 rooms) is the biggest real programme in examples/; 9gp's scaling claim ('>16 rooms') and acceptance criterion ('larger-than-house programme') cannot be measured without a bigger one.

Build a reproducible benchmark programme larger than harbor (target ~24-32 rooms, multi-storey, with a realistic per-level required-room partition and adjacency-to-c load like harbor's). Provide its patterns.config / costs.config (reuse config inheritance, homemaker-py-n5k) and an init.dom, mirroring the examples/harbor-house layout. Wire it into the existing experiment harnesses (run_search_scaled.py / run_staged_search.py) and record a BASELINE total-fail count at a fixed budget for the current default search (adjacency-aware seeding + staged), exactly as §11.6/§11.7 reported harbor. This baseline is the yardstick proportion-seeding and 9gp are measured against.

Deliverable: examples/<new>/ with configs+init.dom, a documented baseline (seeds 0-2, total fails at budget), recorded in DESIGN.md §12.1 + bead notes.

A >16-room multi-storey benchmark exists under examples/, runs through the current harness, and has a documented baseline fail count (>=3 seeds) recorded in DESIGN.md.

Benchmark delivered: examples/maple-court/ (26 entries / 52 rooms / 3 storeys, ~1015 m2 internal, ~790 m2/floor plot). Mirrors harbor's adjacency-to-c load + secondary adjacencies; room codes avoid generic c/o/s leading letters. Baseline (staged adjacency-aware, URB_NO_OCCLUSION=1, 20000 evals): seed0=145, seed1=158, seed2=152, mean=151.7 fails. All native re-score OK. Best (145, seed0) saved as generated.dom. Recorded in DESIGN.md §12.1.


Phase 7: scaling validation & residual reduction (post-c4c)

Continuation of the closed c4c epic (Phase 6). Phase 6 evidence is decisive about WHERE leverage lives: the two search-machinery experiments (§11.4 graded high-fail objective, §11.5 niching+restarts) BOTH landed negative and BOTH concluded the high-fail plateau is a REACHABILITY problem (operators+encoding cannot reach low-fail basins), not population-management or objective-shaping. The two wins (§11.6 adjacency-aware seeding, §11.7 adjacency-aware lift) came from CONSTRUCTION/SEED quality. Harbor is now ~85 fails (best 78), down from the 95/105 plateaus; the residual is geometry/shape-bound (size/proportion/crinkliness).

Three gaps block further scaling progress and must be done in order:
1. There is NO larger-than-house benchmark. Harbor (16 rooms) is the biggest real programme in examples/. 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.
2. Proportion-aware seeding: §11.6 noted the seed uses 0.5 splits -> 'more, smaller leaves' -> geometry fails. Sizing splits from target dims attacks the §11.7 geometry residual directly, in the proven construction direction; cheaper than an encoding rewrite.
3. 9gp (canonical Polish encoding) must be RE-SCOPED: its 'topology signature for niching' justification is dead (§11.5 falsified niching; genome.signature already exists as the cheap stand-in). The surviving, evidence-supported parts are M1/M2/M3 Wong-Liu moves (reachability) and shape-feasibility pruning (residual + inner-loop budget = scaling).

Ordering rationale: benchmark first (makes scaling measurable for everything downstream), then the cheap proven-direction seeding win (sets the strongest baseline), then the re-scoped canonical-encoding capstone (lands on the best seed, with a benchmark to prove its scaling claim).

Do NOT build search/selection machinery on unmeasured premises — that is exactly what §11.4/§11.5 did and both regressed. Every child lands an experiment with results recorded in DESIGN.md §12.x + bead notes, same discipline as Phase 6. The benchmark child is the root dependency; proportion-seeding depends on it (so the win is measured at scale too); re-scoped 9gp depends on both (best baseline + scaling measurement).

(1) A reproducible >16-room benchmark exists with a documented baseline fail count; (2) proportion-aware seeding shows a measured fail reduction on harbor AND the new benchmark; (3) re-scoped 9gp lands M1/M2/M3 + shape feasibility and shows measured search improvement on the larger-than-house benchmark.

EPIC COMPLETE. leu.1 established the >16-room maple-court benchmark (baseline 151.7→ leu.2 136.0). leu.2 proportion-aware seeding: measured win on both larger programmes (harbor -13%, maple -10%), default-on. 9gp (re-scoped): M3 reassociate + shape-feasibility filter landed + measured NEGATIVE — the residual is the geometry/shape floor of the constructed layouts, not reachability/feasibility-bound. Net: Phase 7 reduced the benchmark residual via construction (leu.2) and validated that further search-machinery gains are unavailable (9gp), a 3rd search-machinery negative vs 4 construction wins. See DESIGN.md §12.


Staged per-floor search (curriculum: credible base floor, then upper floors as deltas)

Search the genome in its causal dependency order. The base-floor tree is the master; upper storeys are deltas (Below-inheritance). The programme partitions rooms by required level (harbor: 10 L0, 4 L1, 2 free), so each floor's target room set is known up front. Today the search discovers both floors simultaneously via random typing + the rare/drastic level_add (weighted 0.2) — an uncontrolled, degenerate version of staging.
Stage 1 — base floor: search the single-storey tree over the level-0 room set, dimensionality reduced (one tree, no deltas).
Stage 2 — upper floors as deltas: seed each upper storey with ITS required room set (via the construction op, homemaker-py-c4c.2), search the deltas; keep the base MUTABLE at low probability so it can adapt to upper-floor pressure.
CRITICAL non-goal: do NOT hard-freeze the base. A base optimised purely as ground floor is a §4.2-style partial objective and can be a bad SUBSTRATE. Stage 1 objective must include (a) a reserved, vertically-alignable circulation core and (b) a substrate-readiness term: enough divisible area/cut structure to host the level-1 room set later.

Premise gated by homemaker-py-c4c.1: only high-value if single-storey construction already reaches low fails. Substrate-readiness proxy candidates: count of base leaves large enough to subdivide for L1 rooms; presence of a core node with vertical continuity. Stage transition: when stage-1 base hits a fails/score threshold or budget fraction, freeze-soft and open the delta genome. Composes with canonical encoding (homemaker-py-9gp) — deltas are where redundancy/coarse moves hurt most.

Staged search beats single-stage on harbor-house (best fails/score), measured at equal native-fitness budget and recorded in DESIGN.md §11.x + bead notes. Reserved-core + substrate-readiness shown to prevent the bungalow trap (stage-2 does not have to carve a core from scratch — track core-carving moves). No regression on programme-house.

DONE. Implemented driver.search_staged (Stage 1 single-storey base over level-0 set with substrate-readiness ranking bonus; Stage 2 upper floors lifted as constructed deltas, base mutable at base_p=0.15). New: programme.{n_storeys_required,partition_rooms_by_storey,write_stage1_programme}, graph.substrate_readiness, operators.{lift_base_to_storeys,_pick_weighted_by_storey,base_p}, driver.search rank_bonus_fn/seed_factory/base_p hooks, experiments/run_staged_search.py, tests/test_staging.py. RESULT (harbor, 20000 evals, seed 0): staged 95 fails vs single-stage 105 (-10, -9.5%); gain in crinkliness 27->18 + edge 12->8, small access cost +5. Anti-bungalow CONFIRMED: all Stage-2 core_divide/undivide in winning lineage are noops (core inherited, not carved). Regression PASS: programme-house warmstart-2f4 still reaches whole-pop 1-fail. DESIGN.md §11.3 filled. Remaining high-fail plateau is §11.4 (graded objective) territory.


Programme-aware construction + missing-room repair operator

Highest-leverage fix for the epic's diagnosis. Today mutate_divide (operators.py:71) types new leaves at RANDOM, so required programme spaces go missing -> 'missing' stacking dominates fitness on full programmes (harbor: 6 missing-room records stacking critical+size+width+adjacency+level). Make the required room set a constructive invariant rather than something the search must stumble onto.
Two parts:
1. Constructive seeder: generate initial topologies that instantiate each required space (respecting count/level/type) by construction, instead of random divide+retype chains.
2. Repair operator mutate_place_missing: detect a required-but-absent space and insert it (divide a compatible leaf, type the new leaf to the missing code, prefer a slot satisfying its adjacency). Complements mutate_level_compound_fix (which repairs level, not presence).
Wire the seeder into driver bootstrap and the repair op into mutate() weights.

Seeder must place generic C (circulation/core) and O (outside) too, not just programme codes. Keep it stochastic (diverse population) but biased to cover the required set + correct levels. Repair op should be lex-safe: prefer insertions that don't create more new fails than the missing-stack it removes (cf. the §4.10 deceptive-valley lesson — a naive insert dumps a room into a bad slot and nets worse).

On harbor-house, 'missing'-type failures collapse to ~0 across the population (record before/after fail histograms); measured net-fail improvement vs current 74-fail out1.dom baseline, recorded in DESIGN.md §11.x + bead notes. No regression on seeded programme-house (still reaches 1-fail optimum, §4.10).

DONE 2026-06-17. Implemented constructive_topology seeder + mutate_place_missing repair op (operators.py), wired into driver bootstrap + mutate weights. A/B on harbor (20k evals, seed 0, identical config): old random-bootstrap 133 fails (103 missing, 77%) -> new constructive 105 fails (12 missing, 11%); missing-records 22->2; -21% total. Seed head-start 163->139. §4.10 regression PASS: warmstart-2f4 still reaches 1-fail population at 50k. Verdict: construction necessary, reframes bottleneck to quality-fail packing (crinkliness/size/access/edge) of complete dense design -> unblocks §11.3 staging, motivates §11.4 graded objective. Follow-up filed: adjacency-aware seeding. Full numbers in DESIGN.md §11.2. 186 tests pass.


Experiment: single-storey harbor premise test (per-floor construction vs multi-storey coupling)

De-risk the staged-search and construction work BEFORE building either. Strip harbor-house to its 10 level-0 rooms as a single-storey programme; run the current memetic search from a bare plot; record best fails/score and the fail-type histogram. This isolates the question: is the bottleneck per-floor CONSTRUCTION (placing the right room set on one floor) or the multi-storey COUPLING (deltas, core alignment, level constraints)?
- If single-storey 10-room reaches near-zero fails: the difficulty is coupling -> staged per-floor search (homemaker-py-<staging>) is the high-value lever.
- If it still stalls at many fails (esp. 'missing'): per-floor construction itself is the bottleneck -> programme-aware construction (homemaker-py-<construction>) is required first and staging alone won't rescue it.
Run from blank-slate (init.dom equivalent) AND from a bootstrap population; report both.

Build examples/harbor-house-l0/ from harbor's level-0 spaces only (drop level: keys or set all to 0; keep adjacency among the retained codes). Reuse experiments/run_search_scaled.py harness. Cheap (~minutes at native-fitness throughput).

Single-storey 10-room harbor variant created and committed under examples/; current search run and best fails/score + fail histogram recorded in DESIGN.md (new §11.x) and bead notes; explicit verdict on construction-vs-coupling.

VERDICT: per-floor CONSTRUCTION is the bottleneck, not multi-storey coupling.
Built examples/harbor-house-l0/ (10 explicit level:0 codes = 13 room instances, single-storey constraints), seeded from bare init.dom.
Run: 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 evals / 250 topologies / 234s. Best 33 fails (fitness 2.25e-12, deep in 0.5^n regime); whole pop stuck 33-35. 40->33 over full budget. NOT near zero.
Fail histogram: 13 missing (all 3 m meeting rooms never built) + 6 adjacency + 4 access + 4 size + 2 edge-too-long + 2 crinkliness + 1 proportion + 1 too-few-stairs(single-storey artifact). Missing = 39% — matches the 'still stalls esp. missing' branch.
=> c4c.2 (programme-aware construction + missing-room repair) is the prerequisite; staging (c4c.3) alone won't rescue it. c4c.3 already correctly depends on both. Full writeup in DESIGN.md §11.1.


Phase 6: topology-search quality for full/multi-storey programmes

Diagnosis (survey 2026-06-17): the delivered speedups (native fitness ~140x, geometry inner loop ~1.6x) landed in the two layers that were never the bottleneck. The geometry inner loop polishes WITHIN a failure tier (DESIGN.md §4.5/§4.7: 0 fail changes, by design — the 0.5^n cliff protects it). But final design quality is dominated by FAILURE COUNT, which is almost entirely a topology property. Topology search on full programmes is the weakness:
- blank-slate programme-house (init.dom): memetic stalls at 18 fails vs urb-evolve 6 (§7 Phase 2 verdict);
- harbor-house (16 rooms): out1.dom=74 fails, generated.dom=130 fails, both at ~machine-epsilon score; fails dominated by 'missing' room stacking (each missing room stacks critical+size+width+adjacency+level, §6).
Smoking gun: operators.mutate_divide (operators.py:71) assigns each new leaf a RANDOM type from programme-codes+C+O. Nothing guarantees the required programme spaces are instantiated, so on a large programme required rooms go missing -> catastrophic 0.5^n stacking, and the search is a random walk over type assignments with a flat/catastrophic gradient in the high-fail regime.
This epic groups the topology-search-quality work: programme-aware construction, staged per-floor search, graded high-fail objective, topology diversity, then the canonical-encoding capstone. Everything experiment-driven; results recorded in DESIGN.md sections + bead notes.

Causal frame: 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 required-room sets known per floor 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 ground floor can be a bad SUBSTRATE: vertical core must stay aligned, load-bearing walls must stack). Curriculum, not freeze.

Memetic search reaches a competitive low-fail design on harbor-house (16 rooms, multi-storey) and on blank-slate programme-house, beating the current 74/18-fail plateaus; each child bead lands its experiment with results recorded in DESIGN.md.


Python native fitness evaluation (port urb-fitness.pl)

We need a Python implementation of the urb-fitness scoring tool that is faithful to the Perl oracle (urb-fitness.pl / ProgrammeDriven.pm). This is the 'native fitness' component identified in DESIGN.md §6 as gating topology search at scale — the oracle requires a subprocess+file roundtrip per eval which is too slow for large populations.

The native fitness must reproduce all scoring terms from the Perl source:
- size, width, proportion (per-space Gaussian scoring)
- 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.

Validation target: match oracle scores on the programme-house corpus (35+ .dom files) to within the ~3.7% gap documented in homemaker-py-gpx.


Investigate cf0b8a77e8b2325f ~18% raw_value discrepancy (py lower than oracle)

For prefix cf0b8a77e8b2325f: oracle=1.079112e-03 py=9.133243e-04 ratio=0.8464 (python is ~18% too low). debug_nfails shows py n_fails=5 oracle n_fails=5 (same failures), stair_fits=[1.3145] in python, building_factor=0.1104 (vs oracle's implied ~0.1303). The discrepancy is in raw_value (py=11837 vs oracle implied ~13975) or possibly building_factor. Need to check: (1) per-leaf quality values (crinkliness, area_outside, access) via debug_quality.txt; (2) whether the stair corners differ (cf/rl: py=[2,3] perl=[2,3] — SAME, so corners ok); (3) any quality term not yet ported or computed differently. Run debug_quality.py and compare per-leaf contributions.

Investigation complete: traced 18% discrepancy (cf0b8a77) through entrance corner logic and weighted path length bugs, both now fixed in w1e.


Port Perl entrance-corner logic into Python stair-fit (ca/cb parity)

Perl's check_stair_fit (Leaf.pm:104-142) adds entrance edge corners to corners_in_use before computing stair_fit. Python's process_storey does not. For ca9e80c5c1502f10 and cb93a2d2de7f5d37 the oracle stair leaf 'lr' has corners [1,2,3] (from perl_bf.pl) but debug_corners.py Perl-compatible trace gives [2,3] — the extra corner 1 comes from Entrances(graph)->Boundary_Id logic. Need to: (1) port dom.Entrances() — returns {leaf_id: boundary_id} for the best-entrance leaf at ground level (Entrances() returns {} if level>0); (2) port leaf.Boundary_Id(side) — returns the node sharing that edge; (3) in fitness.process_storey, after stack_corners_in_use, add entrance-edge corners before computing stair_fit. Acceptance: ca/cb ratio ≈ 1.0 (currently 1.33).

Fixed: entrance corner logic via _entrance_bid_for_stair (mirrors Perl Entrances), plus root cause: _avg_path_len_from now uses weighted Dijkstra (centroid distances) matching Perl graph.average_path_length — fixes has_circulation edge removal order. All 4 debug prefixes ratio=1.000, 39 tests pass.


Fix corners_in_use _ib(None,None) bug: triple-at-idx=3 always passes in Perl

In graph.py corners_in_use(), the _ib helper returns False when pa=None or pb=None. Perl's is_between_2d(point, undef, undef) returns True (distance_2d(undef,x)=0 so abs(0-0-0)<1e-6). At triple-check idx=3, c1=corners[4]=None and c2=None, so _ib(w, None, None) must return True to match Perl — meaning the triple always succeeds at idx=3. Fix: add 'if pa is None and pb is None: return True' before the existing 'if pa is None or pb is None: return False'. This is already applied to graph.py. Needs: run 35-file corpus parity test to confirm aa0dcab98927d2c9 passes (corners [0,1,3] → stair_fit=0.878 → sf_factor≈0.570 ≈ oracle).

Fixed as part of homemaker-py-w1e: the _avg_path_len_from weighted Dijkstra fix corrects has_circulation edge removal ordering, which was the actual cause of wrong stack corner counts. The _ib(None,None)=True fix was already in place but the weighted path length was the remaining blocker.


Disable occlusion/daylight in Urb oracle (env flag); re-baseline scores

Strategy decision (Bruno, 2026-06-12): occlusion/daylight is orthogonal to whether a better, scalable optimisation system can be built — disable it in Urb rather than port it. Patch Urb behind an env flag (e.g. URB_NO_OCCLUSION=1): quality_daylight returns 1 for outdoor spaces too, and Crinkliness/Area_Outside pins the CIEsky_vertical illumination factor to 1 (simple crinkliness = unweighted external wall area / floor area). Keep the occlusion object plumbing — it carries the Walls/boundaries cache crinkliness needs (ProgrammeDriven.pm:97). Then re-baseline everything once at this clean boundary: corpus .score files, the DESIGN.md $4.5 gains table, accept_innerloop.py gate bars. Also measure oracle s/dom with the flag on — occlusion sampling may be a real slice of the ~1 s/dom cost. The native Python fitness then ships with simple crinkliness only; full occlusion rebuild is deferred post-Phase-5 (homemaker-py-2g5).

Env-flagged Urb patch; flag on: corpus re-scored, gate bars re-derived, oracle s/dom re-measured; urb-evolve confirmed to respect the flag for the Phase-2 benchmark

URB_NO_OCCLUSION=1 patch in Urb (Leaf.pm quality_daylight -> 1, Dom.pm Area_Outside illumination pinned; flag-off byte-identical, verified). Corpus re-baselined: 35/35 scores shift, one expected crinkliness failure-set change, 0.92 s/dom batched (x1.08). New reference gains recorded in DESIGN §4.7 and accept_innerloop bars (x1.63/x1.70/x1.68, deterministic seed). urb-evolve respects flag by construction. NOTE: Urb working-tree changes left uncommitted in /home/bruno/src/urb for Bruno's review.


Native fitness validation: 35-file corpus parity vs oracle; retire oracle (Phase 3 gate)

DESIGN.md §7 Phase 3 gate. Validate the assembled native fitness against urb-fitness.pl across all 35 programme-house .dom files: scores within float tolerance AND identical failure sets. Swap behind the same interface as oracle.score so inner loop and search driver are unchanged; keep the oracle available as validation reference but stop using it in search. Then re-run topology search at scale (separate issue).

35/35 files: score parity within tolerance, failure sets identical; search runs end-to-end on native fitness with measured speedup vs oracle

35/35 score parity + fail-set parity; NativeEvaluator added; optimise() defaults to use_native=True; 23x speedup; one bug fix (_entrance_bid_for_stair via-outdoor case)


Native fitness: storey/building checks + missing-space failure stacking

DESIGN.md §6. Port ProgrammeDriven/Storey/Building checks: space-count matching with MISSING-SPACE FAILURE STACKING (2 base failures + 1 per size/width/proportion/adjacency/level requirement, up to ~7 — ProgrammeDriven.pm:192-212; reshaping must preserve this hierarchy), adjacency/level/requires_below checks, staircase fit/volume/min-max, public access, circulation & outside ratios, min internal area (1.2x programme sum), storey limit/minimum, structural failures (edge too long >8 m both variants, unsupported covered outside, covered outside above ground, level not connected, inaccessible usable space), preprocess_building s->O conversion, and the 0.5^n penalty over value/cost.

Failure sets and final scores match the oracle on sample files; failure-stacking counts identical

Implementation complete: storey/building checks, failure stacking, staircase logic, public access, circulation ratios, structural checks all ported. 39 tests pass.


Native fitness: leaf quality terms + cost model

DESIGN.md §6. Port Leaf.pm quality terms (size, width, proportion, perpendicular, access) with programme-driven parameter lookup (get_space_params fallback chain, generic c/o/s handling, width_inside [4.0,1.0] default), gaussian scoring, FAIL_THRESHOLD=0.1. Also the COST DENOMINATOR — fitness is value/cost: per-leaf area costs, interior/exterior wall edge costs, boundary costs, value rates (Leaf.pm:194-251, Storey.pm:122-147). Cost couples to geometry too.

Per-leaf quality factors and per-storey cost/value match Perl (float tolerance) on sample corpus files with DEBUG output diffed

Crinkliness scope (2026-06-12): port SIMPLE crinkliness only — external wall area / floor area with the CIEsky illumination factor pinned to 1 (boundary-overlap geometry from Dom->Walls stays in scope; the sky model does not). Must match the URB_NO_OCCLUSION-flagged oracle (homemaker-py-gp2), not stock Urb. quality_daylight = 1 for all spaces.

0-mismatch parity: 35 files, 407 leaves, 2849 factors


Native fitness: adjacency/connectivity graph build + Merge_Divided semantics

DESIGN.md §6 port scope, §7 Phase 3 (native fitness gates topology search at scale — §4.6). Port the door_width (1.2 m) adjacency graph (Urb Dom Graph), Merge_Divided, and the TWO-PHASE build: adjacency/level/vertical checks run on the UNMERGED tree, graphs rebuilt after Merge_Divided for storey processing (ProgrammeDriven.pm:83-103). Port faithfully — including has_vertical_connection's no-spatial-overlap stub (ProgrammeDriven.pm:399-423) unless the fidelity decision (§8.1) says otherwise; record the decision.

Graph edges/widths and merged structure match Perl on the 35-file corpus; vertical-connectivity fidelity decision recorded

Graph edges/widths match Perl on all 35 corpus files (2 bugs fixed: empty-string boundary excluded by Python 'in' operator substring check, and upper-storey rotation not delegating to below-link). Merge_Divided ported. Vertical-connectivity fidelity decision recorded in graph.py module docstring (faithful stub, no spatial overlap). Tests in test_graph.py.


Geometry inner loop: full-objective equal-offset ratio optimiser

DESIGN.md §5.1, §7 Phase 1. Productionise experiments/optimize_fullfitness.py into homemaker: optimise(topology, x0=None) -> (geometry, fitness). DOF = equal-offset division ratios of free branches (solver.free_branches, lowest-storey cut ownership), clipped to [eps, 1-eps]. Objective = full oracle fitness (never a proxy — §4.2 falsified). Must support warm-start x0 (§5.6) and a population/batch evaluation mode so each iteration scores via one batched oracle call (§4.6).

Reproduces or exceeds §4.5 gains (x1.24–x1.67, no new failures) on 2f45907, candidate-002, c964435; works as a library call on any corpus .dom

innerloop.optimise() lands: batched CMA-ES sigma ladder (0.05/0.15, IPOP popsize doubling, deterministic seeding) over equal-offset free-branch ratios vs full oracle fitness; warm-start x0 supported. Acceptance vs unprojected originals: x1.65/x1.66/x1.58 against bars x1.24/x1.67/x1.59, no new failures, 46 oracle calls vs NM's 200. Two near-bar results accepted as reproduced-within-noise (1% tol) — draw spread brackets the single-NM-draw bars; approved by Bruno 2026-06-12. Gotchas: equal-offset projection of legacy unequal cuts loses fitness/adds failures (midpoint projection used); pycma seed=0 means clock-seeded.


Experiment: warm-vs-cold start of inner loop (Lamarckian inheritance)

DESIGN.md §5.6, §4.6. Warm-starting a child topology's inner loop from the parent's optimised ratios is the main lever for cutting per-topology cost (~3 min/topology cold). Apply single topology mutations to optimised corpus designs, re-optimise warm (surviving cuts keep values, new cuts get heuristic defaults) vs cold, compare oracle-call counts to convergence at equal final fitness.

Speedup factor measured across >=10 mutated topologies; decision recorded (expect order-of-magnitude; if <2x, revisit §4.6 Phase-2 scoping)

Experiment script committed (experiments/warm_vs_cold.py, 1cc86c8) and machinery validated oracle-free; one mutated child scored through the oracle OK. Waiting on homemaker-py-gp2 reference run to finish, then execute under URB_NO_OCCLUSION=1 (3 parents x 400 evals + 12 children x 2 x 200 evals, ~1.5-2 h oracle time). Default budgets: parent 400, child 200; target = evals to 95% of best final.

Measured (URB_NO_OCCLUSION=1, parent budget 400, child 200, 12 single mutations across 3 designs): cold start reached 95% of warm final in 0/12 cases within budget — speedup unbounded at practical budgets; warm finals beat cold finals x1.2-x4 in 12/12; 6/12 warm starts were within 95% at 1 eval (near-neutral mutations). Decision: Lamarckian warm-starting is MANDATORY in the memetic driver (homemaker-py-b39), not an optimisation; cold starts produce strictly worse geometry at equal budget. Note: 2 undivides were exactly fitness-neutral (same-type merge == Merge_Divided equivalence) — locality datum for homemaker-py-nyb.


Batched oracle: score many .dom files per invocation

oracle.py currently scores one .dom per urb-fitness.pl call (~1.65 s/dom). DESIGN.md §4.6: batching amortises Perl startup to ~0.99 s/dom and is required so population/batch optimisers can score a whole generation in one oracle call. Extend oracle.py with a batch API: write N .dom files, one perl invocation, parse N .score/.fails pairs. Keep the single-file path for compatibility.

Batch of 35 corpus files scores in one perl invocation; per-file results identical to single-file calls; measured s/dom reported

score_batch() lands in oracle.py; 35-file corpus parity verified single-vs-batch (1e-12 rel fitness, exact fail sets); 0.98 s/dom batched vs 1.27 single, x1.30


Multi-use leaves: one leaf satisfies several COMPATIBLE different codes (type superposition)

A leaf that legitimately and simultaneously satisfies several DIFFERENT compatible programme requirements (e.g. study + guest bedroom, or kitchen + dining). Distinct from erc.3 leaf-sharing, which aggregates k instances of the SAME code; this is a strict generalisation across DIFFERENT codes. Idea from Bruno (this corresponds to Stewart Brand's 'How Buildings Learn' — loose-fit / long-life rooms whose use churns over a building's lifetime).

WHY IT MATTERS
1. Architectural deliverable: adaptable multi-use rooms (Brand loose-fit), not just an optimisation trick.
2. Generalises the erc.3 floor-lowering lever to the SINGLETON (count:1) long tail that same-type sharing cannot reach: one leaf covering one X AND one Y removes a room-leaf, paying the ~1.8/leaf crinkliness tax (§13.1) once instead of twice. Crinkliness is scale-invariant, so a larger multi-use leaf is not penalised for size.

TWO READINGS
(a) Superposition as a SEARCH RELAXATION: carry a distribution/set of candidate types per leaf, evaluate a relaxed (expected/best-case) fitness for a smoother landscape, then COLLAPSE (argmax) at the end. Risks: relaxation gap (relaxed optimum need not sit near a good integer solution); collapse is itself a constrained rounding/assignment problem (cannot collapse 5 superposed leaves all to 'kitchen' when 1 is required); and search-machinery bets are 0/3 historically (§11-12) vs construction 4/4 — the floor is geometric, so pure search-easing may fight the wrong battle. LOWER PRIORITY framing.
(b) Multi-use as the DESIGN GOAL (preferred): the leaf permanently serves a SET of compatible codes; no collapse needed, multi-use survives into the output. Mirrors erc.3's mechanism exactly but with a SET of codes instead of an integer count: stamp leaf with the codes it serves (type-guarded as in erc.3 leaf.share/share_type); fitness count credits each code in the set, size scored against the union/least-upper-bound of targets, width/proportion as today (scale-invariant), adjacency satisfied if the SET satisfies it.

IMPLEMENTATION SKETCH (path b)
- dom.Node: a set/list of served codes (generalises leaf.share/share_type from erc.3). Survives search via deepcopy; emit in .dom only when non-trivial (as with 'share').
- graph.check_space_counts: a multi-use leaf credits coverage to EACH code in its set (type-guarded: honoured only while its served set is consistent with its assignment).
- fitness size/width/proportion: score the multi-use leaf against the combined target (union/LUB) of its served codes; crinkliness/access unchanged.
- construction: a new constructive option that fuses COMPATIBLE singleton rooms into shared multi-use leaves (analogous to operators._share_rooms but across codes), honouring adjacency/level.
- default OFF; controls reproduce §12.2 baseline.

KEY OPEN QUESTIONS (Bruno to spec)
- Who declares type-COMPATIBILITY? A new architectural input, analogous to adjacency (e.g. a 'compatible:' / 'multiuse:' list per space in patterns.config). kitchen+bathroom is nonsensical; study+guestroom is fine.
- Does the final design COLLAPSE to single uses or stay loose-fit (keep superposition as a deliverable)? Brand argues for keeping it.
- How exactly to combine size/width/proportion targets for a leaf serving 2+ codes (max? union? a 'dominant use' target?).
- Interaction with erc.3 same-type sharing and x3b per-code control — composable? (a leaf could be 'k of X' AND 'one Y').

RELATES TO: erc.3 (same-type leaf-sharing, the special case), x3b (per-code shareable flag), erc.7 (factor/synergy sweep), erc epic (lower the geometry floor). Concept only — implement in a future session.


Per-code shareable flag (SpaceReq.share) + homemaker-evolve CLI wiring

Make leaf-sharing (erc.3, §13.3) safe to default-on by giving the programme author per-code control, and expose it on the real CLI (not just the experiment env var).

Design (agreed with Bruno, open to refinement — he has follow-up questions):
- patterns.config per-space optional key 'share: N' -> SpaceReq.share (int, default 1 = not shareable). N>=2 means up to N rooms of this code per shared leaf.
- Master enable stays the 'leaf_sharing' conf/CLI flag (default OFF -> baseline, controls reproduce).
- Global grain selector 'leaf_share_factor': 0 => per-code opt-in only (share a code iff it has share:N>=2); F>=2 => global mode (share all sized multi-instance codes at grain F) with per-code 'share' overriding (share:1 opts a code OUT). This single knob covers both the safe default-on philosophy (0 + per-code keys) and the §13.3 experiment (F=3, reproducible, no example-programme edits).
- operators._share_rooms picks grain per code accordingly; fitness honours the explicit leaf.share (type-guarded) as today.
- homemaker-evolve gains --leaf-sharing / --leaf-share-factor, threaded to driver.search/search_staged (already plumbed).
- Tests: per-code grain, opt-out, default-OFF parity. NOT editing example programmes so §13.3 stays reproducible.

Relates to dyh (productionise). erc.7 covers the factor/max_share sweep + erc.4 synergy.


Leaf-sharing × erc.4 depth-balancing synergy + factor/max_share sweep

With the missing-fail leak closed by explicit multiplicity (§13.3), revisit the erc.3↔erc.4 synergy the diagnostics predicted: depth-balanced construction lands shared leaves at their correct absolute k×target area, which should further cut size+crinkliness. Also sweep leaf_share_factor (3 won here; try 2/4) and leaf_share_max (default 4) on maple+harbor, seeds 0/1/2, staged 20k, vs the §13.3 factor-3 result (maple 86.3, harbor 50.3).

erc.4 DONE (§13.4): depth-balanced standalone is MODEST end-to-end (−5.8% maple / −3.2% harbor, overlapping arms) — much less than its −11/−12% seed-floor probe, because 20k search erodes the seed advantage via divide/undivide. BUT the floor probe shows depth-balance is ADDITIVE with leaf-sharing: bal+sh3 beats share3-alone (harbor 65.3 vs 73.3, maple 113.7 vs 133.0). erc.7 is now the decisive test: does the additive seed floor survive to convergence when stacked on the leaf-count cut the search CANNOT erode? Wire DEPTHBAL=1 + LEAFSHARE=1 vs LEAFSHARE=1-alone in run_depthbal_ab.sh-style A/B.


Productionise leaf-sharing: evolve CLI flag + patterns.config key

erc.3 (§13.3) proved leaf-sharing lowers the floor −37% maple / −32% harbor end-to-end, but the flag is only reachable via the LEAFSHARE env in run_staged_search.py. For real runs: (1) expose --leaf-sharing / --leaf-share-factor on homemaker-evolve (evolve.py), threading to driver.search/search_staged (already plumbed); (2) optionally read a leaf_sharing key from patterns.config so the fitness + construction stay consistent without env injection (fitness already reads conf; construction would read it in evolve). Consider whether to default it ON given the decisive win. Also: the genome.signature ignores leaf.share, so a shared vs unshared leaf of the same type/structure collide — assess if niching needs share in the signature.

Superseded by x3b (per-code shareable flag + CLI wiring), which is the concrete implementation of dyh's 'CLI flag + patterns.config key' scope with the per-code opt-in design.


Thread parent failure strings onto Individual

Store the sorted .fails tuple on driver.Individual so operators can read which constraints the parent violates. The score is already recomputed per child (driver.py:146 want_grade path / innerloop result); capture score_with_fails output instead of discarding the strings. Near-zero cost. Prereq for the repair operator (homemaker-py-71d).

Also feeds erc.1 (per-leaf shape-fail vs density/granularity profile): storing the sorted .fails on Individual makes per-leaf fail attribution available to the diagnostic without re-scoring. Cheap, generically useful — promote ahead of the Tier-3 operator work if erc.1 is picked up first.


Constructive seeder is nondeterministic across processes (id-based set iteration)

BUG / reproducibility. operators._assign_adjacency_aware builds Python sets of dom.Node objects (circ/dominated/frontier) and iterates them; set iteration order for objects is id()-based, which varies across processes. Result: constructive_topology(seed=0, adjacency_aware=True) yields DIFFERENT topology signatures in separate processes (verified: sig hashes 4480 vs 16064 for maple-court seed 0), so the whole staged search trajectory is non-reproducible run-to-run. Measured single-run noise ~±3 fails (c3g div=3 control 129 vs §12.3 126 for the same maple seed 0). IMPACT: per-seed numbers in the §11/§12 ledger are not reproducible; only multi-seed MEANS are stable, and small effects (±3-4, e.g. the §12.3 negatives) are near the noise floor. FIX: make the dominating-set/assignment iteration deterministic — sort candidate nodes by the existing idx (leaf index) instead of relying on set iteration order, or drive all tie-breaks through idx. Re-establishing determinism will shift baselines slightly; note in DESIGN.md. Files: operators._assign_adjacency_aware (circ set, dominated union, frontier, the for s in circ loops).

RESOLVED with a corrected diagnosis (operator: investigated 2026-06-22).

MISDIAGNOSIS: the constructive seeder is NOT nondeterministic. _assign_adjacency_aware ends every max/min with a unique idx tiebreak (-idx[L]); its set unions (circ/dominated/frontier) are used only for membership, so iteration order never leaks. Proven: constructive_topology(seed=0, adjacency_aware AND not) gives BYTE-IDENTICAL signatures across processes for all four example programmes (stable sha1, e.g. maple-court aa=e688f744326b in 3 separate processes). The cited '4480 vs 16064' was a MEASUREMENT ARTIFACT: Python's builtin hash() of a str is salted per-process (PYTHONHASHSEED), so hashing an IDENTICAL signature string in two processes yields different ints (reproduced: 51920/5342/59970 for one identical string). Serial search (workers=1) is byte-for-byte reproducible (identical .dom across runs).

REAL BUG (fixed): parallel-only nondeterminism in driver._run_batch. It admitted futures via concurrent.futures.as_completed -> completion order varies run-to-run, 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 real source of the +-3..6 'noise'. FIX: iterate the futures list in SUBMISSION order (block on each f.result() in turn; all still run concurrently), reproducing the serial admission sequence. After fix: two workers=4 runs are byte-identical (162 fails, identical .dom). 211 tests pass.

IMPLICATION FOR LEDGER: per-seed numbers are reproducible ONLY for a fixed worker count. Serial != parallel is EXPECTED (children-per-iteration = 1 vs n_workers changes batch granularity, hence the search), not nondeterminism. Any ledger A/B comparing runs at DIFFERENT worker counts (or pre-fix parallel) conflated this with a real effect — re-run sub-+-3 effects at a fixed worker count.


M3 Wong-Liu re-association reachability move

9gp.2: add mutate_reassociate, the associativity move (a|b)|c <-> a|(b|c) (same-axis tree rotation on owned/live cuts) missing from the swap(M1)/rotate(M2) set. Targets the §11.4/§11.5 reachability bottleneck. Round-trip/invariant tests. MEASURE value on maple-court vs leu.2 baseline — either result is a valid verdict per the re-scoped bead. DESIGN.md §12.3.

MEASURED — NEGATIVE (DESIGN.md §12.3). M3 reassociate landed + A/B'd: maple 136.0→139.3, harbor 74.0→78.0 (neutral-to-worse, never a win) across seeds 0/1/2. Reaches new tree shapes but they are not better — third independent negative on search machinery (§11.4/§11.5/§12.3). Kept default-OFF. Valid verdict per re-scope.


Shape-feasibility pre-filter before inner loop

9gp.1: predict per-leaf shape fails (size/width/proportion/crinkliness) at the proportion-aware target geometry, prune clearly-infeasible topologies before the inner loop so budget flows to feasible ones. Reuse operators._size_divisions_from_targets + fitness quality methods. Default OFF; threshold is a measured parameter. Hook in driver._evaluate. Measure on maple-court + harbor vs leu.2 baseline. DESIGN.md §12.3.

MEASURED — NEGATIVE (DESIGN.md §12.3). Shape-feasibility filter landed + A/B'd: maple 136.0→140.0, harbor 74.0→77.0. Filter DID prune/explore more topologies in several runs, but extra topologies didn't lower fails. Calibration: shape floor ≈ achieved total (geometry-bound residual, confirms §11.7), so no lower-fail basin for saved budget to find. Kept default-OFF.


Constructive seeder + staged dispatch ignored storey_minimum

n_storeys_required only reads level: keys, so a programme with storey_minimum>max(level)+1 (e.g. programme-house: storey_minimum:2, all rooms level:0) was seeded one storey short by constructive_topology and routed to plain (non-staged) search. Fitness then fired a 'storey minimum' fail the search had to repair structurally. Surfaced while measuring leu.2 (proportion-aware seeding deepened the basin around the wrong-storey-count seed). Fix: programme.storey_minimum()/n_storeys_for(); driver.search passes min_storeys to constructive_topology; search_staged routes on max(n_storeys_required, storey_minimum). Independent win: programme-house single-stage baseline 8.0 -> 5.0 fails with correct 2-storey seed.

Fixed. programme.storey_minimum()/n_storeys_for(); driver.search passes min_storeys to constructive_topology; search_staged routes on max(n_storeys_required, storey_minimum). No-op for harbor/maple; programme-house single-stage baseline 8.0->5.0 with correct 2-storey seed. 204 tests pass.


Proportion-aware constructive seeding (size splits from target dims)

Follow-up to §11.6/§11.7. Adjacency-aware seeding cut the topology load (adjacency-to-c / access) but §11.6 explicitly noted the seed still splits at 0.5, producing 'more, smaller leaves' whose size/proportion/crinkliness fails the inner loop then has to recover. With topology fails now cut by seeding, this GEOMETRY residual is the dominant remaining term (§11.7 verdict). Attacking it at the seed is the proven-productive (construction) direction and is far cheaper than the 9gp encoding rewrite.

Idea: when constructive_topology / lift_base_to_storeys place a cut, size the division ratio from the leaves' TARGET dimensions (programme target areas/widths) instead of 0.5, so the raw seed geometry already sits near feasible proportions and the inner loop starts inside (or much closer to) the size/width/proportion basins. Keep adjacency-aware placement (§11.6/§11.7) unchanged; this only changes split RATIOS, not topology or type assignment. Behind a flag for clean A/B, default-on if it wins.

Measure raw-seed geometry fails (size/width/proportion/crinkliness) before/after AND end-to-end total fails at budget on harbor, programme-house, AND the new leu.1 benchmark, same protocol as §11.6. Record in DESIGN.md §12.2 + bead notes (incl. negative result if it does not win).

Proportion-aware split sizing implemented behind a flag; raw-seed geometry-fail reduction quantified; end-to-end total-fail change measured on harbor, programme-house, and the leu.1 benchmark (>=3 seeds each); result (positive or negative) recorded in DESIGN.md.

DONE (positive), default-on. End-to-end (20000 evals, 3 seeds, staged): harbor 85.3->74.0 (-13%, best 69), maple-court 151.7->136.0 (-10%, best 126). PROP=0 reproduces 11.7/12.1 baselines exactly. programme-house regresses at fixed budget (deeper-local-optimum: well-fitted seed walls off the undivide restructuring path) but a budget sweep shows it's convergence-SPEED not asymptote (PROP=1 reaches 1 fail at 150k, beating PROP=0's 2; floor is 2). Win requires rotation+ratio sizing from target dims (area-only regressed via slivers). Surfaced + fixed storey_minimum bug (cq1). Default flipped on: driver.search/search_staged seed_proportion_aware=True, harness PROP=1. DESIGN.md 12.2. 204 tests pass. New maple best 126 saved as generated.dom.


Adjacency-aware constructive seeding (cut adjacency/access fails)

Follow-up to homemaker-py-c4c.2. constructive_topology currently assigns room types to leaves at RANDOM, ignoring each space's adjacency requirement. On harbor this leaves 8 adjacency + 13 access fails in the seeded design. Cluster each required room near its required neighbour (esp. circulation c) at construction time — e.g. assign rooms to leaves whose sibling/parent is C, or grow the tree so each room lands adjacent to a circulation spine. Should directly cut the adjacency+access fail load that now dominates the complete-design quality-fail regime (DESIGN.md §11.2 verdict).

DONE (positive), DESIGN.md §11.6. _assign_adjacency_aware: greedy connected-dominating-set of circulation leaves on the geometric leaf_graph so every room borders a connected circulation spine; rooms on dominated leaves, O peripheral. Default-on via constructive_topology(adjacency_aware=True), threaded driver.search(seed_adjacency_aware). Seed quality (harbor 10 seeds): adjacency 29->12, access 27->8. End-to-end single-stage 20000 evals total fails mean: harbor 110.0->90.7 (-17.5%, ADJ=0 seed0 reproduces §11.2 105 baseline exactly), programme-house 12.3->9.3 (-24%); adjacency-aware single-stage harbor (mean 90.7, best 85) beats the §11.3 staged 95. Follow-ups filed: lift_base_to_storeys adjacency-awareness + secondary adjacencies.


Topology diversity: structural niching + restarts (replace fitness-scalar dedup)

The population dedups on the FITNESS SCALAR (driver.py:174, abs(fitness) within 1e-9) and replaces worst-by-key. There is no structural/topological diversity preservation, no restarts, no islands. On a rugged combinatorial landscape this converges prematurely — and it is the root cause of the blank-slate gap (§7 Phase 2 verdict): a single mutation chain loses to urb-evolve's random-population diversity (init.dom: memetic 18 fails vs urb-evolve 6).
Add: (1) a topology signature (canonical tree hash / partition signature) so 'same topology, different geometry' is detectable and niching is by STRUCTURE not score; (2) diversity-preserving replacement (crowding / niching); (3) restarts or a small island model so blank-slate exploration matches urb-evolve's upfront diversity.

A cheap topology-signature hash (string-encode the per-level tree + types) unblocks niching without waiting for the full canonical encoding; the canonical Polish encoding (homemaker-py-9gp) is the principled long-term signature and makes (a|b)|c == a|(b|c) collapse exactly. Wire signature into admit() in place of / alongside the fitness-scalar guard.

On blank-slate programme-house, memetic reaches <=6 fails (matching/beating urb-evolve) at equal native-fitness budget; population structural diversity quantified (distinct topology signatures over time) before/after; recorded in DESIGN.md §11.x + bead notes.

DONE (negative), DESIGN.md §11.5. Implemented genome.signature (ratio-invariant structural topology hash), structural niching (niche_by_signature) replacing the fitness-scalar dedup, and soft restarts (restart_patience); SearchResult gained n_distinct_signatures/diversity_history/n_restarts. Diversity criterion MET: final-pop distinct topologies ~5/16 -> 16/16, ~30% more topologies seen with restarts. Gate NOT met: blank-slate programme-house total fails (20000 evals) before/niche/restart = seed0 11/14/12, seed1 11/11/14, seed2 15/13/13 (mean 12.3/12.7/13.0); harbor staged seed0 = 95/94/108 (legacy 95 reproduces §11.3). Niching is a tie within seed noise, restarts strictly worse. Falsifies the epic's premise that the fitness-scalar dedup is the premature-convergence root cause: legacy already holds 14/16 distinct on harbor; the plateau is a reachability (operator/encoding) problem, not population-management. Both flags default-off, kept for reuse; genome.signature is the cheap stand-in for the 9gp canonical Polish encoding.


Graded high-fail objective (gradient in the high-fail regime)

Phase 4 (homemaker-py-yg5) chose lexicographic (-n_fails, fitness) — correct for not being FOOLED by the 0.5^n cliff (§4.9). But lexicographic-by-TOTAL-count gives almost zero selection signal in the high-fail regime: on harbor every candidate sits at ~49-74 fails, so neighbours are indistinguishable and the search has no gradient to climb. There is no partial credit for a size-fail that is nearly in range, nor for covering one more required requirement. §7 predicted penalty reshaping would 'flatten the fail cliff' for blank-slate; lexicographic did not deliver that for high counts.
Add a graded objective for the high-fail regime: continuous proximity per unsatisfied constraint (how close a size/width/proportion is to its band) and/or count of DISTINCT unsatisfied requirements with sub-credit, used as a tie/secondary key beneath fail-count. Must preserve: (a) inner-loop 0.5^n cliff protection (§5.4) — inner loop unchanged; (b) the missing-space hierarchy (§6) — must not make dropping a room attractive.

Likely a third comparison key: (-n_fails, -n_distinct_unsatisfied_or_proximity_sum, fitness). Or a soft margin inside fail counting only in the outer comparator. Keep the scalar fitness (with 0.5^n) untouched so the inner loop is unaffected. Extends homemaker-py-yg5; reuse experiments/penalty_reshape.py harness.

Measured escape from a high-fail plateau on harbor and/or blank-slate programme-house that the current lex comparator cannot escape at equal budget; before/after best-fail trajectory recorded in DESIGN.md §11.x + bead notes. Inner-loop cliff protection verified unchanged (re-run the §4.9 inner-loop 0/9-regression check).

NEGATIVE RESULT (DESIGN.md §11.4). Implemented graded proximity key (-n_fails, grade, fitness) behind use_grade flag (default off): fitness._leaf_grade / score_with_grade sum f/FAIL_THRESHOLD over failing per-leaf quality factors; scalar fitness + fail count untouched. Inner-loop 0/9 regression: PASS (re-ran §4.9 part 1). Harbor staged A/B, 20000 evals, seeds 0/1/2 total fails: lex 95/96/106 (mean 99.0) vs lex+grade 99/98/102 (mean 99.7). Grade wins 1/3, loses 2/3, slightly worse on mean, NO plateau escape. Acceptance criterion (escape lex cannot achieve) NOT met. Root cause: premise falsified — within a fixed fail-tier 0.5^n is constant so fitness still spans ~6 orders of magnitude (1e-37..1e-31), giving lex's secondary fitness key a strong gradient already; grade above fitness DISPLACES it (stalls fail-reducing restructurings), below fitness is inert. High-fail plateau is a topology-basin problem -> defer to §11.5 niching/restarts + 9gp canonical encoding. Code kept default-off for reproducibility / possible reuse as a §11.5 diversity signal.


homemaker-fitness: native Python CLI replacement for urb-fitness.pl

We need a Python CLI tool that replicates the behaviour of urb-fitness.pl so we can score .dom files without shelling out to Perl. The tool should: accept .dom file paths as arguments (or glob *.dom in cwd if none given), load patterns.config and costs.config from cwd and parent dir (local overrides project-level), skip scoring if .score and .fails files are already newer than the .dom (unless FORCE_UPDATE env var is set), score each .dom using fitness.Fitness.score_with_fails(), write the score to <dom>.score (40-digit float format), write the failures to <dom>.fails, print the score to stderr. Expose as homemaker-fitness entry point in pyproject.toml and as python -m homemaker_layout.fitness_cmd module. This replaces the oracle.py shelling-out path for Phase 3 native fitness.

Implemented as homemaker_layout/fitness_cmd.py with homemaker-fitness entry point; exact score parity verified against urb-fitness.pl on corpus


Native fitness parity gap on multi-storey designs (~3.7%)

During programme-house cold-start runs with the fixed level_add operator, the generated 2-storey design showed native=1.2388e-04 vs oracle=1.1944e-04 (3.7% gap), exceeding the 0.01% rel_tol in test_native_fitness_score_parity. All existing single-storey corpus files pass parity fine (73/73). Hypothesis: a subtle discrepancy in value or cost computation for multi-level trees — candidates are staircase quality, circulation connectivity, or per-storey cost accumulation. To investigate: score a sweep of known multi-storey corpus files natively vs oracle and identify which term diverges.


Make homemaker-py standalone: remove dependency on Perl Urb package

Currently tests and fitness scoring depend on the Perl Urb package (urb-fitness.pl) and corpus files in /home/bruno/src/urb/examples/. The tool should be fully standalone and not require any external Perl packages or local urb corpus paths. This includes: bundling or reimplementing any needed reference data, making the native Python fitness the default path, and ensuring tests pass without /home/bruno/src/urb present.


Blank-slate cold-start initialisation

The outer search stalls when starting from init.dom (Phase 2 gate: 18 fails after 2000 evals vs urb-evolve's 6). The root cause is single-seed topology mutation chaining — building structure one room at a time gives no gradient across the large zero-feasibility region. Fix requires multi-start bootstrap: generate a diverse initial population by random topology sampling, or a greedy room-placement initialiser that satisfies adjacency/level constraints before handing off to the memetic loop. Without this the tool is only useful for refining existing designs, not designing new buildings from scratch.

Cold-start from init.dom reaches comparable fail count to urb-evolve within equal eval budget; tested on programme-house

Bootstrap implemented: auto-detect bare-plot seed, generate pop_size random topologies, evaluate each at child_budget before memetic loop; 3 new tests all green


Canonical slicing encoding (normalized Polish expression) + shape feasibility

RE-SCOPED 2026-06-19 under epic homemaker-py-leu. Canonical slicing encoding capstone (DESIGN.md §5.5, §7 Phase 5): normalized Polish expression / skewed slicing tree (Wong-Liu) for redundancy-free, high-locality topology moves; bottom-up shape-feasibility checks to prune infeasible topologies before the inner loop. Goal: scale to larger programmes. Excluded representations stay excluded (§2): no sequence-pair / B*-tree (non-slicing).

SCOPE CHANGE — one of three original justifications is now DEAD. The original bead leaned on 'provides the principled topology SIGNATURE that c4c.5 niching needs ((a|b)|c == a|(b|c) collapse)'. §11.5 (c4c.5) FALSIFIED niching: maximal structural diversity did not lower fails, and genome.signature already exists as the cheap stand-in. So the niching-signature rationale is dropped. The surviving, EVIDENCE-SUPPORTED parts:
  (a) M1/M2/M3 Wong-Liu moves — richer topology operators that attack the REACHABILITY bottleneck §11.4 AND §11.5 both independently fingered (operators+encoding cannot reach low-fail basins). This is the core justification.
  (b) Shape-feasibility pruning before the inner loop — targets the §11.7 geometry/shape residual (size/proportion/crinkliness) AND saves inner-loop budget, which is the part that actually buys SCALING.
Associativity collapse for its own sake is unproven at 16 rooms; its value must be MEASURED on the leu.1 >16-room benchmark, not assumed.

Survey carry-over (still true): current encoding is base-floor slicing tree + per-storey deltas (GNode), not a Polish expression; 11 mutation operators work on decoded Node trees; decode() fixed-point removes intra-encoding redundancy but tree structure is not canonical. Genome: genome.py; operators: operators.py; tests: test_genome.py, test_operators.py.

ORDER: lands LAST in the epic — on the strongest seed (after leu.2 proportion-aware seeding) and with the leu.1 benchmark in place to actually measure the scaling claim. Do not build encoding machinery on an unmeasured premise (the §11.4/§11.5 failure mode).

Encoding round-trips with the genome; M1/M2/M3 moves implemented; shape-feasibility pre-filter prunes infeasible topologies before the inner loop; MEASURED search improvement on the leu.1 larger-than-house benchmark vs its documented baseline; result recorded in DESIGN.md §12.3.

MEASURED — NEGATIVE, re-scope satisfied (land + measure). Both M3 reassociate and shape-feasibility filter implemented as Node-tree operators (no Polish rewrite), unit-tested, and A/B'd on maple-court+harbor seeds 0/1/2 (DESIGN.md §12.3). Both neutral-to-slightly-worse; controls reproduce §12.2 exactly (maple 136.0, harbor 74.0). Verdict: Phase-7 residual is NOT reachability/feasibility-bound — it is the geometry/shape floor of the constructed slicing layouts (3rd search-machinery negative vs 4 construction wins). Full canonical Polish rewrite NOT justified: its one testable promise (associativity reachability) was tested directly and did not pay. Both kept default-OFF.


Scaled topology search on native fitness

DESIGN.md §7 Phase 3 closing step. Once native fitness passes corpus parity, re-run the Phase-2 memetic search at real scale (population/generations comparable to urb-evolve) on the native objective. This is the first point where the §1 scaling question gets a real answer.

Full-scale run on programme-house beats both urb-evolve and the small-scale Phase-2 result; larger programme attempted


Benchmark: memetic loop vs urb-evolve at equal oracle-call budget (Phase 2 gate)

DESIGN.md §7 Phase 2 gate. Compare against urb-evolve from the same seeds/programmes at equal oracle-evaluation budget — NOT generations (urb-evolve has diversity injection/culling baked in, so generations are not comparable). Go/no-go: memetic loop must beat equal-budget urb-evolve. Scaling up waits for native fitness.

Best-fitness and failure-count comparison at >=2 budgets, >=3 seeds; go/no-go decision recorded in DESIGN.md

Phase-2 gate run (benchmark_vs_urbevolve.py, 2026-06-13, 2000 evals, URB_NO_OCCLUSION=1): 2/3 seeds → REVIEW. Memetic beats urb-evolve by 1.91x/1.63x on seeded designs; blank-slate init.dom stalls at 18 fails vs urb-evolve's 6 (random-pop init advantage). Fix: patterns.config was missing from re-score cwd (run_search.py), giving false near-zero finals in first run. Results recorded in DESIGN.md §7 Phase 2 gate.


Memetic search driver, small-scale (budgets in oracle evaluations)

DESIGN.md §5, §7 Phase 2, §4.6 arithmetic. Memetic EA/SA over topology genomes wrapping the geometry inner loop (warm-started per §5.6); score = best full fitness over the inner loop. Explicitly small-scale on the batched oracle: tens of topologies, budget accounted in oracle evaluations, not generations. Population evaluation batched into single oracle calls.

End-to-end run on programme-house completes within a stated oracle-call budget and logs evaluations; produces valid .dom output

driver.search() lands: steady-state memetic GA, tournament selection, operators + crossover, warm-started inner loop (Lamarckian write-back), budgets accounted in oracle evaluations. Acceptance run (URB_NO_OCCLUSION=1, budget 2000, seed c964435): 2010 evals / 23 topologies, best 0.00765/2 fails via crossover = x1.14 over the geometry-only optimum; output .dom re-scores standalone at exactly the recorded fitness. En route: found + fixed Urb ratio_o/ratio_type first-match nondeterminism (class-sum patch, 35/35 corpus parity) after the search reward-hacked it; operators now emit canonical uppercase generics (Bruno's correction: C=circulation, Is_Covered is a predicate).


High-locality topology operators (mutation + subtree crossover)

DESIGN.md §5, §7 Phase 2, §8.4. Mutation moves: divide/undivide leaf, swap children, rotate cut, retype leaf, per-floor delta edits, storey add/delete (cf. Urb Mutate.pm — but geometry sliding belongs to the inner loop, not the operator set). Crossover: area-matched subtree exchange (a subtree = a contiguous region, so crossover is meaningful — Crossover.pm). Operators must be high-locality: small genome change => small phenotype change, so warm-started inner loops stay cheap.

Each operator produces valid genomes (oracle scores them without error); locality measured (mean fitness/geometry perturbation per operator)

operators.py lands: 7 mutations + area-matched crossover, valid-by-construction via genome.encode repair. 115/115 oracle-valid children; locality measured: geom-pert 0.07-0.33 per op, fitness-pert 0.68-0.99 (0.5^n cliff flags raw moves — warm restart + penalty reshaping confirmed load-bearing). Also fixed dom._link stale below-links on structural mutation.


Topology genome: base-floor tree + per-floor deltas + type assignment

DESIGN.md §5.2, §7 Phase 2. Genome = base-floor slicing topology (primary) + per-leaf type assignment + per-floor divide/undivide deltas (Below-inheritance as regulariser; cut owned by lowest storey where its path is divided — §10). Must round-trip to/from dom.py Node trees so the oracle and inner loop consume it directly. Includes storey count and per-floor type overrides.

Genome <-> .dom round-trip on all 35 corpus files preserves fitness; multi-storey wall stacking preserved

genome.py encode/decode lands. 35/35 oracle fitness parity after round-trip (flag-on); genome fixed-point + owned-projection tests. Dead-field discovery: corpus upper storeys carry drifted dead divisions (97) and rotations (187) — canonicalised by decode, validated fitness-neutral.


Experiment: inner-loop optimiser bake-off at equal oracle budgets

DESIGN.md §7 Phase 1, §8.3. DOF is only ~rooms-1 (6–7 on corpus). Compare Nelder-Mead vs CMA-ES vs batched multi-start pattern search at equal oracle-call budgets, measuring fitness gained per oracle call and wall-clock (batch-friendliness matters — §4.6). Measure, don't commit blind.

Table of fitness-per-budget across >=3 candidates; one optimiser chosen and recorded in DESIGN.md

Bake-off complete: CMA-ES confirmed as Phase 1/2 optimiser. NM wins quality per eval but sequential architecture incompatible with batching (§4.6). Compass stalls on narrow valleys. Results in DESIGN.md §8.3 and experiments/bakeoff_innerloop.*


Bakeoff: repair operator vs baseline on harbor-house

Bake off the failure-directed repair operator against the current baseline on examples/harbor-house (3m.dom config). Seed from the 3M best (3m.dom) and run ~200k evals, multiple seeds. Also sweep child_budget DOWN (e.g. 80 -> 40 -> 20) to test the hypothesis that reallocating evals from ratio-polishing to topology repair lowers fails. Metric: final n_fails and crinkliness/connected/access counts. Reuse experiments/bakeoff_harbor.py pattern.


mutate_repair: failure-directed topology repairs

New operator mutate_repair(parent_root, fails, reqs, rng) in operators.py dispatching on failure class, targeting the leaf id named in each fail string. Priority order = ratio-invariant fails first:
- crinkliness on L -> retype a geometric neighbour of L to O (interior light well) or reassociate/swap L toward facade (attacks 13)
- 'level N not connected' -> retype a bridging leaf to C to join circulation components (attacks 2)
- access on L -> retype a neighbour to C (attacks 1)
- too few stairs -> core_divide to add aligned vertical core (attacks 1)
Reuse leaf-adjacency graph from _assign_adjacency_aware, plus reassociate/core_divide/retype. Wire into operators.mutate weighting and the driver child-generation path (driver.py:452). Depends on fails being available (parent thread task).


Failure-directed topology-repair operator (harbor-house plateau)

harbor-house plateaus at 27 fails under a 3M-eval run. Fail breakdown of the 3M best (3m.dom): 13 crinkliness, 7 size, 2 edge-too-long, 2 level-not-connected, 1 proportion, 1 access, 1 too-few-stairs.

Diagnosis: ~16 of 27 fails (crinkliness 13, not-connected 2, access 1, stairs 1... actually 17 incl stairs) are INVARIANT to split ratios, but the inner loop (child_budget=80 CMA evals/child) spends essentially all eval budget on ratios. The outer comparator only keeps n_fails (driver.py:259) and operators pick targets at random, so the search reaches these discrete adjacency/daylight fails only by luck.

Crinkliness root cause: a landlocked leaf (no facade edge, no adjacent uncovered O) has area_outside=0 -> crink=0 -> quality_uncrinkliness hits the 'if not crink: return 0.0' branch (fitness.py:339) -> guaranteed fail for ALL ratios. Big rooms (cr1 80m2, da1 60m2, n 60m2) are worst. Fix is interior O courtyards / facade access = TOPOLOGY only.

Plan: read the parent's structured .fails (already computed at driver.py:146, just not stored on Individual) and apply targeted, mostly-deterministic topology repairs per failure class, attacking the ratio-invariant fails the inner loop cannot touch. Reuses reassociate, core_divide, retype, and the leaf-adjacency graph.

Reparented under erc (Phase 8) as a Tier-3 search-machinery bet, LOW prior per erc's thesis ('search machinery cannot help — the floor IS the result', 0/3 wins from grade/niching/feasibility). Honest framing: this is NOT refuted by that scoreboard — those 3 losses were all selection/pruning changes; none added a TARGETED REPAIR OPERATOR, which is a new class. But do not invest here until a construction lever (erc.3/.4/ld2) moves the floor. Must follow erc's shared protocol: A/B maple-court + harbor seeds 0/1/2, 20k evals staged, control reproduces baseline (maple 136.0, harbor 74.0), verdict in DESIGN.md §13.x.


Experiment: island model — prime population from N independent seeds, crossover-heavy migration phase

User-proposed lever (2026-06-23): the Perl Urb workflow ran the search many times and kept the best because runs settled into different local minima. The Python tool is deterministic per --seed, so the analog is: run N independent seeds (e.g. 16), then PRIME a fresh population with those N converged elites and run a second, crossover-heavy phase — an island model with synchronous migration.

KEY DISTINCTION from prior negatives: this is NOT the §11.5 (c4c.5) niching/restart experiment. Those injected FRESH constructive/random seeds for raw diversity and landed null. Here the migrants are FULLY-CONVERGED elites (each spent a complete budget), so they are high-quality building blocks, not diversity filler. The §11.5 'diversity does not help' result does not directly refute this; the mechanism is different (recombination of converged basins, not exploration).

HONEST PRIOR (against): this is a SEARCH-MACHINERY bet, and the leu/c4c epics are decisive that search machinery keeps landing neutral-to-negative (§11.4 graded objective, §11.5 niching+restarts, §9gp M3 reachability + shape-feasibility filter = 3 search-machinery negatives) while CONSTRUCTION/SEED quality wins (§11.6 adjacency-aware seeding, §11.7 adjacency-aware lift = 4 construction wins). The residual is diagnosed as geometry/shape-bound (size/proportion/crinkliness), not population-management-bound. So baseline expectation is neutral.

WHY IT MIGHT STILL PAY: the one untested sub-mechanism is whether crossover can stack wins across independent basins (run A solved cluster X, run B solved cluster Y, child inherits both -> lower total fails than either parent). That has never been tested with converged migrants.

Control / baseline: 'best-of-N' — run N=16 seeds, take the single lowest-fail/highest-fitness result. This is essentially free (the N runs happen anyway) and is the legitimate descendant of Urb's multi-run habit. The experiment must BEAT best-of-N to count, on equal TOTAL budget (N short runs + migration phase vs N+ longer independent runs).

Phase A: run search() for seeds 0..N-1 at a per-seed budget, collect each result.best.root (.dom).
Phase B: prime a population from those N elites and continue evolving with high p_crossover (e.g. 0.5-0.8) to stress recombination. Reuse existing machinery — no new representation:
  - The seed_factory / bootstrap path in driver.search already accepts a custom seed producer; a factory that cycles through the N pre-evolved roots primes the population directly (no fresh construction).
  - Set bootstrap=True so the N elites are evaluated as the initial population, then the memetic loop runs.

ALIGNMENT RISK to measure, not assume: operators.crossover (operators.py:1001) is AREA-MATCHED subtree exchange — it pairs a region of A with the area-closest third of B, with no notion of programmatic/spatial role. Two independently-evolved trees encode similar arrangements with different tree structures (the encoding is not canonical — 9gp closed-negative, abandoned), so the same functional cluster sits at a different path/area/orientation per run. Area-matched splice across independent optima may therefore be disruptive rather than synthesizing, and the inner loop re-solves ratios at the splice boundary (spliced quality not preserved). Instrument: track whether any migration child ever beats max(parent fails) reduction; if crossover children are never net-positive, the null is mechanistic (alignment), not budget.

Benchmarks: maple-court + harbor seeds (the §12.x A/B set), so controls reproduce documented baselines (maple 136.0, harbor 74.0). Record in DESIGN.md (new §12.x) per project convention.

NOT gated on canonical encoding: 9gp is CLOSED with a negative verdict (associativity/reachability tested directly, did not pay). Do not revive the Polish rewrite as a prerequisite.


Experiment: topology diversity x scaled tournament pressure (joint A/B)

Open lever left untested by §11.5 (homemaker-py-c4c.5): structural niching was A/B'd against the legacy fitness-scalar dedup with selection pressure HELD FIXED at a binary tournament (k=2). §11.5's own mechanism note says maximal diversity under fixed pressure just diffuses effort — i.e. diversity and pressure are coupled and were never co-tuned. This issue isolates that coupling: sweep tournament size jointly with niching to test whether sharper selection converts the extra structural diversity into lower fails, rather than diffusing it. Premise from §11.5 is a diagnosis, not a tested result; the project pivoted to the canonical encoding (homemaker-py-9gp) instead. Tracking so the lever is not silently lost.

§11.5 raised structural diversity to 16/16 but held selection pressure FIXED at a
binary tournament (driver._tournament, k=2, driver.py:154; never overridden, no
search() parameter, no env var). The §11.5 writeup names the coupling as the
mechanism behind its own null result: "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." That is, diversity and
pressure were varied as if independent when they are coupled: niching widens the
population, but k=2 was never sharpened to convert the extra exploration back into
exploitation.

Implementation:
- Expose tournament size as a parameter: add `tournament_k: int = 2` to search()
  (and search_staged()), thread it into both _tournament call sites
  (driver.py:448 crossover pair, :452 mutation parent). Optionally an env knob
  HOMEMAKER_TOURNAMENT_K mirroring HOMEMAKER_POP for the experiments harness.
- Reuse the existing genome.signature / niche_by_signature machinery from c4c.5
  unchanged — this issue adds ONLY the pressure knob and the joint A/B.

A/B design (equal native-fitness budget, URB_NO_OCCLUSION=1, 20000 evals):
- Grid: niche_by_signature ∈ {off, on} × tournament_k ∈ {2, 3, 4}.
- The (niche=off, k=2) cell is the legacy baseline; (niche=on, k=2) reproduces
  §11.5's "niche" column. New cells are the higher-pressure rows.
- Seeds: programme-house seeds 0/1/2 (reuse §11.5 seeds for direct comparison),
  plus harbor-house staged seed 0. NOTE the §11.5 sample (3+1 seeds) was thin and
  its null sits within seed noise — widen to >=5 programme-house seeds so a real
  effect is distinguishable from noise this time.
- Reuse experiments/run_search_scaled.py (NICHE env already wired) +
  run_staged_search.py for harbor; add the k knob to both.
- Report total fails at budget per cell (primary), plus final-pop distinct
  signatures and distinct-seen (confirm niching still bites at higher k).

On blank-slate programme-house at equal native-fitness budget (>=5 seeds), some (niche, k) cell beats the legacy (off, k=2) baseline mean fails by more than seed noise; OR the joint sweep confirms the §11.5 null is robust to selection pressure (no k recovers a win from 16/16 diversity). Either outcome recorded as a DESIGN.md §11.x subsection + bead notes, with the per-cell fails table. Negative result is an acceptable close.

Diagnosed during a session reviewing §11.5. Tournament pressure is hard-coded k=2 (driver.py:154); confirmed no override anywhere in src/ or experiments/, no env var, no prior issue. Cheap to run: niche machinery already exists (c4c.5, default-off), only the tournament_k knob is new. Lower priority because §11.5 + §11.4 both concluded the plateau is a reachability (encoding/operator) problem, so this is a loose-end falsification check rather than the expected lever.


Construction granularity / leaf-shape lever for the geometry residual

HYPOTHESIS with measured motivation (DESIGN.md §12.3 residual diagnostic), unproven — must be A/B'd vs the §12.2 baseline before adoption (same discipline as §11/§12 levers). Finding: maple-court shape fails are UNIFORM (~68/73 leaves fail), at only 0.44 plot utilisation, dominated by crinkliness (perimeter/area) then size (undersize). So the residual is NOT placement-mismatch (no good leaves to place into) and NOT density/area-bound — it is OVER-GRANULAR construction: 73 small leaves for 52 rooms => high perimeter/area + below-target sizes. Candidate levers (construction side): fewer/larger leaves, merge or share leaves across same-class rooms, coarser circulation spine, or a granularity that trades adjacency coverage for leaf shape. Cheap first experiment: vary the circulation-per-room ratio and/or a min-leaf-area floor in constructive_topology, measure shape-fail floor (operators.predicted_shape_fails) and end-to-end fails on maple+harbor. Alternative outcome to accept: 52 distinct rooms cannot be well-shaped as 52 leaves at this density (geometry floor of the slicing representation). Files: operators.constructive_topology/_grow_leaves/_assign_adjacency_aware.

MEASURED — NULL (DESIGN.md §12.4). Cheap raw probe: coarser spine lowers SHAPE floor (maple 135→110, harbor 83→66) but raises access/adj equally → raw TOTAL flat-to-worse; div=3 near the total-floor min. End-to-end A/B (20000 evals, seeds 0/1/2): maple div6 137.0 / div8 134.3 vs baseline 136.0; harbor div6 75.3 vs 74.0 — all within ±1.7, inside the ~±3 noise floor, huge per-seed spread. Coarsening the spine does NOT pay end-to-end (shape gain cancelled by access damage that is not free to repair). Kept circ_divisor=3 default. En route found nondeterminism bug xcy (±3 noise). Residual is the geometry floor of the slicing representation at this density.


Adjacency-aware lift_base_to_storeys + secondary adjacencies

Follow-up to s44 (DESIGN.md §11.6). s44 made constructive_topology cluster rooms around a connected-dominating-set circulation spine (geometric leaf_graph), cutting harbor single-stage fails 110->90.7 mean and beating the staged §11.3 best of 95. Two gaps remain: (1) lift_base_to_storeys (staged Stage-2 upper floors) still assigns leaf types at RANDOM — port the _assign_adjacency_aware CDS approach to it so staged search benefits too. (2) Secondary adjacencies (k1<->da1, da1<->o, etc., ~4 harbor rooms) are not clustered — extend _assign_adjacency_aware to place rooms with non-c adjacency reqs next to their required neighbour after the c-spine is laid.

DONE positive, DESIGN.md §11.7. Adjacency-aware lift (CDS seeded from inherited core) + secondary-adjacency room placement. Staged harbor 20k evals: ADJ0 mean 99.0 (=§11.4 baseline), ADJ1 mean 85.3 (-14%, best 78). New best harbor overall. operators 22 tests pass.


Config inheritance: load parent patterns.config/costs.config as base layer

urb-evolve.pl walks up one directory level and loads ../patterns.config and ../costs.config as a base configuration before merging the programme directory's own files on top (local keys win). homemaker-evolve and fitness.load_config should replicate this: when loading a programme directory, first check the parent for each config file and load it, then deep-merge the local file over the top. This lets shared defaults live in a project root while individual programmes only override what differs.


Package install: pyproject.toml with entry points

The project currently requires PYTHONPATH=/home/bruno/src/homemaker-py/src and is run via 'python3 experiments/...'. There is no installable package. Add a pyproject.toml with: package discovery for src/homemaker/, a [project.scripts] entry point for homemaker-evolve (homemaker-py-2wc), and minimal metadata. After 'pip install -e .' the tool should be on PATH and importable without PYTHONPATH. Keep the existing pyproject.toml if one exists and extend it.

'pip install -e .' succeeds; 'homemaker-evolve --help' works from any directory; 'import homemaker' works without PYTHONPATH

pyproject.toml already had entry point; renamed package to homemaker-layout throughout, GitHub repo renamed, pip install -e . verified


Test suite

There are no automated tests. Validation has been done entirely through experiment scripts and the 35-file corpus parity check (homemaker-py-uxz). This is acceptable during exploration but fragile as the codebase grows. Need pytest-based unit tests covering: geometry port correctness (vs known values, not just vs oracle), fitness term correctness (size/width/proportion/adjacency/access/crinkliness/stair terms individually), genome operators (mutations preserve tree invariants), inner loop (convergence on known landscape), and a fast corpus smoke test (subset of the 35 files, score within tolerance). The corpus parity experiment can be the integration test baseline.

pytest runs clean; geometry, fitness terms, operators, and inner loop each have unit tests; corpus smoke test covers at least 5 files

Added test_geometry.py (26 tests) and test_fitness.py (35 tests); full suite now 175 tests, all passing


Parallelise outer search population evaluation

The outer memetic search evaluates topologies sequentially. Each eval runs the inner loop (CMA-ES) to convergence — independent across population members. Native fitness is pure Python with no shared mutable state, so population evaluation is embarrassingly parallel. multiprocessing.Pool or concurrent.futures.ProcessPoolExecutor over the child generation batch would give near-linear speedup with population size. At 71.8 evals/s single-threaded on a seeded programme-house run, parallelisation across available cores would proportionally increase the effective budget within the same wall-clock time.

Population generation parallelised; throughput scales with core count; verified correct (same result distribution as serial)


Revisit Nelder-Mead for inner loop (post-oracle)

The Phase 1 bakeoff (homemaker-py-d0s) chose CMA-ES over Nelder-Mead because CMA batches oracle calls (18 vs 200 per topology) — critical when oracle cost is 1 s/dom. That constraint is gone: native fitness evaluates at 71.8 evals/s with no batching penalty. The bakeoff showed NM wins quality per eval by +15% at budget 200 (x1.56 vs x1.41 gain). NM is also simpler, has no hyperparameters, and is inherently sequential which matches the inner loop's single-topology use. Re-run the bakeoff with native fitness; if NM still wins, swap it in. Also evaluate gradient-based optimisation (autograd through the native fitness functions) as a potential further improvement.

Bakeoff re-run with native fitness; inner loop updated if NM or gradient method outperforms CMA-ES; gain improvement documented

NM swapped in as default; bakeoff shows wins at all DOF sizes — programme-house +9% at budget 80, harbor-house decisive win (CMA harmful at 35-40 DOF)


CLI tool: homemaker-evolve (equivalent to urb-evolve.pl)

Wrap the existing memetic search driver as a proper command-line tool, analogous to urb-evolve.pl. The tool should: accept a programme directory and optional seed .dom file as positional args; honour env vars for budget/population (MAX_ITERATIONS, MAX_POP or equivalents); write the best .dom found to the programme directory (or stdout); print progress to stderr; handle SIGINT/SIGTERM gracefully (write best-so-far and exit cleanly). The bulk of the logic already exists in driver.py and experiments/run_search_scaled.py — this is a thin wrapper that makes the search usable from the shell and composable with other tools. Install as bin/homemaker-evolve or src/homemaker/bin/homemaker-evolve.


Fix Urb programme width default (upstream of homemaker-py-can fix)

The native fitness fix in homemaker-py-can derives a sane width from sqrt(size/proportion) when a programme space has no explicit width. The same bug exists upstream in Perl Urb: Fitness/Base.pm and ProgrammeDriven.pm fall back to width_inside [4.0, 1.0] for any programme space without an explicit width key. Fix the Perl oracle to match the native behaviour (same sqrt(size/proportion) formula).

Fixed: get_space_params now derives width from sqrt(size/proportion) when no explicit width key is present. 34/36 corpus files score higher with the fix; all 111 tests pass after rescoring with URB_NO_OCCLUSION=1.


Programme width defaults: t3 contradiction (impossible width_inside default)

DESIGN.md §8.2, confirmed in source. t3 (3 m2 WC) has no width spec so inherits width_inside [4.0, 1.0] (Fitness/Base.pm:60) — geometrically impossible; designs 'pass' only by failing size instead. Fix AFTER faithful-port validation (port-faithfully-first policy, §8.1): a sane width default scaled to area (e.g. sqrt(area/proportion)) or per-room widths in patterns.config. Applies to native fitness; optionally upstream to Urb.

No programme space has a default width incompatible with its target area; corpus re-scored and effect documented

Fixed in get_space_params: when a programme space has no explicit 'width', derive target from sqrt(size/proportion) instead of falling back to width_inside [4.0, 1.0]. Re-scored 35-file corpus: 32 files improved (+1-121%), 5 files lost spurious width fails. All 109 tests pass. Upstream Perl fix tracked as homemaker-py-8fe.


Penalty reshaping: replace 0.5^n while preserving inner-loop protection

DESIGN.md §4.7, §5.4, §7 Phase 4, §8.5. The 0.5^n cliff gives the outer search no gradient and rewards flag-count over geometry, but it also PROTECTS the inner loop from trading into new failures (§4.5). One fitness shape cannot naively be both soft outside and cliff-protected inside. Candidates: cliff-inside-inner-loop only, lexicographic (failure count first, score second), additive/soft, multi-objective Pareto. Must preserve the missing-space failure hierarchy (worse to drop a room than to have a poor one). Measure landscape + search outcomes; this helps Urb today too.

Chosen scheme documented with measurements: search improves while inner loop still never trades into new failures

Implemented lexicographic outer-search comparison (-n_fails, fitness). Inner loop unchanged (0.5^n cliff protection preserved). Experiment penalty_reshape.py confirms 0/9 fail regressions in inner loop and shows lex avoids the 3-fail trap that scalar hits 1/3 of the time. Fixed stale _CHILD_INNER_KW sigmas entry.


Experiment: inner-loop slack-expansion objective term

Inner-loop counterpart to plot-fill construction. If Diagnostic B shows the inner loop has room to expand leaves into slack but no objective gradient to do so (the scalar rewards hitting target area but not exceeding it where slack exists), add a term/incentive so the ratio optimiser pushes leaf boundaries out to consume neighbouring slack and satisfy size, rather than parking at target.

CONDITIONAL on Diagnostic B: build this only if B localizes the gap to the inner loop (room to expand, no gradient); if B shows construction targets too-small dims, prefer the plot-fill construction sibling. Must preserve the §5.4 inner-loop cliff / §4.9 lexicographic protection — the term sits where it cannot displace the fail-count ordering. A/B vs §12.2 baseline, seeds 0/1/2, 20000 evals, staged, default-OFF. Record DESIGN.md §13.6.

DEPRIORITISED by Diagnostic B (§13.2). B shows the inner loop CANNOT repair undersize: the slack is depth-driven maldistribution baked into the frozen topology, and the equal-offset ratio DOF cannot shrink a 14x leaf to feed a starved one without trading into shape fails (0.5^n cliff). Wrong DOF and wrong direction — the blocker is slicing POSITION, not a missing expansion reward. Fix belongs upstream in construction/topology (erc.4 re-scoped, erc.3). Keep as a low-priority follow-up only if a depth-balanced construction still leaves a residual size gradient the inner loop could pick up.


Experiment: compactness-aware cuts (minimize leaf perimeter/area)

Attacks the #1 factor, crinkliness (346) — a per-leaf perimeter/area property DISTINCT from proportion (aspect ratio). Proportion-aware seeding (leu.2) sizes splits but does not bias toward balanced, square-ish subdivision. Add a KD-tree-style 'keep both children compact' cut rule (prefer the cut orientation/position that minimises summed child perimeter/area) in construction.

CONDITIONAL on Diagnostic A: if A shows per-leaf shape-fail is FLAT across densities (floor intrinsic to slicing density), better cuts at the same leaf count will not pay → this should be closed wont-fix in favour of leaf-sharing. Only build if A shows shape-fail RISES with density. A/B vs §12.2 baseline, seeds 0/1/2, 20000 evals, staged, default-OFF. Record DESIGN.md §13.5.

DEPRIORITISED by erc.1 verdict (§13.1): per-leaf shape-fail flat vs slicing density and cuts already squarest (_size_divisions_from_targets picks squarest rotation) yet still ~1.8 fails/leaf => little compactness headroom at fixed leaf count. Floor is intrinsic to leaf COUNT, not cut quality. Revisit only if leaf-sharing (erc.3) underdelivers.


Rebuild occlusion/daylight/sun subsystem in Python (post-Phase-5, after optimisation fully native)

DESIGN.md §6 port scope — a whole subsystem, not a term. quality_daylight (Leaf.pm:281-296) needs Urb::Misc::Sun + Urb::Field::Occlusion (+CIESky); quality_uncrinkliness also takes the occlusion object. Indoor spaces return 1 for daylight; cost is outdoor spaces + crinkliness. Port Sun_horizontal (262980-minute normalisation) and the occlusion wall set from Dom->Walls.

Daylight and crinkliness factors match Perl (float tolerance) across the corpus, including multi-storey cases

Re-scoped 2026-06-12: occlusion disabled in the Urb oracle instead of ported (see homemaker-py-gp2). Native fitness ships with simple crinkliness (illumination factor = 1, in homemaker-py-gnw). This issue is now the eventual Python occlusion rebuild, only after optimisation works entirely in Python. Restores outdoor-daylight and shaded-wall selection pressure.
Reframed 2026-06-17: orthogonal to epic homemaker-py-c4c. This is fitness FIDELITY (restoring daylight + shaded-wall selection pressure to match Perl), not search CAPABILITY — it changes what 'good' means, not the search's ability to find good. It will NOT improve final designs in the sense currently sought. Stays P4, deferred until the topology-search-quality epic lands and optimisation is fully native.


Multi-storey staircase consistency: when dividing or retyping a circulation (C) leaf at one level, the same structural change should be propagated to the matching leaf on ALL other storeys so the stair core path is maintained. The optimizer cannot fix staircase disruptions through trial-and-error geometry alone — it requires a synchronized multi-level operator that applies the same topology change to every storey simultaneously.