39.24's sweep listed two entries as DEAD rather than suspect -- inert code that
reads as live. Neither changes a score or a failure on any corpus artefact, and
that is verified rather than asserted: every artefact scores identically to its
39.25 measurement.
ratio_public_outside and ratio_private_outside. evaluate_building read both and
multiplied a gaussian into the building factor for each. Neither key exists in
CONF_DEFAULTS and no patterns.config in the repository declares either, so both
branches were guarded and never ran. Removing them also retires what fed them:
the four public_length_*/private_length_* tracking keys accumulated per leaf in
process_storey, and the _public_length/_private_length helpers, which had no
other caller.
NOT removed, because they are live: _public_access, _public_access_outside,
_public_access_pins and the has_public_access_* tracking flags, which drive real
checks and collapse_global's preserve_public_access. Only the length-ratio
machinery was dead.
The daylight quality factor. evaluate_leaf set factors["daylight"] = 1.0
unconditionally -- pinned since the URB_NO_OCCLUSION descope (6) and unable to
be anything else. It was never in _GRADED_FACTORS, so it contributed nothing to
the graded signal, and 39.18's geometric mean then had to special-case it in
factor_is_asked as a factor that is never asked. A constant that exists only to
be excluded is worth deleting. If 2g5 rebuilds occlusion it reintroduces a real
daylight factor, which would need factor_is_asked to say True anyway.
Two tests referenced the removed factor. test_leaf_grade_ignores_non_graded_keys
now names a key that genuinely does not exist; the aggregate underflow test
dropped its daylight entry, which would otherwise have been counted as asked and
changed the expected geometric mean.
Worth doing despite changing no number: 39.20 and 39.25 were both cases where
something inert looked live -- a parity test that never ran, a per-level rule
switched off in every config -- and in both the misreading cost real time and
produced a wrong conclusion. An objective with fewer things in it that do
nothing is one where "this term does nothing" is informative rather than
routine.
Still open on dpt, each needing a ruling or a rate change rather than a
measurement: quality_size's upper side, the minimum-internal-area factor as a
third statement of "build the rooms", and the 0.5**n_fails curve.
426 passed.
Refs homemaker-py-dpt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
Owner: "maybe plot_ratio is having unintended consequences, Alexander simply
says that all levels should have accessible outside space, he doesn't say how
much."
Both halves are right, and the codebase had the two rules the wrong way round.
ratio_outside is a gaussian on the outdoor FRACTION, applied as a whole-building
multiplier, and its declared targets have no stated basis and contradict each
other: health-centre targets 0.06 and sits at 0.096-0.129, so it is penalised
x0.39-x0.77 for having too MUCH; programme-house targets 0.30 and sits at
0.098-0.293, penalised x0.40-x0.999 for having too LITTLE. Penalties as large as
the ratio_circulation ones 39.24 removed, pulling two programmes in opposite
directions on the same quantity.
force_roof_garden already implements the rule Alexander actually states -- per
level, no outdoor space at all is a hard fail, no quantity attached. It has
existed all along and was switched OFF in every corpus config.
Near-miss worth recording: measuring first, I found zero "no outside space"
fails across the twelve baseline runs and briefly read that as the requirement
being met everywhere. It meant the check never ran. Same shape as 39.20's parity
tests -- no failures from a test that is not executing looks exactly like no
failures from a test that passes, and the tell was again the config, not the
code.
Enabled, it bites on 4 of 25 baseline levels: maple s0 level 1, and
programme-house level 0 in all three seeds. A house with no outdoor space on its
own ground floor is a fair criticism of the layout, and exactly what a
building-level fraction cannot catch, since 22% outdoor concentrated on one
storey satisfies it perfectly.
The upper side ratio_outside used to provide is covered in a better currency by
the minimum-internal-area factor (internal area >= 1.2x the programme's declared
room area), which is live -- binding on harbor s0 x0.920 and programme-house s0
x0.787.
This is the one change in 39.22-39.25 whose risk is NOT measured: nothing here
proves the outdoor fraction will not drift up once the search is free to raise
it, and outdoor space is profitable (1.64 return against a room's 0.66). The
re-baseline (bk9) is what shows it, and 39.25 asks for the fraction to be
recorded there.
4 hard fails added, none removed. 426 passed.
Refs homemaker-py-hxi.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
Owner: "I think a corridor could be worth a sixth of a room, this is ok. maybe
we should dump the ratio_circulation altogether if there is already a pressure
in circulation caused by the cost benefit ratio per msq. this is the kind of
thing we want to root out of the scoring model: anything that is double
counting, or using a gaussian where a linear ramp is appropriate, etc."
value_circulation = 50 stands; hxi's rate question is closed.
The duplication argument is stronger than it first looks. score = value/cost is
already a ratio, so the per-m2 economics (50 against a build cost of 200) is not
merely an absolute pressure -- adding corridor moves value/cost by an amount
that depends on how much of the building is already corridor. It is ALREADY
proportional. ratio_circulation said the same thing again as a whole-building
multiplier, on a curve where twice the corridor is far more than twice as bad.
Correction to my own first measurement: I overrode ratio_circulation and got
scores going DOWN when a <=1 multiplier was removed, which is impossible. All
four corpus programmes DECLARE ratio_circulation, so the CONF_DEFAULTS value I
had changed was never in play and the two arms differed only in sigma. Same
trap as value_supported in 39.19.
Against the keep-it case, recorded because it is the one real argument: three of
four declare a POSITIVE target (harbor/maple 0.08, health-centre 0.10), making
the term formally two-sided rather than "less is better". It does not survive
the numbers -- the lower side is worth at most 13.3% on the large programmes
against 99% on the upper side, and "a building needs some circulation" is
enforced structurally by access and connectivity, which no amount of value can
buy off.
Disabled in CONF_DEFAULTS and the four corpus configs, each with the reason
inline and a note that a [target, sigma] pair re-enables it. Fail sets
unchanged; it was always a value multiplier. Scores +42% to +7712%.
39.24 also sweeps every remaining term against the owner's two tests. Verdicts:
perpendicular, proportion, width, crinkliness, access, size's lower side,
ratio_outside, staircase volume and the count/limit fails are all sound.
Filed as homemaker-py-dpt: size's UPPER side (cost already charges area; 82% of
size fails are over-target), the minimum-internal-area factor (a third
statement of "build the rooms"), the 0.5**n_fails curve (a ruling, not a
measurement), and two dead paths -- ratio_public/private_outside, which no
config declares, and the daylight factor pinned to 1.0 since the descope.
Also updated a test I added last turn which asserted ratio_circulation was the
second charge; it now pins that the linear ramp is the ONLY one.
419 passed.
Closes homemaker-py-hxi.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
Owner's ruling: "as long as circulation is more expensive to build than it has
value then we have a linear ramp. a gaussian ramp is probably not appropriate
here as double the amount of corridor is simply twice as bad, so it should
score the same as two half size corridors".
Both halves check out. The linear ramp is already there -- value_circulation 50
against a build cost of 200, so every m2 of corridor is worth -150 and the
objective pushes for less of it without needing a cap. And the AMOUNT of
circulation is separately governed at building level by ratio_circulation
[0.00, 0.20], a gaussian on the circulation fraction, which is where that
question belongs. The per-leaf size gaussian was a third charge on the same
thing.
It was also the only one of the three that depended on how the corridor was cut
up. One 20 m2 corridor scored gaussian(20,0,14) = 0.360 and contributed 360;
two 10 m2 halves scored 0.775 each and contributed 775 between them. Splitting a
corridor in half multiplied its value by 2.15x -- an artefact of where the tree
happened to cut, rewarding the search for fragmenting its own spine. The
ruling's test (one 2A leaf must score as two A leaves) is exactly what a
gaussian on an amount cannot satisfy, and is now a test.
size_circulation = None; quality_size returns 1.0 for circulation and
shapecurve gives amin, amax = 0, inf.
BUG this exposed: get_space_params falls through to a habitable default when a
generic family key is missing and could not tell "missing" from "present but
null", so a corridor silently inherited a room's 16 m2 size target.
_generic_param now returns (found, value); pinned by a test. The same trap
applied to 39.22's proportion_circulation.
Fail-set effect of 39.22 and 39.23 together: 16 corridor size fails and 7
proportion fails removed, none added. harbor 33/43/42 -> 32/40/38, maple
54/73/55 -> 51/65/52, health-centre 4/9/5 -> 3/9/5, programme-house unchanged.
The layouts are identical -- these are failures the objective should never have
been reporting.
Two shape-curve tests moved fixture: both built an infeasible upper storey from
a 'C' leaf, infeasible precisely because of the bounds now removed. The fixture
is a cr1 leaf, whose infeasibility is a contradiction between two of its own
bounds (needs >= 180 m2 for its aspect bound, <= 101.5 m2 for its size bound
across the box's fixed 23.52 m span) rather than a tight fit. The invariants
they test are unchanged.
Left open on hxi: the rate gap, value_circulation 50 against value_inside 300
on identical build cost. Whether a corridor is worth a sixth of a room per m2
is a design judgement, and the linear ramp is only as steep as that number.
419 passed.
Refs homemaker-py-hxi.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
Owner's ruling: "there should be no cap on the proportion of a corridor,
especially for big buildings, the crinkliness rule is there to prevent these
becoming unpleasant spaces."
proportion_circulation was [1.5, 0.5], failing a corridor above aspect 2.57 --
at the 1.97 m minimum width the width factor allows, a corridor 5.1 m long; at
the 2.4 m target width, 6.2 m. The corpus shows the consequence: median
circulation leaf 14.3 m2 at aspect 1.67, a stubby room rather than a corridor.
The justification checks out arithmetically. The unpleasant space the cap was
standing in for is a long BURIED corridor -- and crinkliness already sends that
to zero, since a buried leaf has crink == 0. A long corridor along a facade
scores 0.90. Aspect cannot tell those two apart; crinkliness can, so the cap was
duplicating a rule that already exists and does the job better.
Shipped: proportion_circulation = None (no aspect requirement).
quality_proportion returns 1.0 for circulation, and shapecurve.leaf_constraints
yields rmax = inf so the DP agrees with the fitness instead of pruning
topologies the objective would accept. A habitable room's aspect target is
untouched. The narrow side still holds -- width_circulation keeps a corridor
>= 1.97 m, and "edge too long" still caps a single wall at 8 m.
Unlike 39.14/39.18/39.19 this DOES change the fail set, which is the point.
Across the twelve baseline artefacts it removes exactly 7 corridor proportion
fails and adds none: harbor 33/43/42 -> 33/42/40, maple 54/73/55 -> 54/71/54,
health-centre 4/9/5 -> 3/9/5, programme-house unchanged.
Also recorded in 39.22, and a retraction: hxi was titled "search is rewarded
for deleting the circulation spine", which 39.8 had already measured and
refuted -- 0 of the 7 connectivity-breaking deletions sampled were rewarded. My
own earlier comment on the bead restated that retracted claim; corrected, and
the bead retitled.
What binds next: removing the cap roughly doubles a corridor leaf's reach, from
proportion at ~6.2 m to size_circulation at 12.5 m (2.4 m wide hits the 30 m2
fail edge there). size_circulation's target area is ZERO, the other half of the
double-charge -- circulation priced as overhead once in value_circulation = 50
and again in a size factor whose optimum is non-existence. Not changed: it is a
distinct parameter with its own rationale, unruled, and 39.16 is a standing
reminder about inherited constants.
415 passed.
Refs homemaker-py-hxi.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
Owner's decision: "we need to abandon the perl oracle, this was only useful
when initially porting, but I suspect many of the remaining problems have been
carried in from the perl (such as the weird scoring of outdoor and circulation
space, which definitely needs fixing)".
39 supports that second clause. Every defect the section found is inherited,
not introduced: the two-sided crinkliness gaussian that double-charges surplus
daylight (39.14), quality as a product over a variable number of factors
(39.18), value_supported priced as value_inside so a terrace was worth more per
m2 than a room (39.19), and circulation returning 0.07 per unit cost (hxi).
So parity with the oracle was never a safety net -- it was a commitment to
reproduce those defects. Each of 39.14, 39.18 and 39.19 would have been a
parity failure had parity ever been checked, and keeping the tests would have
meant reverting the fixes or explaining the failures away.
Removed: oracle.py, test_oracle.py, the two parity tests and their fixture
machinery in test_dom_corpus.py, innerloop.OracleEvaluator with its use_native
and urb_root plumbing, the same plumbing through driver, and fourteen
experiments/ scripts that could only run against Perl. Several of those are
cited in earlier DESIGN sections; the citations now point into git history,
which is the honest state -- they had been unrunnable since the oracle root
(/home/bruno/src/urb) stopped being present. run_search is superseded by
run_search_scaled, which does the same job natively.
Kept: dump_areas.pl/.py, which validate GEOMETRY against Urb (4.1) rather than
fitness, and the prose in fitness_cmd.py and dom.py explaining why the
.score/.fails formats are shaped as they are. Provenance is worth keeping; a
dead code path is not.
CLAUDE.md updated: fitness.py is the only evaluator, and "Urb did it this way"
is no longer an argument that a constant is right. 39.16 is the standing
counterweight in the other direction -- the crinkliness target WAS right and
twice looked wrong only because the code reading it was misunderstood.
Inheritance is neither evidence for nor against.
410 passed. The 69 removed cases account exactly: 64 parity (all skipped, since
no oracle .score was ever committed), 4 in test_oracle.py, and the guard test
39.20 added as a stopgap.
Closes homemaker-py-118. Files homemaker-py-bk9 for the re-baseline that 39.19
made necessary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
Harbor and maple, three 500k plateau starts x two RNG seeds, 8000 evals, all
arms scored under stock. Run against the pre-39.19 defaults, so "stock" is the
old objective.
harbor geomean 39.17 -> 39.00 1W/0L/5T diff +0.167 MDD 0.428
harbor geomean+daylight 39.17 -> 39.00 1W/0L/5T diff +0.167 MDD 0.428
maple geomean 60.50 -> 60.17 1W/1L/4T diff +0.333 MDD 1.434
maple geomean+daylight 60.50 -> 60.00 1W/0L/5T diff +0.500 MDD 1.285
Every margin sits below what N=6 can resolve (N ~= 26-67 needed). Unlike
39.13's ramp, which was an exact null with sd=0 and identical trajectories, the
arms here do diverge and all four land on the favourable side of zero -- worth
exactly what four coin-flips landing the same way is worth. Recorded so a later
run cannot cite it as evidence.
Also recorded: none of 39.13, 39.14 or 39.18 produced a measurable search
improvement at this budget. All were shipped on correctness grounds and 39.19
then required two of them outright. If the 39 objective changes pay off in
search terms, this corpus has not shown it yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
The 39.19 objective change broke three tests. They were the right ones to
break, and how they broke matters more than the change.
test_native_fitness_score_parity and test_native_fitness_fail_set_parity read a
cached .score/.fails beside each corpus .dom and assert the native Python
fitness agrees. They are the ONLY check that the native evaluator still agrees
with the Urb oracle it was ported from, and CLAUDE.md still describes oracle.py
and the Perl tool as kept for cross-validation.
But .gitignore lines 10-11 exclude *.dom.score and *.dom.fails, and
git log --diff-filter=A over those patterns finds zero files ever added on any
branch. No oracle cache has ever existed here, so on a clean checkout all 64
parametrised cases skip.
Worse than skipping is what happens when they do not. Nothing in a .score file
records who wrote it, so a .dom left in that directory by a search run -- with
a .score written by homemaker-fitness, the NATIVE scorer -- silently becomes a
parity fixture, and the test compares the native scorer with itself. That
passes by construction whatever the native scorer says.
Three such cases were live and green: the coldstart-500000-s*.dom artefacts
committed to examples/programme-house during 39.12 and scored natively this
session. They surfaced only because 39.19 made the native scorer disagree with
its own stale output; absent an objective change, a green "native matches
oracle" would have been reported indefinitely.
Stopgap: parametrisation restricted to the Perl corpus's MD5-named files so a
session artefact cannot become a fixture again; the skip message now says
parity is UNVERIFIED rather than reading like an optional missing cache; a
guard test asserts the restriction. All 64 cases skip honestly. Regenerating
the caches with the native scorer would not have been a fix -- it would have
re-cemented the self-comparison.
Filed as homemaker-py-118 (P1): regenerate fixtures from the Perl oracle,
narrow the ignore rules so fixture caches can be tracked, and find out whether
parity still holds -- it may not, since 39.14, 39.18 and 39.19 all changed the
native objective and the oracle has none of them. If parity is being abandoned
deliberately the tests should be deleted with a note. What must not survive is
a test that looks like a guarantee and is not one.
415 passed, 64 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
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
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
The owner supplied the provenance the analysis was missing. The constant is
Christopher Alexander, A Pattern Language 159, "Light on Two Sides of Every
Room", and it changes what the numbers mean.
1/crink = A/(L*h) is floor area per metre of ILLUMINATED wall over storey
height. It is not room depth -- it equals depth only for a room lit on one
side. So 5/6 * h = 2.5 m is 2.5 m of room depth PER WINDOW WALL: one side
allows 2.5 m at the peak and 4.86 m at the fail edge, two opposite sides allow
5.00 m and 9.72 m. A 4 m room scores 0.395 lit on one side and 0.902 lit on
two. The factor is the pattern stated as a ratio, and it is not
miscalibrated.
WITHDRAWN from 39.14: the "2.5 m absurd optimum" reading, and "the corpus's
realised median depth is 2.95 m, so the search built what it was paid for" --
2.95 was the median A/L, while the corpus's single-aspect leaves are a median
3.46 m deep and its two-opposite leaves 4.42 m. Ordinary rooms. Section
retitled, passage struck in place.
WITHDRAWN from 39.15: calling six specs "self-contradictory". They are large
rooms, and under Alexander a large room is supposed to need two aspects; the
audit's new column reports the pattern working, not a mis-specification. What
is real is the tension between that demand and what the plan form supplies.
SURVIVES, on a better argument: crinkliness_shape="daylight". 159 states a
MINIMUM, and a two-sided gaussian turns a minimum into a target -- 68% of the
133 leaves in the clipped region are lit on two or more sides, mean quality
0.770, docked for satisfying the pattern well, on top of the
exterior_wall/boundary_wall charge those windows already carry in cost.
New 39.16 records this and relocates the residual. Over the 430 graded
baseline leaves: unlit 77 (100% fail), one side 208 (15%), two-corner 87 (2%),
two-opposite 34 (0%), three/four 24 (4%). Light on two sides all but
guarantees a pass and only 33.7% of leaves get it, so the open question is why
a binary slicing tree on a convex plot can only give a third of its leaves two
aspects -- a plan-form question, not a scoring one. Filed as
homemaker-py-773; 39.11's courtyard finding is the same question from the
other side.
Both errors came from reading a dimensionless ratio as a length, so the
provenance and the interpretation now sit next to the constant in fitness.py,
not only in DESIGN.md.
405 passed, 72 skipped.
Refs homemaker-py-u5q.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
39.13 -- the tail rescale, and its verdict. NULL, and not for want of power:
12 of 12 pairs byte-identical, both programmes, same trajectories. The failing
tail is 0.034% of corpus value, so making it orderable cannot move a search.
Kept (free, and k54 needs the region orderable) but recorded as correct and
inert, not as a fix. It also corrects gvb's premise: zero exposure is NOT
beyond the inner loop's reach -- perturbing division ratios alone moves the
zero-exposure set on 6-12 of 12 trials at +-25%, and in the direction wanted
(harbor s1 8 -> 6 buried leaves).
39.14 -- what the factor actually rewards. 1/crink is the room's depth from
its daylit wall in storey-heights, so the variable is sound and its fail
boundary (1.62h = 4.86 m) agrees with 38.3's independently-derived frontage
bound. The two-sided gaussian on it is not: the near side penalises surplus
daylight that edge_cost and outside_edge_cost already bill at 100 and 133.3
per m2, it has never once produced a fail (it needs crink > 21.5; corpus max
is 3.95), and its peak sits at a 2.5 m deep room -- an ordinary 4 m room
scores 0.395 and the corpus's realised median depth is 2.95 m. The search
built what it was paid for. A/B at pilot budget is underpowered rather than
null: the arms reach different layouts but the same fail counts.
39.15 -- the magic numbers. A sigma is not a preference, it is an acceptance
interval target +- 2.1460*sigma, so it decides failures. The blanket
hypothesis does not survive -- programme-house reaches 1 fail, structural on
two of three seeds. The specific one does, and it shows 39.1's CLEAN verdict
answered a weaker question: sweeping a spec's tolerance box asks whether SOME
shape is feasible, and all 67 pass, but at the DECLARED target area and aspect
harbor needs 7 corner rooms and maple 6, while health-centre and
programme-house need none. Within-programme, those codes fail 62% and 78% of
their instances against 26% and 31% for all others. Three declared quantities
are jointly contradictory and nothing said so; the resolution is an author
decision, not a retuned constant.
Also recorded: 82% of size fails are rooms larger than target, which is the
same shape of double-charge but explicitly NOT the same case -- size's upper
bound is the main brake on growth and must not be removed on the analogy.
405 passed, 72 skipped.
Closes homemaker-py-9gj, homemaker-py-u5q.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
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
With tym landed the DP finally runs on real leaf-sharing searches, so v4s
asked for the search-level A/B. No A/B was run: two structural facts make
the payoff zero before any seed is drawn, and measuring a no-op would have
produced a null that reads like a measurement rather than a fact.
Reach. driver._evaluate gates the warm-start on `x0 is None`, and every
child gets x0 = innerloop.warm_x0(...) from its parent (driver.py:764), so
it fires only for bootstrap-population individuals. Instrumented over a
4000-eval run: 8 DP solves, exactly pop_size. At the corpus baseline's 500k
budget that is 8 evaluations out of 500,000.
Applicability. Constructed-seed feasibility over 6 seeds: harbor-house 0/6
with leaf_sharing on AND off, programme-house 0/6, harbor-house-l0 4-5/6
(75% over a real search). Even those 8 bootstrap individuals get no warm
start on the real programmes; only l0, the reduced programme the DP was
validated on, is feasible.
The infeasibility is correct, checked because it is the dangerous
direction: on 4 harbor topologies the DP rejects, an NM polish minimising
the shape-fail family reaches 14/16/17/16 fails, never 0 -- 0/4 false
negatives. Full harbor is genuinely shape-infeasible per topology where l0
is not. Per topology only: programme-house reaches 0 hard / 1 soft in the
500k baseline while its constructed seeds are DP-infeasible.
shapecurve_prune is separately inert -- it acts only inside the
feasibility_max_shape_fails branch and its exact arm needs best_n_fails<=0.
tym was still worth doing: the DP now models leaf-sharing exactly (38.23)
and fires on real runs, which is what turned an untestable question into a
structural answer.
Filed homemaker-py-ldj for what would have to change: give the DP reach
beyond the bootstrap, and/or use infeasibility as a ranking signal (an
infeasible topology still has a shape-fail floor, which is what a
pre-filter wants) rather than a precondition that discards it.
Closes homemaker-py-v4s.
Lint at parity (46).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
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
Three times in this log a verdict rested on a sample that could not have
produced it: 38.19 (programme-house claimed at N=20, resolves at N=60),
38.21 (harbor at n=3 resolves nothing finer than ~15 fails, yet every
recorded margin is smaller), 39.5/38.20 (a 10-fail cpsat margin inside a
+-23-fail noise band). Each was found years later.
experiments/ab_report.py makes it visible when the verdict is made:
minimum detectable difference = t_crit(0.975, N-1) * sd / sqrt(N)
A margin below the MDD is not a weak result but an absent one -- the
experiment could not have distinguished it from zero however it came out.
The report flags that, refuses to endorse a winner, and states the N needed.
Validated against both datasets measured this session, reproducing the
hand-computed figures exactly:
programme-house N=60 +0.567 p=0.017 MDD 0.462 verdict supported
programme-house N=20 +0.700 p=0.085 MDD 0.805 UNDERPOWERED, N~=26
harbor N=24 +1.208 p=0.502 MDD 3.669 UNDERPOWERED, N~=202
Harbor needing ~200 seeds means it cannot answer the collapse_insearch
question at any N this project would realistically run.
Fixed a defect in my own first version: with all-ties (sd=0) the MDD
collapses to zero and the naive abs(mean) < mdd reported "margin exceeds
the MDD -- verdict supported" for a margin of 0.000, with t=nan. A reporter
that endorses a zero margin is worse than none. Degenerate cases are now
explicit and distinguish all-ties from a constant non-zero difference.
Separate correction found while validating: 38.19's published p-values for
N=20 and N=40 were 0.069 and 0.045, from a normal approximation. The exact
paired t-test gives 0.085 and 0.052 -- so N=40 did NOT reach significance
either; it took N=60. The approximation was anti-conservative, making
results look more significant than they are, the same direction of error
this thread is about. Corrected at all four citing sites, and the "needs
N >= 40" guidance raised to N >= 60.
Closes homemaker-py-tco.
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
0wr asked which harbor A/Bs were decided by a narrow margin before 39.4.
Measuring harbor's variance makes the margin-by-margin triage moot.
Harbor's paired seed-to-seed sd is 6.19 fails (24 paired ON/OFF runs,
budget 2500), so n=3 resolves nothing finer than 15.4 fails. Every recorded
harbor margin is below that: 13.9 share_edge_cap 3.7, 20 qpk 8.3, 23 f1d
mixed, 37.1 tiering 6.3. So it is not that SOME harbor results were narrow
-- no harbor A/B run at three seeds could resolve the margin it reported,
independently of what 39.4 did to the programme. Of the 220 possible
3-seed subsets of the 24 runs, 56 (25%) show a clean 3/3 sweep for ON.
Re-measured 20's harbor arm, the one backing a live default:
N=3 2W/1L/0T +2.67 p=0.560
N=12 8W/3L/1T +3.50 p=0.076
N=24 13W/10L/1T +1.21 p=0.502 CI [-2.46,+4.88]
Null. The published "harbor: ON wins 3/3, 80.3 -> 72.0" was a lucky draw --
even seeds 1-3 measured here give 2W/1L, not a sweep.
So 20's claim that the qpk verdict "holds at both example scales tested" is
withdrawn and annotated in place. collapse_insearch's default rests on
programme-house alone (38.19, N=60, +0.57, p=0.017). It is not refuted on
harbor -- direction positive but indistinguishable from zero -- but harbor
must not be cited as corroboration.
Harness generalised (PROG/BUDGET/WORKERS) and results kept.
Filed homemaker-py-... : A/B harnesses should report the minimum detectable
difference for the N they run, so an underpowered verdict is visible when
it is made rather than years later.
Closes homemaker-py-0wr.
Lint at parity (46).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
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
The follow-up issue was cited by an ID written before the issue was
created. The real one is homemaker-py-7t1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
39.5 concluded the exact CP-SAT seeder beats greedy (harbor 102 -> 92,
maple 156 -> 154). Re-checked because fdp made the arms deterministic and
3qj made the model 7.5x slower. Three findings.
A live bug in the cap, found on the way. solve_room_labels sets a
deterministic work-unit budget (4.0) and a wall-clock backstop, commented
as "a pathological-case backstop only". At 2.0s it had become THE BINDING
CONSTRAINT: 2 of 24 harbor solves returned FEASIBLE not OPTIMAL, wall time
hit exactly 2010 ms, and the deterministic budget was never reached (max
2.483/4.0). Those labellings were suboptimal AND load-dependent -- the wall
clock is exactly the cap 39.5 added the deterministic one to escape. Cause:
38.14's t -> n adjacency makes the model much harder, and the 2s value
dated from when solves took ~124 ms. Raised to 30s; 24/24 harbor and 36/36
maple now OPTIMAL, deterministic budget still in headroom (3.569/4.0).
The verdict reverses. Deterministic, 12 seeds, scored canonically:
harbor greedy 1323 (722h) 0.079 s/seed cpsat 1548 (908h) 1.623
maple greedy 1764 (777h) 0.063 s/seed cpsat 2256 (1213h) 1.327
cpsat loses on both, +225 and +492 fails at ~21x the seeding time,
concentrated in hard fails.
Time and quality have different causes. Removing t -> n from harbor takes
cpsat 1.623 -> 0.193 s/seed (8.4x faster) but it is still +205 vs greedy
(was +225) -- so the adjacency explains the time blow-up and ~9% of the
quality gap; the regression is otherwise pre-existing.
Squaring with 39.5: that section records cpsat returning 194/180/171/182
over four identical 10-seed aggregates before the determinism work. Its
10-fail harbor margin sits well inside a noise band that wide, and was
measured with fdp's id()-ordered room_slots live. The seeder-level claim
was never established rather than overturned. 39.5 annotated in place.
Absolute totals are ~6x 39.5's because the objective has changed, so they
are not comparable to that table; the within-measurement comparison is
like-for-like and is what the verdict rests on.
No default changes: assign_solver was already greedy for 37.7's independent
reason. What changes is that "cpsat wins the seeder A/B" should no longer
be cited as a reason to pursue it.
The cap fix takes the suite from ~4.5 to ~10 min and the tests cannot opt
out, since constructive_topology does not thread the solver limits through.
Filed as homemaker-py-2xk.
Closes homemaker-py-vjd.
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
38.18 confirmed the 1ph default-flip was sound for its own era, but that
measurement predates three changes to the objective it was measured
against -- 39.4's namespace fix, 38.10/38.11's per-space crinkliness, and
38.12's missing-space cascade -- and collapse_insearch runs collapse_global
inside every eval, valued against exactly the factors those touched. A
default carried on a superseded measurement is an assumption, not a result.
Re-ran the 1ph protocol as published on the current codebase:
N OFF ON W/L/T diff t p
published 1ph 20 7.95 7.10 11/6/3 +0.85 2.38 0.028
current objective 20 7.85 7.15 10/7/3 +0.70 1.82 0.069
current objective 40 7.60 7.03 21/14/5 +0.57 2.01 0.045
current objective 60 7.58 7.02 29/19/12 +0.57 2.45 0.017
Verdict: the default STANDS. At N=60, mean diff +0.567 fails/seed, paired
t=2.454 (df=59), p=0.0171 exact, 95% CI [+0.105, +1.029] excluding zero;
Wilcoxon signed-rank cross-check agrees (p=0.0138), which matters because
fail counts are small integers and normality is not obvious.
Two caveats. The effect is about a third smaller than published (+0.57 vs
+0.85) -- partly regression from a lucky N=20 draw, partly plausible real
erosion, since several fails collapse_global used to clear have been
redefined out of existence or made harder.
More usefully: the published N=20 can no longer detect its own effect. At
exactly that sample size the current answer is p ~= 0.069, a null by the
conventional threshold. Had I stopped at N=20 the honest report would have
been "the 1ph verdict no longer reproduces" and the default would have
looked unjustified. It took N=60 to resolve. That is the 8sh/1ph/qi6/lj3
pattern this log warns about, now biting the flagship result itself: any
future re-validation of this default needs N >= 40.
20 annotated in place so a reader of the original claim sees the current
figure. Harness takes a seed range now (APPEND=1 to extend a sweep).
Closes homemaker-py-ioe.
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
d86 asked for the rigorous version of 35's spot-check -- check out the
codebase at the 1ph commit, backport the iio stale-leaf-share fix, re-run
the historical seed sets.
One constraint had to be worked around: this repository's history begins
2026-07-30, six days AFTER the 1ph commit (2026-07-24), so that checkout
does not exist here. Closest reachable stand-in is 391f510 (2026-07-30), a
genuine ancestor of the iio fix and therefore pre-iio, carrying that era's
examples/ and objective. Measured there, not at the true 1ph commit, and
that is a real limitation of the reproduction.
Protocol as published: programme-house, budget 3000, 4 workers, seeds 1-20,
ON vs OFF, both arms finished with --collapse. Run twice over one worktree,
as-is then with the 22-line iio hunk applied.
OFF ON W/L/T mean diff t(df=19)
published 1ph 7.95 7.10 11/6/3 +0.85 2.38
pre-iio 391f510 8.05 7.10 11/6/3 +0.95 2.59
same + iio fix 8.05 7.10 11/6/3 +0.95 2.59
The published verdict reproduces, and the iio fix changes nothing: 0 of 40
(seed, arm) cells differ, per-seed counts equal cell by cell.
It could not have been otherwise. The bug needs a leaf carrying a STALE
share/share_type, and leaf-sharing only stamps a share where a code has
count > 1 -- programme-house declares count: 1 for all six codes. Over 8
constructed seeds at that commit, programme-house had share_type set on 0
of 56 leaves; harbor-house on 24 of 128. _collapse_value reads
leaf.share_type, so the bug is structurally unreachable on the 1ph
protocol. That is why 35 saw 2/3 harbor seeds diverge by 5-8 fails while
programme-house at N=20 moves not one cell: harbor has codes at counts 10,
6 and 5.
20's retroactive caveat is discharged for the 1ph section and stays live
for harbor-house/qpk, where shares exist and divergence was measured.
Kept experiments/rerun_1ph_protocol.sh and both result TSVs.
Closes homemaker-py-d86.
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
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
run_staged_search.py reported MISMATCH on its BASELINE arm -- the
LEAFSHARE=0/MULTIUSE=0 control every A/B compares against. Two facts
combined: driver.search_staged had no collapse_insearch parameter at all,
so every inner search() call inherited search()'s True default
unconditionally; and no example patterns.config sets the key, so the final
_native_score rescore got False from a bare load_config. Search optimised
one objective, the rescore graded another.
The 7ua fix pinned the key inside a fitness.load_config monkeypatch, but
that patch was installed only `if leaf_share or multi_use` -- so it fixed
every arm except the control.
Fixed in the right place: search_staged now HAS the parameter (default
True, byte-identical to the inherited default), threaded into all three
internal search() calls. The harness chooses the arm explicitly (COLLAPSE,
default 1), passes it to the search, and passes the SAME value to
_native_score, which overrides the key rather than hoping the config
carries it. The rescore mirrors the search by construction.
Verified on programme-house, budget 150:
baseline MISMATCH 1.56663e-08 vs 1.51708e-08 -> OK
COLLAPSE=0 (knob did not exist) -> OK 1.66216e-08
LEAFSHARE=1 / MULTIUSE=1 -> OK
COLLAPSE=0 scoring differently confirms the knob is not a no-op, and the
default arm's search result is unchanged, so no prior staged number moves.
Audited the other three search_staged callers: run_and_capture_91f.py
already pins collapse_insearch: True; run_island_ab.py never re-scores;
probe_harbor_floor.py did NOT pin it and had the same bug -- now fixed, and
that is the harness which produced every 13.x floor number.
The recorded mitigating factor -- only the continuous score moved, the fail
count matched, and the run_*_ab.sh greps read only the count -- is true and
is exactly what made it dangerous: a harness that reports MISMATCH on its
own control, invisibly to the metric of record, trains everyone to ignore
the warning.
Closes homemaker-py-4ok.
Lint at parity (46); tests 381 passed, 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
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
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
39.11 found health-centre demanding 240 m2 of floor on a 183 m2 plot --
131%, single storey, nowhere for the overflow to go. Every room came out at
0.60x its declared target, 100% undersized, uniformly, and no amount of
searching could fix it.
Owner's ruling: enlarge the plot, sized assuming the building has a
courtyard. The plot is a quadrilateral here, so the courtyard is interior
space the search carves out, not a hole in the site. Sizing must therefore
allow for rooms (240, declared), circulation (~65, ~27%, NOT in demand --
the search creates C leaves) and courtyard (~36).
Scaled about the polygon centroid by k = 1.4606, chosen so the INSET area
lands on 400 m2. Centroid scaling preserves the site's irregular shape and
its one private edge: same site, larger.
plot 183.2 -> 400.0 m2
rooms/plot 131% -> 60% (harbor's ratio exactly, and harbor works)
daylit perim 41.5 -> 61.3 m (49.4 needed)
median room area / (target x share) 0.60x -> 1.00x
The last figure is the one that matters -- 0.60x was the geometric
signature of the infeasibility, now level with harbor/maple's 1.01x.
The courtyard stays REQUIRED by the geometry, not merely permitted: a room
is at most 4.86 m deep before failing crinkliness, so a ~20x20 plot with a
daylit ring leaves a 106 m2 core that cannot reach an external wall.
Enlarging made room for a courtyard; it did not remove the pressure that
produces one.
evolve._preflight is now silent on health-centre.
Closes homemaker-py-7b7, unblocks homemaker-py-7xb.
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
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
No config change -- health-centre re1 already declares crinkliness: none.
This settles the corpus classification at 14 spaces: storage,
plant/mechanical, utilities closets, records room. Replaces the hedge in
38.11 with the ruling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
Ruling: corridors need daylight. Only rooms not occupied from day to day --
a cupboard, a store, a plant room -- do without it.
That overturns the classification 38.8 and 38.9 rested on. Those sections
exempted internal circulation, covered courtyards, WCs, laundries and
reception on my inference rather than any ruling. None are exempt: a
corridor is occupied all day every day, a waiting room likewise, a laundry
is a room people spend time in, a sterilisation room is a workplace.
Re-measured with the classification READ FROM THE CORPUS (a space is exempt
exactly when its own patterns.config declares crinkliness: none) instead of
inferred: 24 of 271 crinkliness fails (9%) were not defects -- not 136
(50%), not 61%. Overstated by about six times.
- uncrinkliness_circulation stays at [5/6, 1.1/3]. 38.10 called its
equality with the habitable target the purest case of an untuned value;
it is not a bug, it is the correct answer reached by default, since
corridors want daylight on the same terms as rooms.
- corpus declarations narrowed 18 -> 14. usage: utility was too coarse and
swept in Laundry Rooms and a Sterilisation Room. What remains is
storage, plant/mechanical, utilities closets and a records room.
- audit_crinkliness_truth.py rebuilt to read the classification from the
configs rather than infer it from usage:, so it cannot drift from the
ruling again.
- 38.8/38.9's inflated figures flagged in place, with 38.11 carrying the
correction.
What survives, none of it dependent on the classification: the 38.6
critique of the three modes; usage_daylight being mis-keyed; the 38.8 A/B
yardstick being wrong; and the per-space crinkliness mechanism itself.
ssz's opening measurement was right but its reading was wrong -- under this
ruling a buried leaf usually IS a defect, so scoring it zero is largely
correct. The narrowed remnant is that the score is FLAT rather than low, so
two equally-buried layouts cannot be ranked. Filed as homemaker-py-9gj; it
is a search-mechanics problem, not a calibration one.
Lint at parity (46); tests 369 passed, 2 pre-existing failures (ut5's stale
baseline and 1ue), both failing identically before this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
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
Correction 1: 38.8's A/B scores every arm under stock urb, which is only
sound if stock is ground truth -- and stock is what this section shows is
miscalibrated. Scoring the repair under the objective it repairs penalises
it for repairing. That result is not evidence against usage_daylight.
The measurement that matters needs no search: are the emitted failures
true? audit_crinkliness_truth.py classifies every stock crinkliness fail by
the leaf's declared usage. 164 of 271 (61%) are reported against spaces
that want no daylight -- stores, WCs, plant, corridors, covered courtyards
-- and it holds on evolved artefacts (maple generated.dom: 64%), not just
constructed seeds. Under value *= 0.5 ** len(failures) each one halves the
fitness of a design that did nothing wrong.
Correction 2: usage: is the wrong key, and usage_daylight as written
mis-keys it. 39.7 established usage: as an ACCESS-requirement class.
"Needs no special access" and "needs no window" are different questions,
and the corpus separates them: usage: none is Waiting Room, Reception,
Reception Office, Entrance Foyer -- a waiting room plainly wants daylight
and DAYLIGHT_USAGES exempts it. The usage: bedroom half is right, but by
luck of where the access axis happened to fall.
The audit is robust to that error (61% -> 57% if none is reclassified), so
the finding stands; the design does not. Daylight needs its own declared
per-space axis, decided by the programme author as usage: was.
usage_daylight stays default off and is not the shipping fix. The
compact-side clip is the right shape for the factor, so the mechanism is
retained pending a daylight: attribute to key it on.
Higher-power A/B stopped: it was chasing a score against a flawed objective
with a mis-keyed fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
Fixed-budget A/B (3000 evals, 3 seeds, harbor + maple, every arm scored
under stock urb so the permissive modes cannot win by deleting a fail
category).
usage_daylight's paired hard-fail deltas are harbor [0,-1,-10] and maple
[0,+2,-10]. The means (-3.7, -2.7) flatter it: the whole signal is seed 2
in both programmes, and seed 2 is the seed where stock itself does worst.
Two seeds in three are flat or slightly worse. On that seed soft rises as
much as hard falls (harbor -10h/+9s, maple -10h/+15s), so totals go
62->61 on harbor and 120->125 on maple.
Because the scoring is stock, that is a genuine trade of hard failures for
soft ones, not a relabelling -- progress under the tiered comparator, where
n_hard is primary, but a fail against this issue's acceptance criterion
("without inflating soft"). Which yardstick is right is now the live
question, and it is gvb's question as much as ssz's.
usage_daylight stays default off; undecided, not refuted. Higher-power run
(urb vs usage_daylight, 10 seeds) is running.
The diagnostic half stands independent of the search A/B: the objective
demands daylight of two thirds of the buried population that does not want
it, and 38.6's contrary null was an artefact of three modes that never
touched those leaves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
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
Closes homemaker-py-tdp. The bound it was built on (every interior leaf
needs L >= A/(1.6202*h) of daylit wall) is sound, but tdp applied it to a
FULLY BUILT plot. None of these programmes ask for a fully built plot.
Recomputed against the area each programme actually demands, harbor-house
and maple-court are not frontage-infeasible: they need 49 m2 and 22 m2 of
courtyard against 277 m2 and 424 m2 of spare plot. The "2.7x / 2.9x short"
figures are withdrawn, and with them the claim that the plateau programmes
are unsatisfiable as specified -- the plateau remains unexplained.
One corpus programme is genuinely infeasible, for a much cruder reason:
health-centre demands 240 m2 of floor on a 183 m2 plot (131%), single
storey. Every room lands at 0.60x its declared target, 100% undersized,
uniformly. Filed as homemaker-py-7b7, blocking homemaker-py-7xb.
- evolve._preflight: two closed-form checks at startup (does the demand
fit the plot; is there enough daylit wall for it). Advisory only, it
never blocks a run -- an author may be exploring an over-tight brief
deliberately. Silent on programme-house.
- diag_exposure_frontage.frontage_budget reports the full budget.
- DESIGN.md 39.11 with the corrected corpus table; 38.3 marked PARTLY
RETRACTED and cross-referenced.
Both measure plot area and frontage through geometry rather than the raw
init.dom corners, so they carry the wall_outer inset and plot rotation,
and "daylit" means what Fitness.area_outside means by it. A hand-rolled
first version skipped the inset and read ~1 m / ~14 m2 optimistic per
plot; 39.11 carries the corrected numbers.
Lint unchanged at 46 pre-existing findings; tests unchanged at 7
pre-existing failures (the uncommitted evolved-3M*.dom fixtures,
homemaker-py-bdf).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
§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
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
§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
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
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
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
Answers "are any config requirements actively fighting the engine". One is.
§39.1 NEGATIVE (recorded): no room spec in any corpus programme is internally
contradictory. Using shapecurve.leaf_constraints' validated FAIL_THRESHOLD
inversions, every code admits an (area, aspect) satisfying size, width,
proportion and crinkliness at once, and none needs more than one exposed side.
The "estimated targets are mutually unsatisfiable" hypothesis is falsified.
§39.2 SEVERE: Urb's type system is prefix-based (c = circulation, o/s =
outside) and programme codes share that namespace. A code starting with those
letters is silently reinterpreted, with three unannounced consequences:
check_space_counts SKIPS it outright (never required, no missing or too-many
fail); get_space_params returns generic *_circulation/*_outside params before
consulting self.spaces; and is_circulation/is_outside flip, changing value
rate, crinkliness exemption, and whether it supplies daylight to neighbours.
harbor-house is affected (maple-court, health-centre, programme-house are
clean): cr1 "Common Room with Fireplace" has all three declared targets
overridden (size 80.0 -> 0.0/14.0) and is valued at 50/m2 not 300; of x2 and
st1/st2 lose width/proportion and are treated as outside space. 5 of 37 room
instances (14%) are silently optional.
Measured: the two cr1 leaves converged to 32.9 and 17.1 m2 against a declared
80, with no too-many-spaces fail despite count:1; of/st1/st2 are absent from
the result with zero fails. Compounds with §38.2 -- the largest room in the
programme sits on the wrong side of the x6 circulation value gap, so the
objective is paid to shrink it.
Benchmark validity: every harbor-house fail count in this document was
measured against a 32-instance effective programme, not the 37 its config
declares.
Adds experiments/audit_programme_config.py (namespace + satisfiability
reports). Filed homemaker-py-ju3 (P0). No src changes; 336 passed, same 7
pre-existing fixture failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
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
Diagnostic investigation of why search stalls in local minima. Adds
experiments/diag_exposure_frontage.py (frontage/exposure/value reports,
no search run required) and records the findings as §38.
Core mechanism: quality_uncrinkliness returns a hard 0.0 for any leaf with
no daylit wall, and since leaf quality is a product feeding
value += quality * rate * area, every buried room contributes exactly zero
value while still costing. 45-56% of interior leaves are in this state under
the default construction stack.
Consequences measured, not inferred:
- Deleting a buried O leaf improves the score x85, a buried C leaf x62.
Nothing pins circulation or outside space, so the search is rewarded by two
orders of magnitude for deleting the circulation spine. This retro-explains
§18, §21/§22 and the level-not-connected fails surviving >1M evals.
- Closed-form frontage bound: every interior leaf needs exposed wall
L >= A/(1.6202*h). harbor-house supplies 54m against 148m needed (2.7x
short), maple-court 56 vs 162; health-centre and programme-house are
feasible. The corpus plateau is predicted by frontage deficit alone.
- Crinkliness is tiered SOFT but 60-100% of its fails are zero-exposure,
which is topological, so §37.1's tiered comparator is mis-informed about
the largest fail category.
- The missing-space cascade emits one extra fail per declared size/width/
proportion key, so under 0.5^n a missing room is weighted 4x differently
depending on patterns.config verbosity.
Filed as homemaker-py-ssz, hxi, tdp, gvb, 1i8 (plus bdf for the pre-existing
fresh-clone test failures found en route).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
homemaker-py-7ua is fixed but only covers run_staged_search.py's
LEAFSHARE/MULTIUSE-gated rescore path. Baseline (LEAFSHARE=0/MULTIUSE=0)
runs still MISMATCH for the same underlying reason -- driver.search_staged
has no collapse_insearch param and always runs with it on -- tracked as
the new homemaker-py-4ok.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uCyZJCh5mBuA2yEFxgQpo
Research spike (DESIGN.md §37.8): classical rectangular dualization assumes
one-vertex-one-rectangle, which breaks on harbor's circulation hub (an
emergent-shape multi-leaf region, not a fixed single module) and is overkill
on the room-only adjacency graph (a trivial 3-edge matching already fully
satisfied by §11.7's seeding). No literature precedent for the multi-storey
stacking constraint either. Not prototyping; no code changes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uCyZJCh5mBuA2yEFxgQpo
Full harbor+maple/3-seed/20k-budget run superseded the earlier inconclusive
pilot table; section now records the closed verdict (no clear win, both
flags stay default off) instead of "driver-level INCONCLUSIVE at pilot
scale".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
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
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