Answering "are we clean". Generic namespace: yes. Usage namespace: no.
FINISH §39.4. The first sweep missed sites, found by a full re-grep:
graph.py's free-area budget, operators.py host-preference / keep-type /
repair-candidate, fitness.py's ("l","c","k") public-access test, bubble.py's
generic adjacency reference, and -- the important one -- cpsat.py, which was
still matching adjacency by raw startswith. graph.code_matches_requirement is
now the single public answer to "does this leaf count as the thing the
programme asked to be next to", shared by has_adjacency, has_vertical_connection
and cpsat.
RETRACT §39.5. It concluded 2g7.5's CP-SAT seeder win did not survive the
correction. That was wrong. The cause was the missed cpsat matcher above: the
exact solver was optimising a different relation than the scorer checked, so a
failing test reporting an incomplete sweep was misread as a baseline shift.
Re-measured over 6 seeds, cpsat now wins on both programmes (harbor 102/92,
maple 156/154). xfail removed.
REAL BUG UNDERNEATH: CP-SAT was never deterministic despite
num_search_workers=1 and a comment claiming it. neighbors[slot] is a set of
dom.Node, which hashes by id() -- a memory address -- so raw iteration made the
model-build order vary and CP-SAT returned a different equally-optimal
assignment each run (measured 194/180/171/182 over four identical aggregates).
sorted() on the slot indices fixes it. Also paired the wall-clock cap with
max_deterministic_time (solves run ~124ms against a 2s cap, so nothing was
timing out -- latent hazard, not the cause). solve_room_labels is now
reproducible on every captured instance; constructive_topology on the cpsat
path still is not, filed as homemaker-py-fdp (plausible contributor to b8g).
§39.6 THE SECOND NAMESPACE. Usage prefixes b/t/l/k (bedroom/toilet/living/
kitchen) classify programme codes by first letter and stay prefix-based by
design, but they are not inert: has_circulation deletes graph edges from them.
Four corpus rooms are misclassified by spelling -- la1 "Laundry Room" and li1
"Library Corner" as living, br1 "Staff Room" as bedroom, tr1 "Treatment Room"
as toilet. Measured on a health-centre seed: tr1 loses its edge to the adjacent
O, br1 loses its edge to t10 "Staff WC" -- both feed the connectivity fails §38
found persisting. Filed homemaker-py-sel; an explicit usage: key is the fix,
but it changes fitness for correctly-spelled programmes too so it needs its own
A/B.
DOCS. README gains a "Room codes and reserved names" section; CLAUDE.md and
AGENTS.md gain the same summary for agents. audit_programme_config.py now
reports the usage class each code picks up alongside the namespace and
satisfiability checks. DESIGN §37.2's note calling the c/o/s quirk "existing
product behaviour, not a bug" is annotated as superseded.
Corpus audit: zero generic-namespace violations across all ten example
programmes. 346 passed, same 7 pre-existing fixture failures, lint unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
Supersedes the previous commit's approach. Renaming harbor's four colliding
codes fixed one programme; tightening the matching rule fixes the rule, so a
room may be called anything. cr1/of/st1/st2 are restored and the examples are
byte-identical to their pre-§39 state -- which also means existing .dom
artefacts (evolved-3M*) stay valid, so migrate_ju3_rename.py is deleted.
The rule: Urb has exactly three GENERIC structural types (get_space_types:
qw/C O S/), the leaves the search creates. Measured across the corpus: 154 C,
110 O, 1 S, not one lowercase generic -- while every programme code is
lowercase, including single-character ones (r, t, m, n). Case is the
discriminator, not length. Every generic test was type[0].lower() in (...), a
case-insensitive PREFIX that swept up any programme code starting with those
letters; they now match the generic set exactly. 30 sites across dom, fitness,
graph, operators, programme, shapecurve and bubble.
NOT applied to the SEMANTIC prefixes: l/k/b/t classify programme codes by first
letter (graph.py builds bedroom<->toilet and kitchen<->living relations from
them) and stay prefix-based. Where the namespaces were mixed in one expression
they were split -- has_circulation's ("b","l","k","c") is three semantic
prefixes plus dom.is_circulation; access()'s ("l","c","s") is semantic l plus
the generic circulation set.
New: dom.GENERIC_{CIRCULATION,OUTSIDE,TYPES} + is_generic(); fitness.
_generic_class(), replacing the _t0 dispatch in quality_size/quality_width/
quality_proportion/value_rate -- the four terms that mattered most and that a
first sweep missed, since they dispatch through a t0 variable rather than an
inline test. graph._adjacency_target resolves a generic adjacency requirement
(programmes write "adjacency: [c, o]") to the generic set while every other
requirement keeps Perl's prefix semantics.
Two subtleties: S is in both generic sets but takes the OUTSIDE parameter
families -- a first translation tested circulation first and silently gave S
the circulation params, caught by test_get_space_params_sahn_proportion. And
validate_codes survives, narrowed to a code spelled exactly C/O/S, which is a
genuine ambiguity; merely starting with c/o/s is now fine.
Invariant asserted as a test: test_scoring_is_invariant_under_programme_code_
spelling relabels one tree and its config together and re-scores. Bit-identical
across 12 comparisons (6 seeds x collapse on/off).
Re-baseline (seed 1, 20k, original names): 58 fails (15 hard / 43 soft) against
the real 37-instance programme, with cr1 at 79.1 m2 vs declared 80 (was 32.9
and 17.1), of/st1/st2 all present and in band, and one fail naming any of them.
57 -> 58 on a 5-instance-harder programme is within noise: "did not regress".
Fallout (§39.5): 2g7.5's CP-SAT seeder win does not survive. Over 6 seeds --
harbor real 102/114 (cpsat loses), harbor old-effective 98/99 (tie, so the win
was already marginal), maple-court 156/144 (cpsat wins). maple is the control:
the solver did not regress, harbor's programme changed. Test xfail'd with that
reason plus a maple companion; both assign_solver flags stay default off.
Filed homemaker-py-w6x to re-check other narrow-margin harbor A/Bs.
345 passed, 1 xfailed, same 7 pre-existing fixture failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
Adds src/homemaker_layout/cpsat.py (OR-Tools CP-SAT) as an exact alternative
to operators._assign_adjacency_aware's greedy/beam room-code placement,
wired in as assign_solver="greedy"|"cpsat" (EXPERIMENTAL, default "greedy",
byte-identical to before) through constructive_topology/lift_base_to_storeys/
driver.search, plus a new operators.mutate_reassign in-search repair
operator (driver.search's enable_reassign=False default, mirrors
enable_ruin_recreate). Both found and fixed a resize-fragility bug (a
second CP-SAT pass against settled geometry, operators._cpsat_relabel_settled)
and a CP-SAT symmetry-blowup stall (explicit interchangeable-code grouping).
Seeder-level A/B on harbor-house is a solid, low-noise positive (~13% fewer
real fitness-scored secondary-adjacency fails, 10 seeds). Full driver.search
A/B is only pilot-scale (budget=3000 vs the bead's own 20k target) and
inconclusive -- both flags stay default-off pending a larger-N confirmation.
Full writeup: DESIGN.md §37.7. Bead left in_progress (own acceptance
criteria not fully met); homemaker-py-5bv tracks the deferred post-collapse
repair item.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
Composer half of the ground-truth bead: examples/harbor-house/drawings/
harbor-house 1.svg turned out to be a Bonsai/Blender render of 3m.dom's own
IFC (32 IfcSpace paths == 3m.dom's upper-storey leaf count), not a human
trace, so no usable reference exists yet -- this builds and tests the
pipeline that will consume one once traced. compose.py parses storey-N
Inkscape layers of cut-lines + labels against a boundary-stub .dom (plot/
height/elevation only, no room shapes to keep aligned across storeys) and
recursively detects guillotine cuts, mirroring geometry.py's own
division-line algebra; non-slicible regions and label mismatches are
reported by location rather than guessed at. homemaker-compose CLI added.
Renamed dom._link to public dom.link since compose.py needs to re-link from
outside dom.py. Full design writeup in DESIGN.md sec 37.3; actual human
tracing of harbor-house/programme-house is tracked as follow-up under
2g7.1, still open.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
operators._assign_adjacency_aware gains beam_width (default 1 = exact
prior greedy behaviour), threaded through constructive_topology/
lift_base_to_storeys/driver.search/search_staged as
construction_beam_width. Verified functioning on an adversarial
synthetic case, but byte-identical raw-seed output to greedy at every
width tested (1/4/8/20) on both example programmes -- no headroom for
the beam to find on this repo's programmes. DESIGN.md section 29.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8agJBT2ZpmF3ErW7wi2wY
Adds operators.mutate_bridge_circulation: retypes the cheapest path between
two disconnected circulation components to circulation, directly clearing a
'level N not connected' fail instead of relying on the qi6 graded comparator
key (measured negative, DESIGN.md §18). Gated off by default via
driver.search's enable_bridge_circulation flag and evolve.py
--bridge-circulation, mirroring enable_reassociate's clean-toggle pattern.
qi6/qpk-protocol A/B (DESIGN.md §21) is directionally positive but mixed at
N=3/N=5 (never worse on total fails; clears 2/5 baseline not-connected fails
vs qi6's 0/4; one seed's RNG-trajectory divergence adds 2 new not-connected
fails) — kept default off pending a larger-N confirmation sweep
(homemaker-py-qjg) and a mutation-weight bump experiment (homemaker-py-lj3).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDZjAATDWW1xFfc7xnJqSt
Diagnosed the geometry-intrinsic residual from 94g's collapse: ratio
re-optimisation isn't the bottleneck (1500-eval NM makes zero difference on
the 12-fail collapsed best layout); the causes are upstream area starvation
and cut-orientation mismatch. Added mutate_shape_rotate/mutate_deslim
targeting each, gated on a Fitness instance like the existing reqs-gated
repair ops.
Evaluated as a finish-time exhaustive hill-climb on the same 6-layout
harbor-house sweep 94g used: zero improving moves found anywhere — every
candidate move traded the shape fail for a new adjacency/access fail on the
co-evolved layout (§4.2's lesson, now confirmed for topology repair). Closes
homemaker-py-7fm; spun homemaker-py-161 for the open in-search-GA question.
See DESIGN.md §19 for the full writeup.
Ramp the leaf-share grain down within one continuous run (e.g. 4->3->2->off),
carrying the whole population across each step — graduated non-convexity over
the single hard sharing->off transition of the §15 finish.
- operators.unfold_shared_leaves(above=cap): unfold only leaves whose share
exceeds the new grain cap, leaving smaller-share leaves collapsed for the
next step. above=1 (default) keeps the full-unfold §15 behaviour.
- driver: max_share override threaded through _overrides_for/_fitness_for/
_evaluate so a phase can rebuild the evaluator at a lower leaf_share_max cap;
search(seed_pop=) evaluates an explicit initial population so a phase hands
its whole population to the next instead of restarting from a single best.
- driver.search_annealed: one phase per descending grain then a de-share
polish; unfold-above-cap between steps; cumulative accounting + grain-tagged
history; honest canonical best (byte-for-byte verified vs homemaker-fitness).
- evolve: --anneal-grain LADDER CLI (self-finishing; §15 finish not applied).
8iv settled the primitive (grid unfold beat the circulation-aware slice), so
the ramp reuses the plain balanced-grid unfold at every step.
Tests: unfold above-cap selectivity, seed_pop seeding, search_annealed phase
stitching / honest finish / degenerate-ladder fallback. 258 pass. DESIGN §16.
Head-to-head A/B on harbor-house still to run; verdict pending (issue open).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8566xAxTnwtJTkpXjYNZm
operators.unfold_shared_leaves(): materialise each live shared leaf
(share=k) into k distinct same-code sibling leaves, splitting its
footprint into k equal-target children with squarest per-cut rotation
(_size_subtree_equal), clearing the share stamp. Surgical — siblings'
evolved geometry is untouched.
Investigation result (harbor-house, evolved-3M sharing seed):
- unfold alone (zero search) closes all 15 critical missing-room fails
and lifts the canonical score 6.73e-29 -> 1.46e-19 (90->59 fails).
- warm-starting a --no-leaf-sharing evolve from the unfolded seed runs
~7-8 orders of magnitude ahead of the naive (un-materialised) warm
start at equal budget. The count deficit, not the sizing, was what
stranded Schedule A deep in the fail hole.
Adds test_unfold_shared_leaves_materialises_deficit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8566xAxTnwtJTkpXjYNZm
§13.6/ld2 verdict: interior-O light-well seeding is net-positive — harbor
-16.4% (all seeds improve), maple net-neutral (-2.8% mean, no programme
regresses). Mirror the pll bal+share flip: default interior_outside
False->True in driver.search/search_staged and operators.constructive_topology/
lift_base_to_storeys (outside_divisor stays 3). The experiments INTERIORO
A/B override is unchanged. test_interior_outside_… now pins the peripheral
baseline to interior_outside=False explicitly. 215 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seed O as interior light wells (most-landlocked leaves first, count scaled
by room count via outside_divisor) instead of one peripheral O, attacking the
erc crinkliness residual: seed diagnostic confirms every crinkliness fail is
under-exposed (landlocked), none over-exposed.
A/B (20k evals, seeds 0/1/2, bal+share stack, §13.6): control reproduces §13.5;
interior odiv=3 gives harbor -16.4% (all seeds improve) and maple -2.8%
(net-neutral). Default-optimal divisor 3 found by seed sweep (6 was null).
Lever default OFF; default-ON flip tracked as erc.8.
- operators: interior_outside + outside_divisor through constructive_topology,
lift_base_to_storeys, _assign_adjacency_aware (fix n_circ budget for >1 O)
- driver.search/search_staged threading; run_staged_search.py INTERIORO/ODIV env
- test_interior_outside_seeds_landlocked_wells_and_scales_count
- experiments/run_interioro_ab.sh; DESIGN.md §13.6
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the area-derived share recovery with explicit, type-guarded per-leaf
multiplicity: construction stamps leaf.share=k and leaf.share_type=code; the
fitness (graph.leaf_share) honours k only while leaf.type==share_type, so any
retype/undivide auto-invalidates a stale share — no operator resets, and a
small leaf cannot retype its way into covering rooms it does not provide. Two
Node fields survive the whole search via deepcopy (genome.decode is unused in
the hot path); .dom emits `share` only on a live shared leaf.
This closes the §13.3 missing-fail leak: floor probe missing 17–44 → 0, and the
achievable floor drops −39% harbor (120.3→73.3) / −32% maple (194.7→133.0) with
no re-emergence as size fails.
Flag threaded through driver.search/search_staged → constructive_topology /
lift_base_to_storeys, exposed via LEAFSHARE/LEAFSHAREFAC in run_staged_search.py
(injects the objective into inner-loop + final-score fitness so both A/B arms
share one programme dir). run_leafshare_ab.sh runs the staged 20k A/B.
Smoke-tested end-to-end (harbor, factor 3, re-score OK). 214 tests pass;
default-OFF reproduces baseline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same-code rooms collapse into fewer, larger SHARED leaves so the ~1.8/leaf
shape tax (§13.1) is paid once per group. Multiplicity k is recovered from
area (k=clamp(round(area/target),1,max_share)) — no genome change — and used
in two default-OFF sites: graph.check_space_counts counts coverage (Σk vs
req.count) so one leaf covers several rooms without a missing fail, and
fitness.quality_size centres on k×target (σ scaled by k). Construction:
operators._share_rooms groups instances; _size_divisions_from_targets sizes
shared leaves to k×target via leaf_mult.
Floor probe (experiments/diag_leaf_sharing.py, harbor+maple, seeds 0/1/2,
+innerloop): total fails −27% harbor / −16% maple at share3, shape factors
fall ~linearly with leaf count (confirms §13.1). Cap: 17–44 missing fails
leak because depth maldistribution (§13.2) keeps shared leaves below k×target
so round() undercounts; inner loop can't close it. Net still positive.
Default-OFF reproduces baseline exactly (214 tests pass). Driver plumbing +
staged 20k A/B remain; §13.3 records the next design fork.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Land the two evidence-supported parts of the re-scoped 9gp capstone as
operators on the existing decoded Node tree (no Polish-expression rewrite),
each default-OFF and measured against the §12.2 leu.2 baseline.
9gp.1 shape-feasibility pre-filter: operators.predicted_shape_fails lays a
topology out at its proportion-aware target geometry and counts shape fails
(size/width/proportion/crinkliness); driver._evaluate prunes clearly-infeasible
topologies before the inner loop (1 eval vs ~80), guarded so nothing that could
beat the incumbent is discarded. search/search_staged feasibility_filter,
feasibility_max_shape_fails (env FEAS/MAXSHAPE), default OFF.
9gp.2 M3 Wong-Liu reassociate: operators.mutate_reassociate adds associativity
(a|b)|c <-> a|(b|c) on same-orientation live cuts — the canonical-slicing move
missing from swap(M1)/rotate(M2), attacking the §11.4/§11.5 reachability
bottleneck. enable_reassociate (env REASSOC), default OFF (weight 0 -> baseline
byte-identical).
Unit tests (operators + driver) green, full suite 211 passed; maple-court smoke
run clean under native fitness. A/B sweep handed off per the plan; DESIGN.md
§12.3 documents the design and the pending measurement.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_assign_adjacency_aware gains fixed_circ (seed the connected-dominating-set from
given circulation leaves) and secondary-adjacency-aware room placement: codes
with the most non-c adjacency requirements are placed first, each onto the open
slot satisfying the most of its requirements against already-typed neighbours
(clustering k1<->da1, da1<->o). lift_base_to_storeys(reqs, adjacency_aware=True)
grows the upper-floor circulation spine off the inherited vertical core and
assigns rooms around it; threaded through driver.search_staged
(seed_adjacency_aware) and run_staged_search.py (ADJ env).
End-to-end staged harbor, 20000 evals, mean total fails over 3 seeds:
ADJ=0 99.0 (reproduces the §11.4 staged lex baseline exactly), ADJ=1 85.3
(-13.7, -14%; best 78). New best harbor configuration overall: staged baseline
99.0 -> single-stage adjacency-aware (§11.6) 90.7 -> staged + adjacency-aware
lift 85.3. Staging and adjacency-aware seeding compose.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
operators._assign_adjacency_aware spends ~one extra leaf per three rooms on a
greedy connected-dominating-set of circulation leaves (read from the geometric
leaf_graph, type-independent), so every room borders a connected circulation
spine and adjacency-to-c + access are satisfied by construction. Default-on via
constructive_topology(adjacency_aware=True), threaded through
driver.search(seed_adjacency_aware) and run_search_scaled.py (ADJ env).
End-to-end single-stage, 20000 evals, mean total fails over 3 seeds:
harbor 110.0 -> 90.7 (-17.5%; ADJ=0 reproduces the §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 best of 95 — the first Phase-6 fail-count
reduction from seeding. Follow-ups (lift_base_to_storeys, secondary adjacencies)
filed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the required programme room set a constructive invariant instead of
something the topology search must stumble onto by random divide+retype.
- operators.constructive_topology: bootstrap seeder that sizes each storey to
its required rooms (partitioned by level; level-free rooms distributed),
+1 core C and +1 O per storey, then assigns types. Stochastic for population
diversity. Wired into driver bootstrap when the programme has required spaces.
- operators.mutate_place_missing: repair op that inserts a missing required
space by dividing a host leaf into [room | remainder]. Lex-safe host ranking
(generic O first, never displace a required room); honours required level.
Weight 2.0 in the mutation mix; noops cheaply once the set is complete.
A/B on harbor-house (20k evals, seed 0, identical config):
old random-bootstrap 133 fails (103 missing, 77%)
new constructive 105 fails ( 12 missing, 11%) -21% total, missing-stack
collapsed; seed head-start 163->139.
§4.10 regression PASS: warmstart-2f4 still reaches a 1-fail population at 50k.
Verdict (DESIGN.md §11.2): construction is necessary and reframes the
bottleneck to quality-fail packing of a complete dense design (crinkliness/
size/access/edge) -> unblocks §11.3 staging, motivates §11.4 graded objective.
Follow-up filed (homemaker-py-s44): adjacency-aware seeding.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copy programme-house corpus (36 .dom + .score + .fails + patterns.config)
into examples/ and update all 5 test files to use project-relative paths.
Native Python fitness (use_native=True) was already the default; tests now
run without /home/bruno/src/urb present.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
run_urbevolve took (seed, budget, pop, cell) but cells call
fn(seed, budget, cell, **kw) — every urb-evolve cell died on TypeError,
deferred silently by pool.map. mutate_swap lacked the empty-candidates
noop guard the other operators have, crashing on init.dom-style bare
plots. Regression test: every mutation survives an undivided tree.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bruno's correction: 'C' was never a 'covered' type — Is_Covered is a
geometric predicate. Urb generics are canonically uppercase (get_space_types
qw/C O S/; corpus 100% uppercase). The driver/operator type pool emitted
lowercase 'c'/'o', creating mixed-case designs that fragmented Dom->Ratios
class buckets and fired the latent ratio_type first-match nondeterminism
(which the search promptly reward-hacked). Operators now emit uppercase
generics only and class checks match case-insensitively (t[0].lower() in
'cos', cf. Is_Circulation/Is_Outside). The Urb-side class-sum patch remains
as defensive hardening, zero-impact on canonical designs (35/35 parity).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
operators.py (homemaker-py-nyb): divide/undivide/retype/swap/rotate/
level_add/level_delete + Urb-style area-matched base-storey crossover.
Operators edit the decoded Node tree; genome.encode absorbs all repair
(dangling deltas, storey misalignment) so every child is a valid genome
by construction. Geometry moves deliberately absent — the inner loop owns
continuous DOF, and 8cs made Lamarckian re-optimisation mandatory.
Fixes dom._link to CLEAR stale below-links when a path vanishes from the
storey below (undividing a base branch left upper nodes pointing at
orphaned quads; oracle scoring unaffected but in-process geometry crashed).
Acceptance (experiments/operator_locality.py, flag-on): 115/115 children
scored without error; geometry perturbation small for core ops (retype
0.07, divide/undivide 0.14, swap/crossover 0.16-0.17), fitness
perturbation large for all (0.68-0.99 rel) — the 0.5^n cliff flags most
raw moves, confirming warm-started re-optimisation + penalty reshaping
as the load-bearing design choices. 27 tests pass.
Closes homemaker-py-nyb.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>