Commit graph

80 commits

Author SHA1 Message Date
Claude
109849e438
A terrace is no longer worth more per m2 than a real internal room
Owner's ruling. Measured over the twelve baseline layouts as realised value per
m2 (rate x quality, not the rate alone):

  as shipped before this   room  67.0   terrace 294.6   violates, 4.39x
  value_supported=100 only room  67.0   terrace  98.2   violates, 1.46x
  geometric mean only      room 132.9   terrace 296.8   violates, 2.23x
  both                     room 132.9   terrace  98.9   satisfies

The 4.4x is roughly 2.2x aggregation and 2.0x rate, so neither half alone is
enough. That is why 39.18's geometric-mean aggregation moves from default-OFF
to default-ON here rather than waiting on its own A/B: it is not an optional
improvement, it is half of a ruling.

value_supported 300 -> 100, and set to value_outside rather than to a number
that makes the inequality come out -- back-solving from the corpus's measured
mean room quality would rot the moment either changed. Outdoor space is worth
the same to an occupant whatever level it sits on; the real difference between
a ground garden and a roof terrace is what it takes to BUILD, and cost already
says that (outside 10.0 vs outside_supported 110.0). Value describes worth,
cost describes structure, and the level belongs in the second.

Changed in CONF_DEFAULTS and the four corpus patterns.config files, which all
declared 300.0 explicitly. NOT changed in harbor-house-l0 (a shape-curve test
fixture) or y51-sweep-* (historical fixtures that exist to reproduce past
measurements) -- repricing those would destroy what they are for.

Neither change can move a fail, structurally rather than luckily: value rates
never enter fail emission, and evaluate_leaf emits each fail from its factor
before anything is combined. Verified corpus-wide: identical fail sets, scores
+11% to +169% (and -5% once, on a layout that is mostly terrace).

tests/test_terrace_value_ruling.py pins the ruling as an invariant of the
objective, and asserts that reverting the aggregation breaks it again, so
neither half can be quietly dropped.

The 500k cold-start baseline (39.12) is superseded -- this changes what "good"
means. The layouts stay valid and their fail counts are unchanged, but a fresh
corpus run is needed before any new number is compared with them.

Still untouched: circulation returns 0.07 per unit cost against a room's 0.66,
by far the worst thing a building can contain. That is homemaker-py-hxi.

Refs homemaker-py-ecx.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-09-05 19:25:02 +00:00
Claude
a76ed3b9b8
Quality aggregation: divide out how many questions a leaf was asked
39.17 left the search's storey choice unexplained and blamed value_rate. It is
not the rate, or not only.

Measured over the twelve baseline runs, value/cost by leaf kind: outside ground
7.40, roof terrace 2.69, room 0.34, circulation 0.02. A terrace returns 2.7x
its cost where a room returns a third of it, so filling upper storeys with
terrace is not the search leaving value on the table -- it is by a wide margin
the most profitable thing the objective offers. 7% of the corpus area produces
32% of its value.

Most of that gap is mean quality: 0.986 for a terrace against 0.223 for a
room. Quality is a PRODUCT of factors and the kinds are not asked the same
number of questions -- an outside leaf is exempt from size, crinkliness and
access, so 3 of 7 factors can ever bite it against a room's 6. Each exemption
is individually right (no programme size target; uncovered outside is lit by
definition; ground-level outside needs no access). The consequence is not: a
leaf exempt from the two harshest factors out-scores one judged on them and
doing well, purely by not being asked, and quality multiplies the value rate.

Stated generally, and this is not about outside space: under a product, adding
any new quality criterion mechanically devalues every leaf it applies to,
including leaves that score 1.0 on it. The objective's scale should not depend
on how many things it measures.

quality_aggregate="geometric_mean" (default OFF, "product" is stock) divides
that out. Computed in log space so six small factors cannot underflow the
product before the root is taken; a zero factor still gives zero, so a fully
buried leaf is worth nothing either way.

Telling "exempt" from "asked and scored 1.0" needs factor_is_asked, which
restates conditions that live inside the quality_* methods. That duplication
can drift, so tests/test_fitness_aggregate.py pins it against every leaf in the
corpus: wherever the predicate says exempt, the factor really is 1.0.

Fail set byte-identical everywhere, and for a stronger reason than 39.13/39.14
had: evaluate_leaf emits each fail from the factor itself before anything is
combined, so no aggregation can move one. Score effect +37% to +169%, reaching
all four programmes where the crinkliness changes reached two; room value/cost
0.34 -> 0.66, circulation 0.02 -> 0.07.

Deliberately not fixed: a terrace still out-earns a room 4:1, which is the
rates (value_supported = value_inside = 300 against costs of 110 and 200), not
the aggregation. That is a design judgement for the programme author, and
39.16 is a standing reminder that "this inherited constant looks wrong" has
been wrong twice already in this section. Left open on ecx with the numbers.

A/B running; verdict to follow.

Refs homemaker-py-ecx.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-09-05 18:57:00 +00:00
Claude
f5612ce06c
Pre-flight: check the ground floor's frontage, not the per-storey average
39.16 relocated the crinkliness residual to a plan-form question. Four
measurements answer it, and two of them refute the premises 773 was filed on.

The search DOES build courtyards -- harbor 8 (273 m2), maple 16 (404 m2),
health-centre 13 (107 m2) over three seeds -- and they work: of 524 lit edges
44% come from the plot wall, 30% from a courtyard, 26% from a perimeter void,
and a courtyard supplies at least one side for 53% of the two-aspect leaves.
No operator is missing. (The shape-curve DP does NOT model exposure --
shapecurve.py:25 -- but per 38.24 it fires ~8 times in 500k evals, so that gap
is not what is costing anything.)

The answer is per-storey. Comparing each storey's demand, sum A_i/(1.6202*h),
with the lit wall its leaves actually hold: every harbor and maple ground floor
is below 1.0 and every top floor above 1.2, and the ratio predicts the fail
rate almost exactly -- above ~1.2 near-zero fails, below 1.0 40-55% of the
storey. health-centre and programme-house sit at 1.6-4.2 throughout and fail
essentially nothing.

That corrects 39.11, which divided demand evenly across storeys and concluded
harbor and maple were frontage-feasible "with room to spare". Programmes pin
rooms to level 0 and the ground floor cannot set itself back to buy perimeter:
harbor's pinned 347 m2 needs 71.4 m against the plot's 53.0 m, maple's 414 m2
needs 85.2 m against 55.0 m, while health-centre and programme-house have 51.0
and 14.2 m spare. Same ordering as the corpus fail counts, and fixed before any
search runs. The averaged check is not just weaker: on maple it asks for a
22 m2 courtyard where the ground floor needs 57 m2.

New third _preflight check, advisory like the others, silent on the two
programmes with slack. tests/test_evolve_preflight.py covers all three checks
and asserts the ground-floor figure exceeds the averaged one -- if they ever
agree, one has stopped earning its place. 39.11 annotated in place.

Also recorded, not acted on: the open space is on the wrong storey (harbor puts
50 m2 of courtyard on the starved ground floor and 223 m2 on the surplus first
floor), because value_rate pays an outside leaf above ground value_supported =
300 -- a room's rate -- against a cost of 110, with nothing tying its value to
whether it illuminates anything. Filed as homemaker-py-ecx.

411 passed, 72 skipped.

Refs homemaker-py-773.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-09-05 17:46:52 +00:00
Claude
366a047a60
Make the crinkliness factor one-sided: stop billing the daylit wall twice
The tail rescale shipped in cd392e7 is a measured NULL as a search
intervention -- 12 of 12 pairs byte-identical on harbor and maple, 8000 evals
from a plateau, not merely underpowered. Of course it is: the whole failing
tail is 0.034% of corpus value. Looking at the rest of the factor, prompted by
the owner, found something much larger above the threshold.

crink = area_outside/area = (L*h)/A, so 1/crink = A/(L*h) is the room's mean
depth from its daylit wall in storey-heights. That is the right variable for a
daylight rule, and the fail boundary it implies (1/crink = 1.62, i.e. 4.86 m at
h=3) is a sensible one that agrees with 38.3's frontage bound derived
independently. What is wrong is hanging a TWO-sided gaussian on it:

* The near side penalises a room for having MORE daylit wall than target --
  while leaf_cost's siblings edge_cost and outside_edge_cost already charge
  that same wall at exterior_wall=100 and boundary_wall=133.3 per m2. The wall
  is billed once in cost and again as lost value.
* It never earns its keep as a failure either: the over-exposed branch only
  reaches FAIL_THRESHOLD above crinkliness 21.5, and the corpus maximum is
  3.95. It has never produced a single fail; it only removes value.
* 133 of the 318 passing graded leaves in the 500k baseline (42%) sit on that
  side, mean quality 0.810.

crinkliness_shape="daylight" (default OFF, "gaussian" is stock) clips it: a
room shallower than the gaussian's peak scores 1.0, because daylight is a
sufficiency requirement and surplus is the cost model's business, not this
factor's. Clipping at the PEAK rather than at FAIL_THRESHOLD is deliberate --
it keeps the factor continuous and preserves the graded approach to the
daylight limit, where clipping at the threshold would put a 10x cliff on the
exact boundary the 0.5**n fail multiplier already steps on.

Fail set byte-identical on all 21 corpus artefacts for all four
shape/tail combinations, so stock stays a valid yardstick for every arm.
Area-weighted crinkliness quality 0.480 -> 0.513, leaf quality product
0.2722 -> 0.2831; per-artefact score +0.2%..+19.6%, and unlike the ramp it
reaches health-centre and programme-house, where the tail change was 0.000%.

Note "daylight" clips the OPPOSITE side from 38.1's superseded compact_ok,
which forgives being buried; composing either with those modes is refused.

ab_9gj_ramp.py becomes ab_9gj_crinkliness.py and takes named arms, since it
now covers both changes; its first arm is the baseline and the yardstick.

Refs homemaker-py-9gj.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-09-05 07:27:02 +00:00
Claude
cd392e77c5
Rescale the underflowing crinkliness tail so the failing region has an ordering
quality_uncrinkliness evaluates a gaussian at x = 1/crink, so its exponent
grows like 1/crink^2 and underflows a double to exactly zero below crink ~
1/15. Measured over the twelve 500k cold-start runs (39.12): 430 leaves carry
a minimum-exposure requirement, 112 fail it, and those 112 span quality
1e-300..1e-1 while contributing 0.034% of total value on 23% of the floor
area. Every value in that range is numerically zero beside a passing leaf's
~1, so the search cannot rank two layouts that differ only in how exposed
their under-lit rooms are.

This is wider than the bead's diagnosis (a flat 0.0 for zero-exposure leaves)
and it explains why 38.1's `floor` mode measured as a no-op: max(q, 0.01) maps
110 of the 112 onto one constant, replacing a flat zero with a flat 0.01.

crinkliness_tail="ramp" (default OFF, "gaussian" is stock) replaces the tail --
only the tail, only below FAIL_THRESHOLD, only on the compact side -- with a
straight line in crinkliness meeting the gaussian exactly at the crossing.
_crink_at_fail_threshold inverts the gaussian there using the same truncated
_E the factor is evaluated with.

Deliberately conservative: nothing at or above FAIL_THRESHOLD moves, so no
calibration changes and no leaf crosses the threshold. The fail set is
byte-identical on all 21 committed corpus artefacts, the four init.dom seeds
included -- asserted in tests/test_fitness_crinkliness_tail.py, not assumed.
That invariance is also what makes it legal to score both arms of the A/B
under stock (the 38.9 trap's one exemption). A fully buried leaf still scores
exactly 0; this restores an ordering within the failing region, it does not
forgive it. Composing with 38.1's superseded modes is refused, since both
rewrite the same tail.

Score effect on the baseline artefacts: +0.3%..+2.8% on harbor and maple,
exactly +0.000% on health-centre, programme-house, and every init.dom -- a
programme with no partially-exposed failing rooms has nothing to grade, and
neither does any starting layout. The ramp is a mid-search signal by
construction, so experiments/ab_9gj_ramp.py defaults to seeding each run from
a 500k plateau artefact rather than cold.

The module-level math import replaces a now-redundant local one.

DESIGN.md 39.13 and the A/B verdict follow in a separate commit.

Refs homemaker-py-9gj.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-09-05 06:29:02 +00:00
Claude
4180a9eb88
Replace the stale 15-fail acceptance target with the cold-start baseline
DESIGN.md 38.7 pinned Phase 9's acceptance test to "harbor-house reaches its
known 15-fail floor". That figure was measured before 39.4, against harbor's
*effective* programme of 32 instances -- cr1/of/st1/st2 were being read as
generic circulation and silently dropped. Against the real 37-instance
programme the same artefact scores 89, so the target is not measurable; and
2v1, the fix it was the acceptance test for, closed NULL (39.8), so there is
no combined fix left to accept.

New 39.12 records:

* the five evolved-3M*.dom artefacts rescored under the current objective
  (69/85/87/89/145), and why they are not a floor -- they were evolved under
  one objective and are scored under another;
* that the bead's migration premise is stale: experiments/migrate_ju3_rename.py
  does not exist, because ju3 tightened the matching rule at source (39.3)
  instead, so the old artefacts parse correctly with no migration;
* the 4x3-seed 500k cold-start baseline (~430 h) as the reference from here,
  with each programme's sd and its minimum detectable difference at n=3 --
  harbor mean 39.3, sd 5.5, MDD 13.7;
* zero missing-space fails in all twelve runs: the dominant term in the 3M
  artefacts is not one the live search still fails on;
* crinkliness at 112 of the 321 corpus fails (35%), all soft, all in the
  regime 9gj says quality_uncrinkliness returns a flat 0.0 for -- the largest
  single component of the residual is one the objective cannot descend;
* 66 of the 84 hard fails as one access-topology family (not-adjacent-to,
  inaccessible usable space, not connected), mechanism in 39.9.

38.7's acceptance paragraph is annotated in place rather than rewritten. The
connectivity clause is demoted to a separately tracked standing defect: it
appears in 10 of the 12 baseline runs.

tests/test_collapse_insearch.py carried its own stale "82 -> 58"; the same
layout now scores 89 -> 64, so the docstring dates the figure instead of
asserting a current one.

Closes homemaker-py-ut5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-09-04 17:16:10 +00:00
Claude
4c95505907
Shape-curve DP models leaf-sharing, so it can fire on real runs
shapecurve.leaf_constraints derived each leaf's feasible area from its own
type's base (target, sigma). quality_size does not: a leaf holding k
same-code rooms is centred on k*target with sigma*k, and a co-typed leaf
adds both codes' targets. The DP modelled neither, so eligible() excluded
leaf_sharing/max_share/multi_use -- and leaf_sharing defaults True in
driver.search, so the guard excluded essentially every real run. The DP was
correct and unreachable.

Why the guard could not just be dropped, measured before touching it: on 6
harbor constructed seeds, 24 of 24 shared leaves (100%) have a real area
outside the unscaled single-room bounds. Relaxing eligible without
modelling k would have made the DP call every one of those topologies
infeasible -- false negatives that prune feasible topologies and misdirect
the NM warm-start. The guard was load-bearing.

Fix: mirror quality_size by asking the SAME Fitness object -- k =
graph.leaf_share(leaf, fit._max_share) when fit._leaf_sharing, then
target*k / sigma*k, else fit._leaf_co_type for the additive case. Same
object, same flags, same branch order, deliberately not re-derived: 39.5's
cpsat._matches bug was a solver optimising a relation the scorer had moved,
and this is the same hazard class.

Verified as an exact inversion: for every shared leaf in a real seed,
quality_size evaluated at the DP's amin and amax returns FAIL_THRESHOLD to
1e-9 (k=3 n-leaf: bounds [128.50, 231.50], both 0.100000).

superpose stays excluded for a different reason than the others: it does
not rescale a target, it changes which type the leaf is scored as, and the
collapse happens after the DP has read leaf.type.

shapecurve_warmstart/shapecurve_prune remain default off, so no current run
changes -- including the cold-start baseline in progress. They are now
applicable, which unblocks homemaker-py-v4s.

Closes homemaker-py-tym.

Lint at parity (46); tests 387 passed (3 new, 1 legacy rewritten to the new
contract rather than deleted), 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-29 21:36:45 +00:00
Claude
57d8391c52
Recover most of the cpsat test-suite cost; keep the part that is real
38.20's cap fix took the suite from ~4.5 to ~10 min because the
assign_cpsat tests now solve to optimality. Recovered to ~6.8 min.

The bigger win was not the threading. The secondary-adjacency test ran the
cpsat arm THREE times and averaged, and its own comment says why: the cpsat
path "is not yet bit-reproducible (homemaker-py-fdp)", so one 10-seed
aggregate could straddle greedy's deterministic value and the test was
flaky by construction. fdp is fixed (38.15), so one pass says exactly what
three did -- that was work spent papering over a bug that no longer exists.

constructive_topology and _assign_adjacency_aware now forward an optional
cpsat_limits=(time_limit_s, deterministic_limit); default None keeps
solve_room_labels' defaults, so production is unchanged -- verified 24/24
harbor solves still OPTIMAL at the defaults. It is not a tuning knob: it
exists so a test whose claim does not depend on optimality can economise.
test_construction_assign_cpsat_yields_valid_seed asserts invariants only
and uses it, 91s -> 53s.

That test now also guards a real trap: too small a budget makes
solve_room_labels return None, _assign_adjacency_aware falls back to
greedy, and the test would pass while exercising nothing. It counts
fallbacks and fails if any occur.

The two quality comparisons keep the full budget deliberately -- their
claims are about the optimum, and cheapening them would weaken what they
assert. That is why the suite does not return to 4.5 min; the residue is
the honest price of optimal deterministic solves.

Also corrected a stale claim in the secondary-adjacency comment: it
measures only "not adjacent to" fails and is not a claim that cpsat seeds
better overall, which 38.20 measured markedly worse.

Closes homemaker-py-7t1.

Lint at parity (46); tests 384 passed, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-29 18:55:54 +00:00
Claude
f6eeee7191
n_workers is an algorithm parameter, not noise
14 recorded "harbor seed 2 scored 71 then 73 on byte-identical re-runs --
parallel/BLAS non-determinism", and b8g carried that forward as noise
widening the error bars on every A/B run at n_workers>1. The premise does
not survive measurement. Nothing is non-deterministic:

  score a frozen .dom, 20 repeats in one process   bit-identical
  same .dom, 8 processes, varied PYTHONHASHSEED    bit-identical
  full search, harbor seeds 0-3, n_workers 1..4,
    repeated across processes                      bit-identical PER count
  the same with OMP/OPENBLAS/MKL_NUM_THREADS=1     IDENTICAL to unpinned

The last line matters most: b8g proposed "likely a one-line env pin in the
worker pool initializer". Pinning BLAS threads changes nothing bit-for-bit,
so shipping that would have looked like a fix, done nothing, and retired
the issue.

What is real is not noise: the trajectory is a deterministic function of
n_workers. harbor seed 3, budget 1500 -- w=1/2/3 all give 64 fails with
identical bits, w=4 gives 65. Each stable across processes. The mechanism
is batch_n = min(n_workers, ...) children bred from ONE population snapshot
before any is admitted, with the shared rng consumed in a different
pattern; at w=1 each child sees the population its predecessor updated. A
4-worker run is partly generational, a 1-worker run steady-state -- same
seed, different search. Divergence is occasional (seeds 0/1/2 agreed, seed
3 did not), which is how it reads as noise when sampled.

14's observation was most likely homemaker-py-xcy, the as_completed
admission-ordering bug, which WAS non-deterministic and is fixed.

Shipped instead of a no-op env pin: driver.search's docstring states the
contract; test_search_is_reproducible_at_a_fixed_worker_count parametrises
over 2/3/4 workers, asserting each is internally stable and deliberately
NOT that they agree; test_scoring_a_frozen_design_is_deterministic guards
the floor.

The run_*_ab.sh harnesses already pin WORKERS=4, so arms inside one harness
are sound. The exposure is comparing across harnesses, or against a
historical figure whose worker count was never recorded.

Closes homemaker-py-b8g.

Lint at parity (46); tests 384 passed (3 new), 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-29 12:52:33 +00:00
Claude
148988df14
constructive_topology was ordered by memory address on the cpsat path
assign_solver="cpsat" gave a different leaf-type signature on every run
from an identical seed, in the same process. One line:

  assignable = scope if scope is not None else set(leaves)
  noncirc = [L for L in assignable if L not in circ]      # id() order

assignable is a set of dom.Node, and Node hashes by id() -- a memory
address -- so iterating it ordered noncirc, and hence room_slots, by where
the objects happened to land in memory. That shifts between calls within
one process as allocation patterns change, with no seed involved.

Only cpsat showed it. The greedy path re-sorts every slot list with -idx[L]
as a unique tiebreak and is immune to the incoming order; CP-SAT consumes
room_slots order as its model's variable order, and the labelling problem
has many equally-optimal solutions. Greedy was not more correct, it was
masking a defect that had been there all along.

Fix: iterate the tree-ordered list, use the set only for membership.

Verified on programme-house, harbor-house and maple-court: 1 distinct
signature over 5 runs on both solvers, and 1 across 4 processes started
with different PYTHONHASHSEED, so context_types' string sets are not a
second source. test_constructive_topology_is_bit_reproducible guards both.

Method: rather than guess which set was at fault, instrument
solve_room_labels with an id-free fingerprint of inputs and outputs and
isolate the FIRST call, since later calls legitimately depend on earlier
ones through leaf types. Five runs gave five distinct first-call inputs,
placing the fault upstream of the solver in one step.

Every A/B on the cpsat path was comparing arms that differed partly by
memory layout -- 39.5's cpsat-vs-greedy verdict included, already down for
re-measurement under homemaker-py-vjd. Same id()-keying hazard as the
documented geometry._cache issue and a plausible contributor to
homemaker-py-b8g, which stays open: n_workers>1 has its own BLAS mechanism
and is not addressed here.

Closes homemaker-py-fdp.

Lint at parity (46); tests 381 passed (2 new), 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-29 11:45:19 +00:00
Claude
ee050532d6
Declare toilet-to-sleeping adjacency where the brief supports it
A toilet next to a sleeping room is a positive even with no door between
them (Brand): the adjacency is what makes a later knock-through possible.
The engine already scores it -- check_adjacency runs against the unfiltered
graph_base_pre -- but only where a programme declares it, and only
programme-house did.

Declared:
  harbor-house  t -> n    bathrooms serve the Neighborhoods (communal
                          sleeping); both unpinned, 6 t / 5 n
  maple-court   tt -> r   Upper Bathrooms among Individual Rooms, both
                          level 2, already 62% adjacent at seed time

NOT declared, and checking before declaring is what caught these:

  maple t -> n is IMPOSSIBLE. Adjacency is evaluated per level, and maple
  pins t to level 0, n to level 1. Declaring it would have added six
  permanently unsatisfiable fails; the 0% seed-time rate was a hard
  impossibility, not search difficulty. maple's ground floor has six
  bathrooms and one sleeping room (Clinic Room x1) -- a ground-floor WC in
  a communal building is public, so Brand does not apply anyway.

  health-centre has no dedicated WC. The ruling was that a treatment room
  "may give access to a toilet, but this would be a dedicated toilet"; t9
  is a Public WC and t10 a Staff WC. Earning the credit needs a WC added to
  the brief -- programme authoring, filed as homemaker-py-5nw.

Both declarations are reachable (best of 8 seeds 2/3 harbor, 2/2 maple), so
the search gets a gradient not a permanent penalty. evolved-3M-nols-3
84 -> 89 fails, all five the new requirement.

Cost: cpsat assignment ~7.5x slower on harbor (0.28 -> 2.11s per seed);
greedy, the default, unchanged at 0.06s. Ordinary runs pay nothing, but
39.5's cpsat-vs-greedy verdict was measured on a cheaper problem than the
corpus now poses -- filed as homemaker-py-vjd.

Two tests were over-fitted to the old seeds and are repaired to assert
their intent, not relaxed to pass: reassign now sweeps six constructive
seeds (seed 0's better-seeded design legitimately has nothing to improve,
5 of 6 others fire), and repair_circulation asserts that repair strictly
helps plus a >=85% bar rather than a sampled 100% hardened into a
guarantee (measured 25% -> 92%, stable over 6 and 12 seeds).

Closes homemaker-py-3qj.

Lint at parity (46); tests 379 passed, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-29 10:57:44 +00:00
Claude
df84bfe1e6
Missing-space cascade no longer weighted by YAML verbosity
check_space_counts emitted, per missing room instance, two base failures
plus one placeholder for each optional key the author happened to type --
has_size/has_width/has_proportion are literally "size" in c from the YAML.
So a missing room cost 3, 4 or 5 fails depending on nothing but how
verbosely its space was written, and under value *= 0.5 ** len(failures)
that is a 4x difference in penalty between two single rooms. The tiered
comparator inherits it directly, since n_hard is dominated by these
cascades -- the search's primary key was partly a measure of config style.

The two paths disagreed about the same room. A PRESENT room is checked on
all three qualities regardless of declaration: get_space_params fills width
and proportion from defaults, deriving width from size when absent, so
programme-house's t2 declares size: alone and still gets a real width
target of 1.633 it can fail on. Missing, it emitted one placeholder where
b1 emitted three. The cascade stands in for the checks that could not run,
and it stood in for the wrong number of them.

Fix: emit all three placeholders always -- a fixed 5 per missing instance,
mirroring the present-room path. 36 of 67 corpus codes were under-counted.
Max weight ratio between two single rooms 4x -> 1x (programme-house),
2x -> 1x (harbor, maple).

This makes fail counts LARGER and that is the point; it is a correctness
fix, not an improvement. harbor evolved-3M-nols-3 82 -> 84, generated
155 -> 174, evolved-3M 131 -> 144; maple generated unchanged (no missing
instances).

NOT taken: 1i8's other option, one fail per instance with the placeholders
informational. It fixes the verbosity dependence too but silently rescales
a missing room from 1/32 to 1/2, the same weight as one crinkliness fail.
Whether it SHOULD cost 1/32 is a real and separate question; bundling it
here would change the objective's priorities under cover of a bug fix.
Magnitude left exactly where it was, filed as homemaker-py-3i3.

Every historical corpus fail count is invalidated again, on top of 39.4 and
38.10/38.11 -- which is why the cold-start re-baseline belongs after the
objective work, not before it.

Closes homemaker-py-1i8.

Lint at parity (46); tests 379 passed (3 new), 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-29 09:33:40 +00:00
Claude
f5286dde3d
Make both failing tests assert their intent, not stale artefacts
The suite is green for the first time this session: 376 passed, 0 failed.

test_collapse_insearch_reproduces_94g_finish_time_result hard-coded both
endpoints of the 17 result -- 15 fails before collapse, 12 after. Those
were measured before 39.4, when harbor's effective programme was silently
32 instances because codes like cr1 were read as generic circulation; the
same layout now scores 82. But the guarantee the test exists to protect,
per its own docstring, is that in-search collapse reaches the SAME layout
as finish-time collapse on fixed geometry -- and two independent constants
never checked that. They can both drift and stay equal, or both hold and
mask an inequality.

Rewritten to compute both sides live and assert they agree, plus that
collapse does not make the layout worse. Measured: 82 -> 58 in-search, and
finish-time collapse independently reaches 58 at iters=3 and iters=6. The
invariant holds; only the constants were stale. Restating the reference
figure itself remains homemaker-py-ut5.

test_classify_fail_tier_covers_full_corpus globbed examples/**/*.fails and
asserted checked > 0. Git tracks ZERO .fails -- they are artefacts the
scorer writes beside a .dom -- so its docstring described files that by
design never exist in the repo, and it passed only on a machine that had
already run the scorer. Split into: a test that GENERATES fails by scoring
three corpus layouts picked for breadth (requiring >= 8 distinct kinds so
it cannot silently narrow); a test that an unclassifiable string actually
raises; and an opportunistic .fails sweep that never requires them.
Verified by moving every .fails out of the tree and re-running.

Closes homemaker-py-1ue.

Lint at parity (46).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-29 07:11:25 +00:00
Claude
e5eb397b52
Checkpoint long searches; the cold-start runs were lost to a reclaimed box
All four 500k runs died about 10 minutes in when the container was
reclaimed. No SIGTERM fired, so no .dom was written and 0 of 12 runs
completed. My plan committed results per finished run, which protected
nothing because no run reached its commit point. The bad assumption was
reading "reclaimed after inactivity" as CPU inactivity; it is conversation
inactivity, and background compute does not hold the box open.

Progress reached before the loss (from the tracked logs): harbor 24,960
evals / 40 fails, maple 14,880 / 79, health-centre 25,920 / 33,
programme-house 138,800 / 2.

The underlying gap is not environmental: a search's only output lands at
the very end or on SIGTERM, so ANY abrupt loss -- reclaimed container, OOM,
power cut -- takes the whole run with it. On a 3M-eval search that is 2.4
days of compute with no recoverable artefact.

  - driver.search gains checkpoint=/checkpoint_every=: the current best is
    handed to a callback at most every N evals. Rate-limited by evals, not
    improvements, which come in bursts early. A failing checkpoint is logged
    and swallowed -- losing a checkpoint is bad, losing the search because a
    checkpoint failed is worse.
  - homemaker-evolve --checkpoint-every N writes <out>.dom.checkpoint via
    mkstemp + os.replace, so a crash can never catch it half-written. It is
    deliberately NOT the output path: a checkpoint is a leaf-sharing run's
    internal best, dishonest under the canonical scorer until the finish
    stage unfolds it (homemaker-py-3l6), and must not be mistaken for the
    finished article.
  - Verified the written checkpoint re-loads as a valid .dom.

Default off, so behaviour is unchanged without the flag.

Lint at parity (46); tests 372 passed (3 new), same 2 pre-existing failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-29 05:43:58 +00:00
Claude
055d7101cd
ssz: crinkliness is declared per space; there is no daylight attribute
Owner's ruling, and it corrects the design not just the classification: the
daylight requirement is already defined in the crinkliness. The gaussian's
compact side IS "too little exposed wall per unit floor"; its exposed side
is envelope cost. 38.9's proposed daylight: axis was redundant, and keying
it off usage: was worse than redundant.

What was actually missing: crinkliness is the only leaf quality factor with
no per-space target. size, width and proportion are all declared by the
space; crinkliness was one global number for every room in every building.

  crinkliness: none        -> no minimum-exposure requirement, may be buried
  crinkliness: [t, s]      -> this space's own target
  key absent               -> the global uncrinkliness target, as today

`none` clips the factor on the compact side, it does not switch it off:
over-exposure is still penalised, because a crinkly leaf costs envelope
whatever it holds. A store may be buried; a store may not be a starfish.

The mechanism is backward compatible -- an absent key resolves to the
global target, so shipping it changes no score. Behaviour changes only
where a config declares something, which keeps the objective change
visible per programme in config rather than hidden in a default.

Owner's classification: everything a person occupies wants a window, WCs
and reception/waiting/foyer included; only stores, plant, records and
laundry do not. migrate_crinkliness_key.py declared crinkliness: none on 18
corpus spaces. Crinkliness fails 271 -> 243, of which not-defects 136 (50%)
-> 108 (44%); the 28 that went are exactly the utility fails.

usage_daylight and needs_daylight are removed as mis-keyed, and
DAYLIGHT_USAGES with them -- a vocabulary value should exist only where the
engine treats it differently. The historical crinkliness_mode modes stay,
default off, so 38.6/38.8 remain reproducible.

uncrinkliness_circulation is now settable to none like any space, but its
default is left unchanged pending a ruling: corridors were not among the
groups ruled on and are 63% of the remaining phantom fails.

Lint at parity (46); tests 364 passed, same 7 pre-existing fixture failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-28 22:39:09 +00:00
Claude
189efdbfc7
ssz: daylight is required of rooms that do not need it
DESIGN.md 38.6 concluded the three crinkliness modes were inert against the
circulation-deletion incentive. Two things were wrong with that measurement.
Its premise, 38.2, is retracted. And its script selected leaves with the
pre-39.4 prefix rule `type[:1].upper() in ("C","O")`, which sweeps every
programme room starting with c or o -- cr1, of1 -- in as circulation.

The simpler problem is that none of the three modes ever touched the leaves
ssz is about. quality_uncrinkliness reaches `if not crink` before any mode
logic that matters, so for a zero-exposure leaf: floor returns 0.01 (one
percent of a unit quality, multiplied into a product and weighed against a
whole leaf's cost -- inert); compact_ok is self-contradictory, announcing
that compact is not a defect and then returning the floor for the most
compact case of all; exempt_circulation reaches at most a third of them.
Measured: 0% / 0% / 0% / 21-33% of buried leaves rescued.

What the buried leaves are, now that 39.7 gives every space a usage: two
thirds of them are spaces that architecturally do not want a window --
stores, WCs, plant, corridors, covered courtyards -- scored identically
with a windowless bedroom. harbor 22/33, maple 33/46, health 9/18.

  - crinkliness_mode="usage_daylight": daylight required of the uses a
    person occupies (programme.DAYLIGHT_USAGES) and nothing else. Elsewhere
    the factor is clipped on the compact side only, so being buried stops
    being a defect while over-exposure still costs -- a crinkly leaf costs
    envelope whatever it is used for. A windowless bedroom stays the hard
    zero it is under stock: 11/11, 13/13, 9/9 still failing.
  - compact_ok repaired to score the buried limit as compact, the behaviour
    its name always claimed. It now rescues 100% including bedrooms, and is
    kept as the upper-bound control, not a candidate.
  - ab_ssz_search.py: the fixed-budget search A/B ssz's acceptance criteria
    actually asks for. Every arm is optimised under its own objective and
    re-scored under stock urb, because the permissive modes return 1.0
    where stock fails and would otherwise win by deleting a fail category.
  - ab_crinkliness_mode_ssz.py: prefix rule fixed, retracted premise
    flagged in its docstring.
  - 38.7's remaining claims from the retracted 38.2/38.3 corrected.

Default is unchanged ("urb"), byte-identical to all prior runs. Lint at
parity (46 pre-existing); tests 366 passed, 10 new, same 7 pre-existing
fixture failures (homemaker-py-bdf).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 16:45:10 +00:00
Claude
024311a2c2
§39.10: preserving constructed connectivity is NULL — and it reframes §39.9
§39.9 named the upstream fix: keep circulation connected DURING the resize
rather than rebuilding it after. Built and measured. It does not help, and the
reason matters more than the lever.

Both halves of the re-cut do damage, in different proportions per programme.
Freezing rotations and letting only ratios move (% levels connected, 12 seeds):
harbor 100 -> 71 -> 50, health-centre 100 -> 8 -> 8, maple 100 -> 92 -> 67. So
health-centre is destroyed entirely by the ratio and maple mostly by the
rotation; a fix must be able to give back either.

operators._size_divisions_preserving_circulation snapshots every cut, resizes,
then reverts the cuts on the tree path between each circulation pair the resize
broke -- programme fully intact, no retyping, only geometry given back. It works
on connectivity (harbor 50->92%, maple 67->97%, health-centre 8->17%) and costs
area accuracy: constructed-seed fails harbor 96.6->141.5, maple 141.8->175.8,
size fails roughly double. (A greedy single-cut revert barely moved -- it stalls
where no ONE revert helps though two would. Targeting the broken pairs is what
made connectivity work.)

The obvious defence -- raw constructed seeds understate it, the resize is only a
warm start, the inner loop should recover -- was TESTED AND FAILS. Full search,
harbor-house, 12000 evals, seed 1:
  OFF  43 fails,  9 hard, 3 connectivity
  ON   65 fails, 26 hard, 4 connectivity
Worse on every axis, including connectivity itself.

REFRAMING: §39.9's fact stands (the resize destroys 41 of 49 circulation edges)
but is NOT ACTIONABLE, because construction-time connectivity does not determine
final connectivity. The search discards and rebuilds the seeder's circulation
either way, and constraining the seed only spends area quality the search cannot
recover. Together with §39.8 (not an incentive problem) that retires the framing
this thread inherited from §38: connectivity is neither a construction problem
nor an incentive one.

Both flags (repair_circulation, preserve_circulation) stay default off with the
numbers recorded, plus byte-identical-default tests. Do not revisit either
without a new formulation -- the standing this document gives bubble.py.

356 passed (+1 new), 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
2026-08-26 15:11:46 +00:00
Claude
7c41069226
§39.9: level-not-connected is destroyed by the resize, not by the search
Answers homemaker-py-yql. §39.8 established the search is not PAID to sever
circulation; this establishes where connectivity actually goes.

CONSTRUCTED, THEN LOST -- at construction time, in the resize.
_assign_adjacency_aware picks circulation as a CONNECTED dominating set and
succeeds every time. _size_divisions_from_targets then moves every wall to hit
the programme's area targets and destroys it.

Measured over 20 constructed seeds per programme, fully-connected seeds:
harbor-house 1/20, health-centre 1/20, maple-court 0/20. The control -- same
seeds with proportion_aware=False, i.e. no resize -- is 100% connected on all
three. Mechanism confirmed on health-centre: 41 of 49 circulation-to-circulation
edges destroyed by the resize, surviving shared walls squeezed to 0.54-1.11 m
against door_width=1.2, so they stop counting as edges. This is the failure mode
§37.7 recorded for CP-SAT assignment, never looked for in connectivity, where it
costs 35-95 points.

§39.7 COST CHECK: zero. Identical rates under prefix-inferred vs declared
usages -- has_circulation never trims C-C edges, so last commit's usage change
could not and did not make connectivity harder to achieve.

REPAIR MEASURED NEGATIVE. operators.repair_circulation_settled applies §37.7's
own alternating-minimisation fix (re-connect against the settled geometry by
retyping the cheapest bridging leaves to C). It restores 100% connectivity on
all three programmes -- and is still the wrong trade: connectivity fails fall
0.8-1.7 per seed while missing-room fails rise 5.0-8.5, because every retyped
leaf displaces a required room at a 3-5 fail cascade (§38.5). Kept default off
with the write-up, per house style for a null lever, plus a byte-identical
default test and a test asserting it does reconnect every storey.

NEXT LEVER, FILED: preserve the connection during the resize (constrain
_size_divisions_from_targets so a shared C-C boundary cannot fall below
door_width) rather than rebuild it afterwards at the programme's expense --
a constraint on an existing solve, not a new repair pass. solver.py's existing
min_width_generic is the same idea applied to leaf width rather than to a shared
boundary, so it may belong beside it.

Adds experiments/diag_connectivity_yql.py (construct / cost / survive reports).
355 passed (+2 new), 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
2026-08-26 14:39:07 +00:00
Claude
3aee813ccd
§39.8: homemaker-py-2v1 connectivity weighting — MEASURED NULL, premise retracted
§38.2 concluded the objective is net-positive on severing a level's
circulation: merging a corridor into a habitable sibling gains x6
(value_inside/value_circulation), while "level N not connected" costs x0.5, so
break-even needs 0.5^w < 50/300, w > 2.58 -- "severing must cost at least 3
fails and costs 1". The arithmetic is right. The premise is wrong.

Shipped anyway, EXPERIMENTAL and default off (byte-identical):
fitness.connectivity_weight_for(value_inside, value_circulation) returns the
smallest weight making severing net-negative -- 3.0 at the defaults, DERIVED
from the rates rather than hard-coded so it tracks them if either is retuned.
conf["connectivity_weight"] takes 1.0 / "auto" / a number and counts each
connectivity failure as w failures in the 0.5^n penalty.

MEASUREMENT: at auto (=3) the §38.2 deletion test does not move at all -- 5/25
rewarded either way, median x0.26 vs x0.27. Reason: the connectivity fail count
is UNCHANGED in every rewarded deletion (115->107 fails but 5->5 connectivity;
107->99 but 3->3; 78->71 but 3->3). Weighting a fail that never fires changes
nothing.

And when a deletion DOES break connectivity, it is already punished. Every such
case, 4 seeds per programme: harbor-house 2 of 32 sampled deletions, both
punished (x0.00, x0.01); maple-court 5 of 32, all punished (x0.58 .. x0.07).
Severing costs 1-2 connectivity fails PLUS the cascade after them, which
already outweighs the x6 gain. The flat rule was never the problem.

Where §38.2 went wrong: the x4.06 "well-daylit circulation leaf" that motivated
the bead was a deletion that did NOT change the connectivity fail count. It was
rewarded for removing the leaf's own quality failures -- §38.1's zero-value
finding -- and I misread it as a pricing mechanism. §38.2 now carries the
retraction inline. Two lessons recorded: a plausible closed-form arithmetic is
not a measurement, and when a fix produces exactly no effect, suspect the
premise before the implementation.

Still standing from §38: §38.1 (buried leaves score zero quality and contribute
no value) and §38.3 (frontage budget) are direct measurements. §39.7 remains
the better lever on the same symptom -- it made the connectivity fails FIRE,
where this would only have made them cost more.

Re-opened as homemaker-py-yql: why level-not-connected persists in the best
layout when severing is already punished. Evidence now points at reachability,
not incentive, and it is newly measurable because §39.7 stopped store cupboards
standing in for corridors.

353 passed (+3 new), 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
2026-08-26 14:15:22 +00:00
Claude
03aadec1ba
§39.7: access requirements become a declared usage: attribute (homemaker-py-sel)
Closes the second namespace sharing a first character with programme codes: the
usage prefixes b/t/l/k, under which a room silently inherited another room's
connectivity rules from its spelling.

usage is a plain, MANDATORY attribute of the space definition -- not a lookup
table. An interim design proposed a top-level usage_classes: table binding
author-coined names to behaviour; withdrawn, because an indirect name->behaviour
mapping living apart from the thing it describes is exactly the shape of the
prefix rule §39 exists to remove, it would be the only such table in a schema
where every other space property is a plain attribute, and the need it served
was already met -- "building specific" is about what a room is CALLED, and
name: is already free text.

Rule that settles it: a usage value exists iff the engine treats it differently
somewhere. Config selects among behaviours; it cannot invent them.

- programme.USAGES (living/kitchen/bedroom/toilet/utility/none) plus the
  behaviour groupings PRIVATE_USAGES / PRIVATE_STRIPS / TOILET_STRIPS /
  SOCIABLE_USAGES. Missing or unknown usage is a load error naming the code,
  from BOTH parse paths.
- Code-level, never leaf-level: usage_of(leaf.type) is looked up fresh, so a
  retype changes the class automatically. 51 sites assign leaf.type, and
  share/share_type plus the r5a resurrection are the precedent for why
  leaf-level attributes rot.
- graph.has_circulation takes the usage map and trims on declared class;
  fitness.access and the public-access check likewise. fitness._t0 is DELETED --
  no first-character type test remains anywhere in the codebase.
- utility is distinct from bedroom (same access requirements today) because it
  is a different use and gives derive_interchange_classes an axis to relax on.
- A toilet now keeps its edge to a terminal room -- the Brand adjacency, which
  the old b-before-t loop ordering severed.
- All 107 corpus entries migrated by experiments/migrate_usage_key.py, comments
  and layout preserved.

MEASURED -- the connectivity model was ~4x too permissive. `none` is not
neutral: nothing is trimmed, so the graph may route THROUGH the room, and 34 of
52 codes had no class (Dental Surgery, Records Room, Utilities Closet all served
as corridors). Edges trimmed, prefix-inferred vs declared, 3 seeds each:
  harbor-house   18 (9%)  -> 79 (39%)   inaccessible fails 0 -> 4
  health-centre  12 (8%)  -> 59 (40%)   inaccessible fails 2 -> 3
  maple-court    53 (17%) -> 123 (39%)  inaccessible fails 1 -> 5

Re-baseline (seed 1, 20k, harbor): 58 fails (15h/43s) -> 61 (16h/45s), now
reporting 1-inaccessible-usable-space x2 plus level 0 and level 1 not connected.
The count rose because the objective got honest -- those failures were always
true of the layout and the old model could not see them. Every harbor number
before this was measured against a graph crediting routes through store
cupboards.

Sharpens §38.2: the objective pays x60-85 to delete circulation, and until now
the deleted corridors were not missed because storage stood in for them. With
that substitution gone, homemaker-py-2v1 is the remaining half -- and now
measurable, because the fails it should prevent actually fire.

350 passed (+5 new), 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
2026-08-26 13:39:41 +00:00
Claude
a25dc2cb59
§39.4 completion + §39.5 retraction + §39.6: the usage namespace is NOT clean
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
2026-08-26 10:09:14 +00:00
Claude
7ec4e5d121
§39.4: tighten generic-type matching, reverting the harbor rename
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
2026-08-26 09:45:28 +00:00
Claude
697914fac8
ju3: reject programme codes colliding with the c/o/s generic type prefixes
Closes homemaker-py-ju3. DESIGN.md §39.3.

The class: key from the bead's design was deliberately NOT built. Auditing
every use of the prefix rule first showed it runs deeper than c/o/s -- l/k/b/t
carry real adjacency semantics (graph.py builds bedroom<->toilet and
kitchen<->living relations from first characters) -- so re-plumbing the type
system would invalidate the whole corpus and every baseline, for a problem
whose damage is the silence, not the convention. Two findings made the smaller
fix sufficient: no corpus programme has ever declared a bare c/o/s code, so
check_space_counts' skip only ever discarded declared rooms; and nothing
references harbor's four codes in any adjacency or co_locate list.

- programme.validate_codes raises on a reserved-prefix code, with the full
  explanation. Called from BOTH parse paths (programme._parse_spaces and
  fitness.Fitness._load_programme parse conf["spaces"] independently, so
  validating one would leave the other door open). l/k/b/t stay unreserved.
- harbor-house and harbor-house-l0 renamed: cr1->fr1, of->ao, st1->gs1,
  st2->gs2. New prefixes are unused in harbor and semantically neutral, and
  the two storage codes still share a prefix, preserving the structure
  evaluate_building's per-code plot-ratio term depends on. name: unchanged.
- experiments/migrate_ju3_rename.py migrates .dom files written before the
  rename (--check dry-runs). Pre-rename artefacts, notably evolved-3M*.dom,
  must be migrated or their leaves read as unmatched generics.
- test_collapse_global's c/o/s exclusion test now uses a generic C leaf, which
  is what the exclusion is actually for; it previously relied on a programme
  code colliding, which is no longer possible.

Re-baseline (seed 1, 20k evals, same settings as §38's run): 57 fails against
the 32-instance effective programme -> 55 against the real 37-instance one,
with all five previously-lost room instances now placed inside their declared
sigma bands (fr1 87.2 vs declared 80, was 32.9/17.1; ao/gs1/gs2 were absent
entirely) and no failure naming any of the four codes. At one seed each,
57 vs 55 is within noise -- the robust result is the room placement, not the
count. Historical harbor numbers are not comparable to post-ju3 ones; filed
homemaker-py-t3s to restate 2v1's acceptance figure once evolved-3M is
migrated.

346 passed (+10 new), same 7 pre-existing fixture failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 09:06:08 +00:00
Claude
50b5cfd476
§38.2 refinement: connectivity is under-priced ~3x, not just a crinkliness bug
Follow-up measurement corrects the first draft of §38 in two ways.

1. Harbor-house's floor is 15 fails (evolved-3M-nols-3, 1.7M evals), not the
   30-40 I quoted from §13.11's 20k-budget runs. Frontage deficit predicts the
   COST of solving, not impossibility: ~150x budget gap between a
   frontage-short and a frontage-surplus programme. Table corrected.

2. Zero-exposure is only half the mechanism, and not the dominant half.
   Splitting the deletion test by lit vs buried shows a WELL-DAYLIT corridor
   (q_crink=0.736) is still worth x4.06 to delete. Cause: value_circulation=50
   vs value_inside=300, so merging corridor into room is a flat x6 gain, while
   'level N not connected' costs only x0.5. Break-even needs 0.5^k < 50/300,
   i.e. k > 2.58 -- severing must cost at least 3 fails and costs 1. Net x3.0
   predicted, x4.06 measured. The objective is net-positive on severing the
   spine even when the circulation is perfectly lit, which explains why both
   'level N not connected' fails survive in the best layout after 1.7M evals.

Adds fitness.quality_uncrinkliness crinkliness_mode (EXPERIMENTAL, default
"urb" = stock hard 0.0, byte-identical: 336 passed vs 331 before, same 7
pre-existing fixture failures). A/B harness ab_crinkliness_mode_ssz.py shows
none of the three modes removes the incentive, and the lit column is 3/8 under
every mode including stock -- clean isolation of the two mechanisms.

Filed homemaker-py-2v1 (P0) for the pricing fix; ssz/hxi now depend on it.
Acceptance test recorded up front: harbor must reach 15 fails in materially
fewer than 1.7M evals AND without either not-connected fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 07:40:37 +00:00
35adcd4b4d homemaker-py-sd3: fix vacuous 94g keep-better guard (collapse_insearch leak)
driver.collapse_best built its evaluator with _fitness_for's default
collapse_insearch=True, so collapse_finish's base_fails/cand_fails were
both measured through score_with_fails' own auto-collapse pass -- base
silently equalled collapsed on 5/5 probed files, making the "keep only
if fails don't increase" safety guard vacuous and understating 94g's
real effect in logs. fitness.collapse_finish now forces canonical
(collapse_insearch=False) scoring for its own measurement regardless of
self's config; collapse_best now builds its evaluator canonically too
(matching what homemaker-fitness reports for the written .dom) and
threads max_share/conn_grade through. Same-family fix in
search_annealed's no-polish-budget rescore branch, which silently
defaulted to collapse_insearch=True via _evaluate's default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uCyZJCh5mBuA2yEFxgQpo
2026-08-05 07:52:44 +01:00
cf634ae949 homemaker-py-2g7.5: CP-SAT exact room-code assignment (seeder + reassign op)
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
2026-08-04 09:19:36 +01:00
3aa08ee6ff homemaker-py-koo: multi-storey (below-link) support for the shape-curve DP
Generalise shapecurve.py's DP to process dom.levels(root) bottom-up per
storey instead of assuming a single free tree. A divided node's split is
free only per solver.free_branches' own criterion (below is None or
undivided there) -- geometry.coordinate always mirrors a below-linked
node's corners from the storey below regardless of whether that storey's
counterpart is divided, so every free region at any storey reduces to the
exact same single-region problem the pre-existing _check/realise already
solved. New _region_roots finds below-fixed leaves (checked directly,
gridless) and below-fixed-box/free-split fringe nodes per storey;
_solve_all_levels realises each storey before checking the one above and
snapshots+restores on any infeasibility, preserving solve()'s all-or-nothing
and is_feasible()'s never-writes contracts across the whole tree.
eligible() now allows any storey count.

Validated on the real (non-de-risked) examples/harbor-house: 200 random
2-storey topologies, DP-vs-NM agreement 99.5%, 0 false negatives, 117.7x
speedup (DESIGN.md §37.6). Full suite 397 passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-03 23:30:18 +01:00
467c732260 homemaker-py-wkh: DP-exact hard pre-filter for driver.py's shape-feasibility prune
Adds shapecurve.is_feasible() (a non-mutating refactor of solve()'s check
phase) and a shapecurve_prune flag composing the DP's exact feasible/
infeasible verdict with operators.predicted_shape_fails' existing heuristic
prune: DP-feasible vetoes a heuristic prune outright; DP-infeasible only
hard-prunes when the incumbent already has zero total fails (exact, since
infeasible proves the shape-fail floor is >=1); otherwise defers unchanged
to today's heuristic threshold. Conservative by design since a wrong prune
is unrecoverable.

Validated 0/400 false negatives across two structurally distinct plots
(harbor-house-l0 + a newly-added programme-house sweep, the first genuinely
non-rectangular plot this DP has been checked against). The real
driver.search A/B on harbor-house-l0 measured NULL (byte-identical off/on)
for a root-caused, pre-existing reason: predicted_shape_fails rarely
triggers organically at this scale, so neither new branch had an opening to
fire -- not a defect in this change. Full writeup: DESIGN.md §37.5.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-03 21:10:28 +01:00
229c3c6933 homemaker-py-6xh: wire shape-curve DP into driver.py as an NM warm-start
Promotes the validated shape-curve DP (experiments/shapecurve_spike.py,
2g7.4, DESIGN.md §37.2) from a reference-only spike into
src/homemaker_layout/shapecurve.py, and wires it into driver._evaluate as a
warm-start for innerloop.optimise: when eligible (single storey, no
leaf_sharing/superpose/max_share/multi_use) and no caller-supplied x0, the
DP's exact shape-feasible ratio point is written onto the tree before NM
runs, off by default (shapecurve_warmstart=/--shapecurve-warmstart).

Caught and fixed a latent bug promoting the spike: realise() could leave
numpy.float64 in `division`, which yaml.safe_dump can't serialise — the
original spike never round-tripped through dom.dumps so this was never hit.

A/B on harbor-house-l0 (experiments/ab_shapecurve_warmstart.py, budget=2000,
5 seeds): mean total fails 16.6 (on) vs 19.6 (off), ~3.5x mean fitness
improvement; mean hard-fail count alone was a noise-level wash at this
sample size. Full writeup in DESIGN.md §37.4.

Deliberately deferred to new tracked beads (children of 2g7): DP-exact hard
pre-filter (wkh), multi-storey below-link support (koo), leaf_sharing/
co_type modelling (tym), true skew-quad polygon algebra (ekc) — 6xh stays
in_progress pending those.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-03 18:43:28 +01:00
9296f75cdb homemaker-py-2g7.1: plan->dom composer (SVG trace -> slicing-tree .dom)
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
2026-08-03 11:03:02 +01:00
8efdc02fd9 homemaker-py-2g7.3: hard/soft fail tiering behind --use-tiers flag
Splits the flat outer-search comparator (-n_fails, fitness) into a tiered
(-n_hard, -n_soft, fitness) so search budget stops being spent polishing
SOFT shape fails (crinkliness/proportion/size/width/edge-too-long/
staircase-volume) while HARD structural fails (missing space, wrong/
required level, level/circulation/vertical connectivity, adjacency,
stairs, covered-outside, storey limits, public access) remain unfixed.

fitness.classify_fail_tier/tier_counts classify every fail string emitted
across fitness.py and graph.py, raising on anything unrecognised so new
fail sites must declare a tier. Validated against all real fail strings in
the checked-in corpus plus every fail-emission call site read from source.

driver.Individual gains n_hard/n_soft (populated from innerloop.Result.
fail_lines); search(use_tiers=...) swaps the comparator when set (default
off, so existing runs are unaffected — inner-loop 0.5^n cliff untouched).
evolve.py exposes --use-tiers / HOMEMAKER_USE_TIERS.

experiments/tier_ab_2g7_3.py runs the acceptance A/B (harbor+maple, 3
seeds, 20k evals) in the background; results pending.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-02 16:00:39 +01:00
12247248d5 homemaker-py-r5a: canonicalise stale leaf-share stamps before any relabel
collapse_global's own commit could relabel a leaf back to the code its
stale share_type names, making share_type == type true again and
resurrecting a multiplicity credit for area never sized for it -- the
commit-door companion to the iio valuation bug. dom.canonicalize_shares()
drops share/share_type whenever share_type != type; called at the top of
collapse_global (covers collapse_global's own commit, 2-opt, and standalone
finish-time use) and _evaluate_full (covers collapse_superposition and
ordinary retype mutations) so the guard is an actual invariant instead of
a per-reader check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dq3WAXft8RszMG2CLH7VkU
2026-08-02 10:45:12 +01:00
929be5b144 homemaker-py-iio: fix stale leaf-share leak in collapse_global's probe valuation
_collapse_value and _usage_quality temporarily overwrite leaf.type to probe a
hypothetical candidate code, but graph.leaf_share reads that overwritten type
against leaf.share_type -- so a stale share (left over from a code the leaf
was since retyped away from) spuriously reactivates whenever the probed
candidate happens to equal the old share_type, skewing the Hungarian
assignment's cell value for that (leaf, code) pair. dom.dump/dom.load drops
such stale metadata on reload (dom._emit only serialises share when
share_type==type), so a live search tree carrying it and its dump/reload
round trip fed different values into the same collapse_global call and
landed on different optimal matchings.

Fix: neutralise share_type during the probe whenever the candidate differs
from the leaf's real current type, restoring it in the finally block. The
leaf's own current type still legitimately carries a live share.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8agJBT2ZpmF3ErW7wi2wY
2026-08-01 21:08:32 +01:00
e37930605e homemaker-py-1s3: land precision-weighted shape combination for multi-use leaves
Follow-up to the previous commit: user flagged that quality_width/
quality_proportion's "stricter of both" (max target, min sigma) combination
for a fused leaf's two codes was an ad hoc hack. Tried two more principled
alternatives and A/B'd all three against the harbor-house/health-centre
example programmes (20k evals x 3 seeds each):

1. stricter-of-both (original)      -> health-centre +24.5% worse
2. precision-weighted Gaussian product -> health-centre -13.9% better
3. mixture (max of two Gaussians)    -> health-centre +20.4% worse

Landed #2 (fitness._gaussian_product): combining two Gaussian evidence
sources about the same quantity via precision-weighting gives an
intermediate target with a narrower spread, unlike the naive max/min hack.
#3's building block (_clipped_gaussian) is kept, documented, and unit-tested
as a recorded negative alternative -- somewhat counterintuitively, the more
philosophically appealing "let the leaf collapse toward whichever code fits"
mixture model was empirically worse, because max() lets a leaf score 1.0 by
satisfying only the weaker of the two codes' targets.

multi_use stays default OFF -- the precision-weighted result improves both
example programmes on average but isn't the clean sweep needed for a
default flip (harbor-house loses 1/3 seeds). DESIGN.md §33 rewritten with
the full three-way comparison.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8agJBT2ZpmF3ErW7wi2wY
2026-07-31 10:20:49 +01:00
27719975b0 homemaker-py-1s3: multi-use leaves as permanent design goal (§26 path b)
Builds path (b) from §26 -- a leaf permanently serving two DIFFERENT
compatible programme codes at once, extending leaf-sharing's same-code
multiplicity mechanism to different-but-compatible codes. Architect-declared
`co_locate` pairs (validated against interchangeable()'s S1-S4 bounds, no
transitive closure so the b3v chain problem can't recur), threaded through
graph.py's checks via a new leaf_codes() resolver and fitness.py's quality
terms (additive size, stricter-of-both width/proportion). Construction-time
only, gated behind `multi_use` (default OFF, bit-identical when off).

End-to-end A/B (20k evals x 3 seeds x 2 programmes) came back net negative:
harbor-house -4.0% but health-centre +24.5% worse (3/3 seeds), because
fusing different codes' shape targets via stricter-of-both can impose a
tighter joint constraint than either code needed alone, which the tightly-
packed health-centre programme can't absorb. Written up as DESIGN.md §33;
multi_use stays default OFF, no default-flip recommended.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8agJBT2ZpmF3ErW7wi2wY
2026-07-31 00:16:12 +01:00
6d719e03ab c94: beam/best-first search over adjacency-aware room placement (null)
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
2026-07-28 00:00:51 +01:00
67f0c38f45 9wi: 2-opt local search past collapse_global's Jacobi adjacency plateau
The Jacobi adjacency relaxation in collapse_global (94g) re-solves a linear
assignment each round holding neighbours' labels fixed from the previous
round, which can 2-cycle between labellings that satisfy zero adjacency
requirements even when a fully-satisfying permutation exists (proved by
test_two_opt_polish_escapes_jacobi_plateau on a minimal 4-cell chain).

Fitness._two_opt_adjacency_polish runs after the Jacobi fixpoint and tries
swapping the labels of every same-level pair of supply leaves, keeping a
swap only on strict improvement -- monotone by construction. Gated behind
collapse_global(local_search=...) / homemaker-collapse --local-search,
default off pending a broader sweep (homemaker-py-cdl). Swept the 11
harbor-house evolved-*/3m/materialised .dom files: 0 regressions, 1 real
improvement (evolved-anneal-3M.dom 21->19 fails).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 20:59:07 +01:00
a103e0114a 8sh: insert/relocate-circulation repair operator (qi6 mechanism (a))
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
2026-07-24 19:48:13 +01:00
a78409df90 1ph: larger-N seed sweep confirms collapse_insearch positive, flip default ON
20-seed programme-house sweep (vs the original 5) resolves the qpk A/B's
mixed 3/5 result as small-sample noise around a true small positive: mean
fails 7.95->7.10 (~10.7%), 11W/6L/3T, paired t-test p~0.028. Flips
collapse_insearch's default from OFF to ON in evolve.py and driver.py
(_overrides_for/_fitness_for/_evaluate/search/polish_finish); opt out with
--no-collapse-insearch. fitness.Fitness itself is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDZjAATDWW1xFfc7xnJqSt
2026-07-24 09:55:34 +01:00
f22691e93d 161: thread fit into GA for in-search shape_rotate/deslim, negative result
driver.search()/search_staged() gain enable_shape_repair (default off),
mirroring the enable_reassociate clean-toggle pattern: only builds a
fitness.Fitness instance and passes it to operators.mutate() when
enabled, so shape_rotate/deslim (7fm) can actually be selected mid-GA
instead of always no-opping on fit=None.

Full A/B sweep (harbor-house, budget=1M, 4 seeds) shows no improvement:
mean fails 14.50 (off) vs 14.75 (on), within seed noise. Confirms 7fm's
finish-time finding at in-search scale — these operators don't rescue
harbor-house's residual fails even with GA selection pressure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDZjAATDWW1xFfc7xnJqSt
2026-07-22 17:43:36 +01:00
2b7a7d2926 qpk: in-search global collapse — run collapse_global per-eval during search
Runs the 94g finish-time cell↔room collapse inside every fitness eval
(collapse_insearch conf flag, default off, bit-identical when off) instead
of once at the end, so search optimises the collapsed objective directly.
Plumbed through fitness.py/driver.py/evolve.py the same way superpose/
conn_grade are; --collapse-insearch CLI flag.

A/B validated against the xi7 protocol (equal budget, both arms finished
with standard finish-time --collapse): POSITIVE, opposite of the 9o5/xi7
prior. harbor-house ON wins 3/3 (mean fails 80.3->72.0); programme-house
mixed 3/5 (mean fails 8.4->7.8). Kept default off pending a larger
programme-house sample; documented as a working opt-in for harbor-house-
scale-or-larger programmes. Full writeup in DESIGN.md §20.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 20:35:18 +01:00
07a4739576 7fm: targeted shape-repair operators (shape_rotate/deslim), negative finish-time result
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.
2026-07-19 11:06:56 +01:00
94d4223a55 qi6: graded circulation-connectivity signal (§18)
The dominant post-collapse fail is the binary "level N not connected",
which is flat across fragmentation (a 7-component storey scores the same
as a 2-component one), so the outer search has no gradient toward
connected circulation. A finish-time convert-to-circulation repair was
prototyped and measured NEGATIVE (195->560 fails: bridging needed rooms
costs more missing-room fails than the one binary fail it clears).

Instead add graph.circulation_connectivity(G) = largest-circ-component
fraction, summed over storeys onto the score_with_grade proximity channel
(conf flag conn_grade; replaces the §11.4 leaf-grade there). It is a
secondary comparator key only — scalar fitness and fail count stay
byte-identical — restoring the gradient the binary fail lacks. Threaded
through driver (_overrides_for/_fitness_for/_evaluate/search; enabling it
implies the grade key) and evolve --conn-grade (default off).

A/B on full-budget runs pending; short smoke run confirms plumbing.

Tests: tests/test_conn_grade.py x9 (fraction contract, non-circ ignored,
monotone under (dis)connection, score/fail invariance); 276 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8566xAxTnwtJTkpXjYNZm
2026-07-18 18:44:24 +01:00
880a214d96 94g: public-access pin + keep-better wrapper + CLI/finish-hook wiring
Public-access term (preserve_public_access, default on): when the building's
only street access is an l/k ROOM neighbour of a public outside leaf (no
circulation fallback — an existential building-level check the per-leaf
objective can't see), that leaf is pinned (kept, its demand slot decremented)
so the collapse can't drop "no outside public access". Best layout 15→13
becomes 15→12 with zero new fails; sweep total 172→171, still monotone.

collapse_finish(root, **kw) -> (tree, base, coll, applied): keep-better wrapper,
scores on throwaway copies (score_with_fails merges in place), returns the
collapse only if fails don't increase.

Wiring: driver.collapse_best updates result.best (lineage +collapse, canonical
re-score); evolve.py runs it after the sharing polish behind --collapse/
--no-collapse (default on). New homemaker-collapse CLI (collapse_cmd.py) applies
it to an existing .dom, writing <stem>.collapsed.dom.

tests/test_collapse_global.py: demand-set relabel, level hard constraint, c/o/s
exclusion, no-op safety, keep-better/unmerged. 267 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8566xAxTnwtJTkpXjYNZm
2026-07-18 10:29:44 +01:00
9cbdf880a4 b3v: interchange:false veto hatch for interchange-class derivation
9o5 §7.5 escape hatch: a per-space `interchange: false` opt-out in
patterns.config removes a code from auto-derived interchange classes,
letting the architect veto a harmful grouping (harbor-house's transitive
8-code chain) without disabling superposition globally.

SpaceReq gains an `interchange` bool (default True). Honoured as an S0
short-circuit in interchangeable() and by filtering derive_interchange_
classes() input. Superpose default stays OFF regardless (xi7 verdict), so
this only bites when superposition is enabled on a real config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8566xAxTnwtJTkpXjYNZm
2026-07-17 16:53:20 +01:00
3b3eef68a7 kpu: Schedule B in-run leaf-share grain annealing (search_annealed)
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
2026-07-16 08:38:08 +01:00
479d4a57d5 3l6: leaf-sharing runs auto-finish (unfold+polish) before write
Leaf-sharing evolve runs optimised a sharing-credited objective (a shared
leaf of code X with share=k counts as k programme rooms, size re-centred on
k*target) but wrote the un-materialised genome, which the canonical
homemaker-fitness rates catastrophically worse (harbor-house: internal
1.03e-05 vs canonical 6.73e-29, 15 critical missing-room fails).

Fix: before write, driver.polish_finish() unfolds every live shared leaf
into k distinct rooms (operators.unfold_shared_leaves — pays down the count
deficit) then warm-starts a leaf_sharing=False polish search from the
unfolded genome so the materialised rooms get proportion/width/size cleanup.
Returned best.fitness is the canonical score (leaf_sharing off => internal
== canonical). This is yaa's proven unfold-then-polish path, made automatic.

evolve.py: new --polish-budget (env HOMEMAKER_POLISH_BUDGET; -1=auto=
budget//2, 0=unfold+rescore only). Interrupt forces polish_budget=0 for a
fast honest output. Default stays --leaf-sharing on (its topology-search
speed retained; output made honest by the finish). Schedule B in-run
annealing remains homemaker-py-kpu.

Verified (harbor-house 3000+1500): reported polish fitness 4.79788e-27
matches canonical homemaker-fitness exactly, 0 critical fails. Tests: 254
pass (+3 polish_finish).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8566xAxTnwtJTkpXjYNZm
2026-07-15 10:21:58 +01:00
aaaaad2e53 yaa: unfold shared leaves at sharing->no-sharing transition
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
2026-07-12 16:34:33 +01:00
c3635634e8 9o5: type superposition + per-eval collapse (multi-use leaves)
Interchangeable codes (similar size/width/proportion, compatible level/stack,
no adjacency edge) form equivalence classes derived from the programme. With
--superpose (default off), each fitness eval COLLAPSES every superposed leaf to
its best in-class usage via an optimal supply->demand assignment (brute force
<=C! within cap C=4, scipy Hungarian beyond), then scores the condensed types.
Because collapse re-types on the unmerged tree before all checks, counts /
adjacency / quality are unchanged downstream -- no Node field, no graph/operator
changes -- and default OFF is bit-identical.

- programme.py: derive_interchange_classes + interchangeable (S1-S4, locked
  thresholds R_SIZE=1.5/R_WIDTH=1.3/R_PROP=1.5, CLASS_CAP=4)
- fitness.py: collapse_superposition, _best_assignment, _usage_quality;
  superpose/superpose_class_cap conf knobs; collapse hooked into _evaluate_full
- driver.py/evolve.py: superpose flag plumbed beside leaf_sharing; --superpose
- tests/test_superposition.py: 17 tests (derivation, assignment, end-to-end)

Closes homemaker-py-9o5 (build); validation A/B is homemaker-py-xi7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 07:08:46 +01:00
bb9b355f14 x3b/§13.10: productionise leaf-sharing — per-code share grain + CLI wiring
Make the §13.3 lever a first-class feature, not experiment-only.

- programme.py: SpaceReq.share (default 1) + has_share, parsed from
  patterns.config 'share: N'.
- operators._share_grain: resolve per-code grain from leaf_share_factor
  selector — 0 = per-code opt-in (share iff share:N>=2), >=2 = global with
  per-code override (share:1 opts OUT, share:N sets grain). _share_rooms
  groups per resolved grain.
- End-to-end conf injection without monkeypatch: load_config(overrides=)
  merges run-level keys last; driver.search / innerloop.optimise /
  NativeEvaluator / _fitness_for thread conf_overrides={leaf_sharing:True}
  through both inner-loop and off-tree scorers when sharing is on.
- homemaker-evolve: --leaf-sharing/--no-leaf-sharing + --leaf-share-factor
  (env HOMEMAKER_LEAF_SHARING / HOMEMAKER_LEAF_SHARE_FACTOR).
- Example programmes untouched (§13.3/§13.9 stay reproducible). Experiment
  load_config monkeypatches updated to accept overrides=.

Tests: grain modes, opt-out, default-OFF parity, load_config overrides,
programme parse, CLI parse. 233 pass. Smoke: harbor 37 vs 95 fails on/off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 22:04:35 +01:00