The tail rescale shipped in cd392e7 is a measured NULL as a search
intervention -- 12 of 12 pairs byte-identical on harbor and maple, 8000 evals
from a plateau, not merely underpowered. Of course it is: the whole failing
tail is 0.034% of corpus value. Looking at the rest of the factor, prompted by
the owner, found something much larger above the threshold.
crink = area_outside/area = (L*h)/A, so 1/crink = A/(L*h) is the room's mean
depth from its daylit wall in storey-heights. That is the right variable for a
daylight rule, and the fail boundary it implies (1/crink = 1.62, i.e. 4.86 m at
h=3) is a sensible one that agrees with 38.3's frontage bound derived
independently. What is wrong is hanging a TWO-sided gaussian on it:
* The near side penalises a room for having MORE daylit wall than target --
while leaf_cost's siblings edge_cost and outside_edge_cost already charge
that same wall at exterior_wall=100 and boundary_wall=133.3 per m2. The wall
is billed once in cost and again as lost value.
* It never earns its keep as a failure either: the over-exposed branch only
reaches FAIL_THRESHOLD above crinkliness 21.5, and the corpus maximum is
3.95. It has never produced a single fail; it only removes value.
* 133 of the 318 passing graded leaves in the 500k baseline (42%) sit on that
side, mean quality 0.810.
crinkliness_shape="daylight" (default OFF, "gaussian" is stock) clips it: a
room shallower than the gaussian's peak scores 1.0, because daylight is a
sufficiency requirement and surplus is the cost model's business, not this
factor's. Clipping at the PEAK rather than at FAIL_THRESHOLD is deliberate --
it keeps the factor continuous and preserves the graded approach to the
daylight limit, where clipping at the threshold would put a 10x cliff on the
exact boundary the 0.5**n fail multiplier already steps on.
Fail set byte-identical on all 21 corpus artefacts for all four
shape/tail combinations, so stock stays a valid yardstick for every arm.
Area-weighted crinkliness quality 0.480 -> 0.513, leaf quality product
0.2722 -> 0.2831; per-artefact score +0.2%..+19.6%, and unlike the ramp it
reaches health-centre and programme-house, where the tail change was 0.000%.
Note "daylight" clips the OPPOSITE side from 38.1's superseded compact_ok,
which forgives being buried; composing either with those modes is refused.
ab_9gj_ramp.py becomes ab_9gj_crinkliness.py and takes named arms, since it
now covers both changes; its first arm is the baseline and the yardstick.
Refs homemaker-py-9gj.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
quality_uncrinkliness evaluates a gaussian at x = 1/crink, so its exponent
grows like 1/crink^2 and underflows a double to exactly zero below crink ~
1/15. Measured over the twelve 500k cold-start runs (39.12): 430 leaves carry
a minimum-exposure requirement, 112 fail it, and those 112 span quality
1e-300..1e-1 while contributing 0.034% of total value on 23% of the floor
area. Every value in that range is numerically zero beside a passing leaf's
~1, so the search cannot rank two layouts that differ only in how exposed
their under-lit rooms are.
This is wider than the bead's diagnosis (a flat 0.0 for zero-exposure leaves)
and it explains why 38.1's `floor` mode measured as a no-op: max(q, 0.01) maps
110 of the 112 onto one constant, replacing a flat zero with a flat 0.01.
crinkliness_tail="ramp" (default OFF, "gaussian" is stock) replaces the tail --
only the tail, only below FAIL_THRESHOLD, only on the compact side -- with a
straight line in crinkliness meeting the gaussian exactly at the crossing.
_crink_at_fail_threshold inverts the gaussian there using the same truncated
_E the factor is evaluated with.
Deliberately conservative: nothing at or above FAIL_THRESHOLD moves, so no
calibration changes and no leaf crosses the threshold. The fail set is
byte-identical on all 21 committed corpus artefacts, the four init.dom seeds
included -- asserted in tests/test_fitness_crinkliness_tail.py, not assumed.
That invariance is also what makes it legal to score both arms of the A/B
under stock (the 38.9 trap's one exemption). A fully buried leaf still scores
exactly 0; this restores an ordering within the failing region, it does not
forgive it. Composing with 38.1's superseded modes is refused, since both
rewrite the same tail.
Score effect on the baseline artefacts: +0.3%..+2.8% on harbor and maple,
exactly +0.000% on health-centre, programme-house, and every init.dom -- a
programme with no partially-exposed failing rooms has nothing to grade, and
neither does any starting layout. The ramp is a mid-search signal by
construction, so experiments/ab_9gj_ramp.py defaults to seeding each run from
a 500k plateau artefact rather than cold.
The module-level math import replaces a now-redundant local one.
DESIGN.md 39.13 and the A/B verdict follow in a separate commit.
Refs homemaker-py-9gj.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
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
fitness.py already imports the module as `_programme` at the top and uses
that idiom elsewhere (`_programme.SOCIABLE_USAGES`). The local re-import
sat in the per-leaf hot path for no reason.
Also snapshots the in-progress ab_ssz_search.csv; the full run is still
going and will supersede it.
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
§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
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
Python silently shadowed the gnw-scope process_storey with the later
hgg-extended one; the first ~45 lines were unreachable dead code that
still read as live. Deleted; the extended definition is a strict
superset. Suite: 405 passed (pre-existing 5 CP-SAT/reassign failures
unrelated, confirmed present on main before this change).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uCyZJCh5mBuA2yEFxgQpo
driver.collapse_best built its evaluator with _fitness_for's default
collapse_insearch=True, so collapse_finish's base_fails/cand_fails were
both measured through score_with_fails' own auto-collapse pass -- base
silently equalled collapsed on 5/5 probed files, making the "keep only
if fails don't increase" safety guard vacuous and understating 94g's
real effect in logs. fitness.collapse_finish now forces canonical
(collapse_insearch=False) scoring for its own measurement regardless of
self's config; collapse_best now builds its evaluator canonically too
(matching what homemaker-fitness reports for the written .dom) and
threads max_share/conn_grade through. Same-family fix in
search_annealed's no-polish-budget rescore branch, which silently
defaulted to collapse_insearch=True via _evaluate's default.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uCyZJCh5mBuA2yEFxgQpo
Splits the flat outer-search comparator (-n_fails, fitness) into a tiered
(-n_hard, -n_soft, fitness) so search budget stops being spent polishing
SOFT shape fails (crinkliness/proportion/size/width/edge-too-long/
staircase-volume) while HARD structural fails (missing space, wrong/
required level, level/circulation/vertical connectivity, adjacency,
stairs, covered-outside, storey limits, public access) remain unfixed.
fitness.classify_fail_tier/tier_counts classify every fail string emitted
across fitness.py and graph.py, raising on anything unrecognised so new
fail sites must declare a tier. Validated against all real fail strings in
the checked-in corpus plus every fail-emission call site read from source.
driver.Individual gains n_hard/n_soft (populated from innerloop.Result.
fail_lines); search(use_tiers=...) swaps the comparator when set (default
off, so existing runs are unaffected — inner-loop 0.5^n cliff untouched).
evolve.py exposes --use-tiers / HOMEMAKER_USE_TIERS.
experiments/tier_ab_2g7_3.py runs the acceptance A/B (harbor+maple, 3
seeds, 20k evals) in the background; results pending.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
Fixes the stale id()-keyed geometry cache read in parallel staged runs:
substrate_readiness runs in the parent process every tournament/admit
comparison but the parent's score_with_fails (which normally clears
geometry._cache) only runs in pool workers when n_workers>1, so evicted
trees' freed addresses can alias into freshly unpickled ones. Also adds
a defensive clear at collapse_global entry per the bead's recommendation
for the same cache class of hazard.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dq3WAXft8RszMG2CLH7VkU
collapse_global's own commit could relabel a leaf back to the code its
stale share_type names, making share_type == type true again and
resurrecting a multiplicity credit for area never sized for it -- the
commit-door companion to the iio valuation bug. dom.canonicalize_shares()
drops share/share_type whenever share_type != type; called at the top of
collapse_global (covers collapse_global's own commit, 2-opt, and standalone
finish-time use) and _evaluate_full (covers collapse_superposition and
ordinary retype mutations) so the guard is an actual invariant instead of
a per-reader check.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dq3WAXft8RszMG2CLH7VkU
_collapse_value and _usage_quality temporarily overwrite leaf.type to probe a
hypothetical candidate code, but graph.leaf_share reads that overwritten type
against leaf.share_type -- so a stale share (left over from a code the leaf
was since retyped away from) spuriously reactivates whenever the probed
candidate happens to equal the old share_type, skewing the Hungarian
assignment's cell value for that (leaf, code) pair. dom.dump/dom.load drops
such stale metadata on reload (dom._emit only serialises share when
share_type==type), so a live search tree carrying it and its dump/reload
round trip fed different values into the same collapse_global call and
landed on different optimal matchings.
Fix: neutralise share_type during the probe whenever the candidate differs
from the leaf's real current type, restoring it in the finally block. The
leaf's own current type still legitimately carries a live share.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8agJBT2ZpmF3ErW7wi2wY
Follow-up to the previous commit: user flagged that quality_width/
quality_proportion's "stricter of both" (max target, min sigma) combination
for a fused leaf's two codes was an ad hoc hack. Tried two more principled
alternatives and A/B'd all three against the harbor-house/health-centre
example programmes (20k evals x 3 seeds each):
1. stricter-of-both (original) -> health-centre +24.5% worse
2. precision-weighted Gaussian product -> health-centre -13.9% better
3. mixture (max of two Gaussians) -> health-centre +20.4% worse
Landed #2 (fitness._gaussian_product): combining two Gaussian evidence
sources about the same quantity via precision-weighting gives an
intermediate target with a narrower spread, unlike the naive max/min hack.
#3's building block (_clipped_gaussian) is kept, documented, and unit-tested
as a recorded negative alternative -- somewhat counterintuitively, the more
philosophically appealing "let the leaf collapse toward whichever code fits"
mixture model was empirically worse, because max() lets a leaf score 1.0 by
satisfying only the weaker of the two codes' targets.
multi_use stays default OFF -- the precision-weighted result improves both
example programmes on average but isn't the clean sweep needed for a
default flip (harbor-house loses 1/3 seeds). DESIGN.md §33 rewritten with
the full three-way comparison.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8agJBT2ZpmF3ErW7wi2wY
Builds path (b) from §26 -- a leaf permanently serving two DIFFERENT
compatible programme codes at once, extending leaf-sharing's same-code
multiplicity mechanism to different-but-compatible codes. Architect-declared
`co_locate` pairs (validated against interchangeable()'s S1-S4 bounds, no
transitive closure so the b3v chain problem can't recur), threaded through
graph.py's checks via a new leaf_codes() resolver and fitness.py's quality
terms (additive size, stricter-of-both width/proportion). Construction-time
only, gated behind `multi_use` (default OFF, bit-identical when off).
End-to-end A/B (20k evals x 3 seeds x 2 programmes) came back net negative:
harbor-house -4.0% but health-centre +24.5% worse (3/3 seeds), because
fusing different codes' shape targets via stricter-of-both can impose a
tighter joint constraint than either code needed alone, which the tightly-
packed health-centre programme can't absorb. Written up as DESIGN.md §33;
multi_use stays default OFF, no default-flip recommended.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8agJBT2ZpmF3ErW7wi2wY
homemaker-py-9wi's 2-opt adjacency polish (collapse_global's local_search
kwarg) was validated on harbor-house alone; this closes homemaker-py-cdl
by extending the A/B sweep to programme-house (34 more files, 46 total):
0 regressions, 2 improvements, rest identical.
collapse_global's own default stays False since it also runs every
fitness eval via collapse_insearch (qpk) on the unmerged tree, where the
2-opt pass would add cost to a hot path the sweep never measured.
Instead default it on at the two one-shot finish-time call sites:
homemaker-collapse --local-search, and a new homemaker-evolve
--collapse-local-search wired through driver.collapse_best's
**collapse_kw.
The Jacobi adjacency relaxation in collapse_global (94g) re-solves a linear
assignment each round holding neighbours' labels fixed from the previous
round, which can 2-cycle between labellings that satisfy zero adjacency
requirements even when a fully-satisfying permutation exists (proved by
test_two_opt_polish_escapes_jacobi_plateau on a minimal 4-cell chain).
Fitness._two_opt_adjacency_polish runs after the Jacobi fixpoint and tries
swapping the labels of every same-level pair of supply leaves, keeping a
swap only on strict improvement -- monotone by construction. Gated behind
collapse_global(local_search=...) / homemaker-collapse --local-search,
default off pending a broader sweep (homemaker-py-cdl). Swept the 11
harbor-house evolved-*/3m/materialised .dom files: 0 regressions, 1 real
improvement (evolved-anneal-3M.dom 21->19 fails).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Runs the 94g finish-time cell↔room collapse inside every fitness eval
(collapse_insearch conf flag, default off, bit-identical when off) instead
of once at the end, so search optimises the collapsed objective directly.
Plumbed through fitness.py/driver.py/evolve.py the same way superpose/
conn_grade are; --collapse-insearch CLI flag.
A/B validated against the xi7 protocol (equal budget, both arms finished
with standard finish-time --collapse): POSITIVE, opposite of the 9o5/xi7
prior. harbor-house ON wins 3/3 (mean fails 80.3->72.0); programme-house
mixed 3/5 (mean fails 8.4->7.8). Kept default off pending a larger
programme-house sample; documented as a working opt-in for harbor-house-
scale-or-larger programmes. Full writeup in DESIGN.md §20.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The dominant post-collapse fail is the binary "level N not connected",
which is flat across fragmentation (a 7-component storey scores the same
as a 2-component one), so the outer search has no gradient toward
connected circulation. A finish-time convert-to-circulation repair was
prototyped and measured NEGATIVE (195->560 fails: bridging needed rooms
costs more missing-room fails than the one binary fail it clears).
Instead add graph.circulation_connectivity(G) = largest-circ-component
fraction, summed over storeys onto the score_with_grade proximity channel
(conf flag conn_grade; replaces the §11.4 leaf-grade there). It is a
secondary comparator key only — scalar fitness and fail count stay
byte-identical — restoring the gradient the binary fail lacks. Threaded
through driver (_overrides_for/_fitness_for/_evaluate/search; enabling it
implies the grade key) and evolve --conn-grade (default off).
A/B on full-budget runs pending; short smoke run confirms plumbing.
Tests: tests/test_conn_grade.py x9 (fraction contract, non-circ ignored,
monotone under (dis)connection, score/fail invariance); 276 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8566xAxTnwtJTkpXjYNZm
Public-access term (preserve_public_access, default on): when the building's
only street access is an l/k ROOM neighbour of a public outside leaf (no
circulation fallback — an existential building-level check the per-leaf
objective can't see), that leaf is pinned (kept, its demand slot decremented)
so the collapse can't drop "no outside public access". Best layout 15→13
becomes 15→12 with zero new fails; sweep total 172→171, still monotone.
collapse_finish(root, **kw) -> (tree, base, coll, applied): keep-better wrapper,
scores on throwaway copies (score_with_fails merges in place), returns the
collapse only if fails don't increase.
Wiring: driver.collapse_best updates result.best (lineage +collapse, canonical
re-score); evolve.py runs it after the sharing polish behind --collapse/
--no-collapse (default on). New homemaker-collapse CLI (collapse_cmd.py) applies
it to an existing .dom, writing <stem>.collapsed.dom.
tests/test_collapse_global.py: demand-set relabel, level hard constraint, c/o/s
exclusion, no-op safety, keep-better/unmerged. 267 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8566xAxTnwtJTkpXjYNZm
Add objective="quality"|"threshold" to collapse_global and make threshold
the finish-time default. The continuous-quality objective maximises
sum(usage_quality*area), which can trade one leaf just over the 0.1 fail
threshold for another just under (a fail SHUFFLE). The threshold objective
maximises the COUNT of passing size/width/proportion factors directly, with
continuous fit only as a tiebreak. A satisfied adjacency and a passing factor
share one weight (_COLLAPSE_FAIL_W) so both fail classes are minimised jointly.
Sweep over 6 harbor-house evolved layouts (total fails, base 195):
adj_off/quality 192 adj_on/quality 185 adj_off/thresh 181 adj_on/thresh 172
adj_on/threshold is monotone across all 6 (never worse than baseline), so it
is the new default. Residual on the best layout (15→13) is the building-level
"no outside public access" constraint, outside the per-leaf model.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8566xAxTnwtJTkpXjYNZm
Global relabel of inside-room leaves via one optimal assignment over the
full leaf set — the 9o5 per-class collapse generalised to N leaves ↔ M
required rooms — as a one-shot finish-time polish on a committed layout.
Assignable room_codes exclude any starting c/o/s to match the scorer's
own partition (check_space_counts skips those; cr1/st1/st2 collide with
the circulation/structure convention). Hard level constraint via a -1e12
forbid penalty. Adjacency handled as an iterated relaxation: geometry is
fixed at finish time so each leaf's graph neighbours are fixed; warm-start
from evolved labels, each pass a linear assignment over quality + an
adjacency bonus (has_adjacency vs current labels), Jacobi to a fixpoint.
Measured (level+adjacency): best evolved layout 15→14 fails, rougher ones
32→28 and 90→83; adjacency-on beats adjacency-off everywhere (off regresses
the best layout +1). Substrate only — not wired into search or a CLI yet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8566xAxTnwtJTkpXjYNZm
Interchangeable codes (similar size/width/proportion, compatible level/stack,
no adjacency edge) form equivalence classes derived from the programme. With
--superpose (default off), each fitness eval COLLAPSES every superposed leaf to
its best in-class usage via an optimal supply->demand assignment (brute force
<=C! within cap C=4, scipy Hungarian beyond), then scores the condensed types.
Because collapse re-types on the unmerged tree before all checks, counts /
adjacency / quality are unchanged downstream -- no Node field, no graph/operator
changes -- and default OFF is bit-identical.
- programme.py: derive_interchange_classes + interchangeable (S1-S4, locked
thresholds R_SIZE=1.5/R_WIDTH=1.3/R_PROP=1.5, CLASS_CAP=4)
- fitness.py: collapse_superposition, _best_assignment, _usage_quality;
superpose/superpose_class_cap conf knobs; collapse hooked into _evaluate_full
- driver.py/evolve.py: superpose flag plumbed beside leaf_sharing; --superpose
- tests/test_superposition.py: 17 tests (derivation, assignment, end-to-end)
Closes homemaker-py-9o5 (build); validation A/B is homemaker-py-xi7.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
§13.8 verdict was positive and monotone-harmless, so default the share-aware
edge-too-long cap to leaf_sharing when share_edge_cap is unset — mirrors the
pll bal+share and §13.6 interior_outside default flips. Explicit
share_edge_cap=False still reproduces the pre-flip control arm.
- fitness.Fitness.__init__: cap defaults to self._leaf_sharing when the conf
key is unset (None); explicit True/False honoured.
- run_staged_search.py: pin conf["share_edge_cap"] = share_edge in both A/B
arms so SHAREEDGE=0 stays a clean control post-flip.
- tests: control arm now pins share_edge_cap=False; new
test_edge_cap_defaults_on_under_leaf_sharing guards the flip.
- DESIGN.md §13.9: rebaseline §13.x floor (maple 80.3→74.0, harbor 34.7→31.0).
Non-sharing runs untouched: programme-house control re-score reproduces
bit-for-bit. 222 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
§13.7 flagged edge-too-long as harbor's top fail class. Dissection showed the
bulk are a leaf-sharing REPRESENTATION ARTIFACT: a share=k leaf aggregates k
same-code rooms, so its walls run ~k× the flat 8 m cap purely for being big —
the same §13.3 leak (size/missing relaxed for shared leaves) on the wall measure,
since edge_cost/outside_edge_cost ignored leaf.share.
Fix: Fitness._edge_cap(*leaves) scales the 8 m cap by the largest type-guarded
leaf_share among adjoining leaves, mirroring quality_size's k×target; non-shared
leaves keep the flat cap so genuine narrow/oversize pathologies stay flagged.
Gated behind a share_edge_cap config knob (SHAREEDGE env), default OFF so the
§13.x controls reproduce.
A/B (full Phase-8 stack, staged, 20k evals, seeds 0/1/2): control reproduces
§13.7 (maple 80.3 exact, harbor 34.7≈34.0); share-aware arm maple 80.3→74.0
(−7.9%), harbor 34.7→31.0 (−10.6%), zero regressions across 6 seeds. Positive
and monotone-harmless (only ever removes a false-positive fail). Verdict:
recommend default-ON; follow-up issue flips the default + rebaselines the floor.
Tests: 6 new unit tests for _edge_cap (221 pass).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JygRv4n2dcyDQqMiDRe7TN
Replace the area-derived share recovery with explicit, type-guarded per-leaf
multiplicity: construction stamps leaf.share=k and leaf.share_type=code; the
fitness (graph.leaf_share) honours k only while leaf.type==share_type, so any
retype/undivide auto-invalidates a stale share — no operator resets, and a
small leaf cannot retype its way into covering rooms it does not provide. Two
Node fields survive the whole search via deepcopy (genome.decode is unused in
the hot path); .dom emits `share` only on a live shared leaf.
This closes the §13.3 missing-fail leak: floor probe missing 17–44 → 0, and the
achievable floor drops −39% harbor (120.3→73.3) / −32% maple (194.7→133.0) with
no re-emergence as size fails.
Flag threaded through driver.search/search_staged → constructive_topology /
lift_base_to_storeys, exposed via LEAFSHARE/LEAFSHAREFAC in run_staged_search.py
(injects the objective into inner-loop + final-score fitness so both A/B arms
share one programme dir). run_leafshare_ab.sh runs the staged 20k A/B.
Smoke-tested end-to-end (harbor, factor 3, re-score OK). 214 tests pass;
default-OFF reproduces baseline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same-code rooms collapse into fewer, larger SHARED leaves so the ~1.8/leaf
shape tax (§13.1) is paid once per group. Multiplicity k is recovered from
area (k=clamp(round(area/target),1,max_share)) — no genome change — and used
in two default-OFF sites: graph.check_space_counts counts coverage (Σk vs
req.count) so one leaf covers several rooms without a missing fail, and
fitness.quality_size centres on k×target (σ scaled by k). Construction:
operators._share_rooms groups instances; _size_divisions_from_targets sizes
shared leaves to k×target via leaf_mult.
Floor probe (experiments/diag_leaf_sharing.py, harbor+maple, seeds 0/1/2,
+innerloop): total fails −27% harbor / −16% maple at share3, shape factors
fall ~linearly with leaf count (confirms §13.1). Cap: 17–44 missing fails
leak because depth maldistribution (§13.2) keeps shared leaves below k×target
so round() undercounts; inner loop can't close it. Net still positive.
Default-OFF reproduces baseline exactly (214 tests pass). Driver plumbing +
staged 20k A/B remain; §13.3 records the next design fork.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement a graded proximity comparator key (-n_fails, grade, fitness) behind
a default-off use_grade flag: fitness._leaf_grade / score_with_grade sum
f/FAIL_THRESHOLD over failing per-leaf quality factors; scalar fitness and fail
count stay untouched so the inner-loop 0.5^n cliff (§5.4) is unaffected (0/9
regression check: PASS). Read once per child in driver._evaluate off the
already-optimised tree; threaded through search_staged (Stage 2 only).
Harbor staged A/B (20000 evals, seeds 0/1/2): lex 95/96/106 (mean 99.0) vs
lex+grade 99/98/102 (mean 99.7) — grade wins 1/3, no plateau escape. Premise
falsified: within a fixed fail-tier 0.5^n is constant so fitness still spans
~6 orders of magnitude; grade above fitness displaces that working signal.
Verdict: reject; lexicographic (-n_fails, fitness) stands. Flag kept default-off
for reproducibility / possible reuse as a §11.5 diversity signal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>