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.

NULL/negative (DESIGN.md §14). Island model (N=4 converged elites -> crossover-heavy migration) does NOT beat best-of-N at equal total budget. harbor: island 68 vs control 67 (within parallel noise); maple: island 124 vs control 116 (decisive loss). Mechanistic child_probe: crossover across converged elites rarely synthesizes — only 1/65 (harbor) & 3/63 (maple) children beat the BETTER parent, max fail-drop 2-5. Confirms alignment hypothesis (area-matched splice across non-canonical 9gp encoding is disruptive). 3rd search-machinery null. best-of-N at Phase-A budget stays a free habit; dedicated migration phase not worth its budget.


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.

CORRECTION: first PH sweep accidentally warm-started from c964…dom (finished design) → floored at 3 fails, not a blank-slate test. §11.5 reproduce-cmds seed PH from init.dom (bare plot). Fixed run_6zy_ab.sh PH_SEED_FILE=init.dom; init.dom verified blank-slate (9 fails @3000 evals). Re-running 30 PH cells from init.dom; 6 harbor cells (correctly used init.dom) kept. Harbor result stands: niche=ON uniformly WORSE than OFF at every k (k2 72→83, k3 77→82, k4 67→75); within niche=ON higher k helps monotonically but never catches niche=OFF; best overall n0/k4=67 (1 seed, within noise). pop_distinct confirms niching bites (16/16 vs 7-10).

Negative result (acceptable per acceptance criteria): joint niche×k sweep shows no (niche,k) cell beats the legacy (off,k=2) baseline. Blank-slate programme-house (5 seeds) baseline mean 4.80 fails is best of grid; every k>2 and every niche=on cell is 6.0-7.0 (within ~1 sd, but central tendency strictly worse). Harbor (staged, seed0) niche=on uniformly worse at all k. Niching bites (pop_distinct 16/16 vs 4-11) but sharper pressure does NOT convert it to lower fails — §11.5 'diffuses effort' null is robust to selection pressure. Plateau is reachability-bound (confirms §11.4/§11.5). Knob tournament_k kept default-off-equivalent (k=2). Written up DESIGN.md §11.8.


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.

wont-fix (DESIGN §13.7): Diag B (§13.2) showed the inner loop cannot repair undersize (wrong DOF — slicing position, frozen-topology ratios). Superseded by depth-balanced construction (erc.4). Condition unmet.


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.

wont-fix (DESIGN §13.7): Diag A (§13.1) showed the floor is intrinsic to leaf COUNT not cut quality; revisit condition was 'only if leaf-sharing underdelivers' but leaf-sharing OVER-delivered (−32…−39%, §13.3). Condition unmet.


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.


ld2/§13.6 interior-O seed diagnostic: ALL crinkliness fails in the constructed bal+share seed are UNDER-exposed (crink<0.62, landlocked rooms with no facade + no uncovered-O neighbour) — zero over-exposed sliver fails. So the erc crinkliness residual is genuine under-daylighting, validating the interior light-well premise. Default outside_divisor=6 was too sparse (null: harbor 147->142, crinkliness even rose). odiv=3 is the seed-optimal joint setting: harbor seed fails 147->129 (-18), maple 219->206 (-14), landlocked fails drop, at cost of more leaves (harbor +4, maple +8). Because it ADDS leaves it carries the §13.4 wash-out risk; A/B to convergence pending.


Proportion-aware constructive seeding (leu.2/§12.2): sizing seed cuts from target AREAS only regresses (thin slivers wreck aspect); you must ALSO pick each cut's rotation for child squareness. It is a convergence ACCELERATOR via a deeper local optimum around the constructed topology: wins where that topology is roughly right and budget is scarce (harbor -13%, maple -10% at 20k evals) but DELAYS small programmes where the seed must be restructured by undivide (programme-house regresses at fixed budget, yet reaches the floor given budget - speed, not asymptote). Default-on. Also: n_storeys must honour storey_minimum, not just level: keys (programme-house storey_minimum:2, all rooms level:0 - was seeded 1 storey short; cq1).


Urb fitness bug found+fixed 2026-06-12 (patch in /home/bruno/src/urb, uncommitted): ProgrammeDriven.pm ratio_o/ratio_type grepped case-insensitively over the ratios hash and took the FIRST key — nondeterministic (x4.5 score swings) for designs with mixed-case type classes (both 'c' circulation and 'C' covered). Fixed to SUM the class (matches Is_Circulation//Is_Outside semantics); 35/35 corpus scores unchanged. CRITICAL for homemaker-py-3y7/gnw: the native port must implement class-SUM ratios. Building.pm has the same unpatched pattern (site-driven path, not used by our oracle). Also: the memetic search reward-hacked this bug before the fix — search results predating it are noise artifacts.


Adjacency in binary slicing tree is structural, not geometric: the inner-loop NM cannot fix topological adjacency failures. Two paths exist: (1) tree-sibling adjacency — a node is adjacent to its sibling in the tree; (2) cross-zone geometric adjacency — leaves from different subtrees that happen to share a boundary. Staircase/adjacency fails require a topology mutation that changes which nodes are siblings or which zones touch. This was proved empirically on programme-house: staircase fail from rot=0 layout could not be fixed by NM but was fixed by level_retype creating a two-C topology (2026-06-14/15).


Deceptive valleys in topology search: when every single-step mutation from a target state passes through a high-fail intermediary (e.g. level_fix displaces a room into 5+ new fails), a compound operator that atomically applies two coordinated changes can escape. Design compound operators to land on the low-fail state directly, bypassing the deceptive gradient. Programme-house example: level_compound_fix atomically moves the level-constrained room AND re-inserts the displaced room adjacent to C in one step (operators.py, 2026-06-14).


Island model (psk, §14) is a NULL: priming a population from N converged independent elites + crossover-heavy migration does not beat best-of-N at equal total budget (maple island 124 vs control 116). The child_probe instrument shows WHY: area-matched crossover across independently-converged elites almost never synthesizes (1-3 of ~64 children beat the better parent, max drop 2-5) because the slicing encoding is non-canonical (9gp), so splices are disruptive not combinatorial. Search-machinery null #3 after graded-objective and niching/restarts; residual stays geometry/shape-bound.


Run-to-run reproducibility in homemaker-layout: serial search (workers=1) is byte-for-byte deterministic; parallel (workers>1) is now deterministic too AFTER fixing driver._run_batch to admit futures in submission order (was as_completed/completion order, bug xcy). Reproducibility holds only for a FIXED worker count — serial vs parallel differ because children-per-iteration is 1 vs n_workers (different batch granularity), which is expected, not a bug. The constructive seeder was NEVER nondeterministic: _assign_adjacency_aware has unique idx tiebreaks; comparing topologies with Python builtin hash() of the signature STRING is invalid (PYTHONHASHSEED salts str hashing per process) — use a stable hash (sha1) or genome.signature equality.


User preference (Bruno): this is a Fedora system — NEVER install Python packages via pip without asking first; always ask whether to install the rpm via dnf (e.g. python3-cma) before considering pip. Applies to any dependency additions.


Experiment seeding pitfall: run_search_scaled.py's default PH_SEED (c964…dom) is a FINISHED programme-house design — passing it warm-starts and floors at ~3 fails, NOT a blank-slate topology search. For blank-slate runs comparable to §11.5/§11.6 baselines, seed from examples/programme-house/init.dom (a bare undivided plot; driver bootstrap auto-triggers only on bare plots). Bit the 6zy sweep — first pass used c964 and falsely showed 3-fail floor across the whole grid.


homemaker-layout PYTHONPATH: package installed as 'homemaker-layout' via pip install -e . so 'import homemaker_layout' works from anywhere without PYTHONPATH. For running tests use 'python -m pytest' from project root /home/bruno/src/homemaker-layout (pyproject.toml adds src/ automatically). Never try pip show homemaker — that's the old homemaker-addon conflict.


Never use corpus filenames (candidate-001.dom, candidate-002.dom, generated.dom, init.dom, etc.) as --output targets when running experiments. These are test fixtures. Always write experimental outputs to scratch/ or a timestamped path. Lesson from 2026-06-14: warm-start runs overwrote candidate-001/002.dom and broke graph tests.


Programme-house optimisation result (2026-06-14/15): best achievable is 1 fail (l1 wrong level, score ~0.005). 0 fails is geometrically impossible: l1 (min 27m²) must occupy ll (~23m²) at level 0, which eliminates the t3-adj-C provider; dividing ll into lll(l1)+llr(C) gives llr proportion ~6:1 (fails). Python memetic optimizer achieves 1 fail in 50k evals vs Perl optimiser's 2-3 fails. Winning topology: TWO C nodes at level 0 — ll(C) for t3-adj-C via geometric contact, rl(C) for staircase via tree-sibling adjacency to rrr(O). Best .dom: scratch/from-warmstart-fixed.dom and scratch/from-compound3-fixed.dom.


warm_x0 initialization bug pattern: when a topology operator explicitly sets division ratios on a newly-created node (e.g. compound_fix sets node.division=[0.25,0.25] for t3), parent.ratios has no entry for that node (it was a leaf). warm_x0 defaults it to 0.5, corrupting the inner loop's starting point and making the operator invisible to lex comparison. Fix: only propagate child ratios for nodes where the parent node was NOT already divided; stale hidden nodes revealed by structural mutations (swap flipping b.below) must NOT contribute their pre-writeback values. See driver.py lines 259-267 (fixed 2026-06-14).


9o5 multi-use leaves is path (a) — superposition as SEARCH RELAXATION that COLLAPSES to specific usage at the end, NOT path (b) loose-fit/no-collapse. Bruno's intent: codes with SIMILAR leaf requirements form an interchangeable equivalence class; during evolution the solver doesn't commit which leaf serves which specific usage (smoother landscape, no fighting over exact leaf usage); at the end the layout is CONDENSED to specific usages by brute-forcing the in-class assignment (3 interchangeable usages over 3 leaves = 3! = 6 combinations to check, pick best). 'Derive automatically' compatibility = requirement-similarity grouping. This reverses the issue's stated 'path b preferred' note.


CORRECTION to urb-fitness-bug memory (Bruno, 2026-06-12): 'C' is NOT a 'covered' type — Is_Covered is a geometric predicate (indoor space above). Urb's generic types are canonically UPPERCASE: C=circulation, O=outside, S=sahn (get_space_types qw/C O S/; corpus is 100% uppercase, never 'c'/'o' leaves). The mixed-case designs that fired the latent ratio_type first-match bug were created by homemaker's own operator type pool emitting lowercase 'c'/'o' — fixed: driver/operators now emit uppercase generics only, and class checks use t[0].lower() in 'cos'. The Urb class-sum patch stays as defensive hardening (zero impact on canonical designs). Native port (3y7/gnw): treat type classes case-insensitively, generics canonically uppercase.


Urb oracle nondeterminism: urb-fitness.pl output varies run-to-run from Perl hash-order randomisation — .fails line ORDER shuffles (compare sorted, use oracle.Score.fail_lines) and the score float can flip by ~1 ULP (compare with math.isclose rel_tol=1e-12, never ==). Not a batching artifact; affects single runs too. Matters for the Phase 3 native-fitness parity gate (homemaker-py-uxz).


CLI tool style: prefer python -m homemaker.module --parameters pattern, installable via pip install -e . with pyproject.toml entry_points. Not standalone bin/ scripts.


Experiment harness gotcha: the leaf-sharing RELAXED objective (§13.3) is injected ONLY by monkeypatching fitness.load_config in the parent process (run_staged_search.py / probe scripts). This is parent-process-only and does NOT propagate into ProcessPoolExecutor workers (n_workers>1), which re-import fitness fresh and score under the STRICT on-disk patterns.config -> r.n_fails MISMATCH (worker strict vs parent relaxed re-score). ALL §13.x floor runs were therefore SERIAL. Any future PARALLEL leaf-sharing experiment will silently mis-score until leaf_sharing lives on disk/CLI (tracked: homemaker-py-x3b). The parallel driver itself is correct; both paths score via load_config(programme_dir).


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.


Strategy decision 2026-06-12 (Bruno): occlusion/daylight is ORTHOGONAL to building a scalable optimiser. Disable it in Urb (env flag, homemaker-py-gp2) rather than port it; native fitness uses simple crinkliness (illumination factor = 1); rebuild occlusion in Python only after optimisation is fully native (homemaker-py-2g5, now P4). Consequence: all scores change when the flag flips — re-baseline corpus/.score, DESIGN \$4.5 gains, gate bars at one clean boundary AFTER homemaker-py-1p0 closes; Phase-2 urb-evolve benchmark must run with the same flag.