Add unit tests for geometry and fitness modules
26 tests for geometry (area, angles, aspect, boundary ids, centroid,
offset, etc.) and 35 tests for fitness (gaussian, config lookup,
quality terms, value rates, costs, stair helpers). Suite: 175 passed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 00:06:02 +01:00
|
|
|
|
"""Unit tests for fitness.py quality terms and helpers (oracle-free)."""
|
|
|
|
|
|
|
Make both failing tests assert their intent, not stale artefacts
The suite is green for the first time this session: 376 passed, 0 failed.
test_collapse_insearch_reproduces_94g_finish_time_result hard-coded both
endpoints of the 17 result -- 15 fails before collapse, 12 after. Those
were measured before 39.4, when harbor's effective programme was silently
32 instances because codes like cr1 were read as generic circulation; the
same layout now scores 82. But the guarantee the test exists to protect,
per its own docstring, is that in-search collapse reaches the SAME layout
as finish-time collapse on fixed geometry -- and two independent constants
never checked that. They can both drift and stay equal, or both hold and
mask an inequality.
Rewritten to compute both sides live and assert they agree, plus that
collapse does not make the layout worse. Measured: 82 -> 58 in-search, and
finish-time collapse independently reaches 58 at iters=3 and iters=6. The
invariant holds; only the constants were stale. Restating the reference
figure itself remains homemaker-py-ut5.
test_classify_fail_tier_covers_full_corpus globbed examples/**/*.fails and
asserted checked > 0. Git tracks ZERO .fails -- they are artefacts the
scorer writes beside a .dom -- so its docstring described files that by
design never exist in the repo, and it passed only on a machine that had
already run the scorer. Split into: a test that GENERATES fails by scoring
three corpus layouts picked for breadth (requiring >= 8 distinct kinds so
it cannot silently narrow); a test that an unclassifiable string actually
raises; and an opportunistic .fails sweep that never requires them.
Verified by moving every .fails out of the tree and re-running.
Closes homemaker-py-1ue.
Lint at parity (46).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-29 07:11:25 +00:00
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
Add unit tests for geometry and fitness modules
26 tests for geometry (area, angles, aspect, boundary ids, centroid,
offset, etc.) and 35 tests for fitness (gaussian, config lookup,
quality terms, value rates, costs, stair helpers). Suite: 175 passed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 00:06:02 +01:00
|
|
|
|
import pytest
|
|
|
|
|
|
|
§39.7: access requirements become a declared `usage:` attribute (homemaker-py-sel)
Closes the second namespace sharing a first character with programme codes: the
usage prefixes b/t/l/k, under which a room silently inherited another room's
connectivity rules from its spelling.
usage is a plain, MANDATORY attribute of the space definition -- not a lookup
table. An interim design proposed a top-level usage_classes: table binding
author-coined names to behaviour; withdrawn, because an indirect name->behaviour
mapping living apart from the thing it describes is exactly the shape of the
prefix rule §39 exists to remove, it would be the only such table in a schema
where every other space property is a plain attribute, and the need it served
was already met -- "building specific" is about what a room is CALLED, and
name: is already free text.
Rule that settles it: a usage value exists iff the engine treats it differently
somewhere. Config selects among behaviours; it cannot invent them.
- programme.USAGES (living/kitchen/bedroom/toilet/utility/none) plus the
behaviour groupings PRIVATE_USAGES / PRIVATE_STRIPS / TOILET_STRIPS /
SOCIABLE_USAGES. Missing or unknown usage is a load error naming the code,
from BOTH parse paths.
- Code-level, never leaf-level: usage_of(leaf.type) is looked up fresh, so a
retype changes the class automatically. 51 sites assign leaf.type, and
share/share_type plus the r5a resurrection are the precedent for why
leaf-level attributes rot.
- graph.has_circulation takes the usage map and trims on declared class;
fitness.access and the public-access check likewise. fitness._t0 is DELETED --
no first-character type test remains anywhere in the codebase.
- utility is distinct from bedroom (same access requirements today) because it
is a different use and gives derive_interchange_classes an axis to relax on.
- A toilet now keeps its edge to a terminal room -- the Brand adjacency, which
the old b-before-t loop ordering severed.
- All 107 corpus entries migrated by experiments/migrate_usage_key.py, comments
and layout preserved.
MEASURED -- the connectivity model was ~4x too permissive. `none` is not
neutral: nothing is trimmed, so the graph may route THROUGH the room, and 34 of
52 codes had no class (Dental Surgery, Records Room, Utilities Closet all served
as corridors). Edges trimmed, prefix-inferred vs declared, 3 seeds each:
harbor-house 18 (9%) -> 79 (39%) inaccessible fails 0 -> 4
health-centre 12 (8%) -> 59 (40%) inaccessible fails 2 -> 3
maple-court 53 (17%) -> 123 (39%) inaccessible fails 1 -> 5
Re-baseline (seed 1, 20k, harbor): 58 fails (15h/43s) -> 61 (16h/45s), now
reporting 1-inaccessible-usable-space x2 plus level 0 and level 1 not connected.
The count rose because the objective got honest -- those failures were always
true of the layout and the old model could not see them. Every harbor number
before this was measured against a graph crediting routes through store
cupboards.
Sharpens §38.2: the objective pays x60-85 to delete circulation, and until now
the deleted corridors were not missed because storage stood in for them. With
that substitution gone, homemaker-py-2v1 is the remaining half -- and now
measurable, because the fails it should prevent actually fire.
350 passed (+5 new), same 7 pre-existing fixture failures, lint unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 13:39:41 +00:00
|
|
|
|
from _helpers import with_usage
|
2026-06-14 08:18:06 +01:00
|
|
|
|
from homemaker_layout import dom, geometry
|
|
|
|
|
|
from homemaker_layout.dom import Node
|
2026-06-18 22:33:29 +01:00
|
|
|
|
from homemaker_layout.fitness import (
|
|
|
|
|
|
CONF_DEFAULTS,
|
|
|
|
|
|
COST_DEFAULTS,
|
|
|
|
|
|
FAIL_THRESHOLD,
|
|
|
|
|
|
Fitness,
|
|
|
|
|
|
_leaf_grade,
|
homemaker-py-2g7.3: hard/soft fail tiering behind --use-tiers flag
Splits the flat outer-search comparator (-n_fails, fitness) into a tiered
(-n_hard, -n_soft, fitness) so search budget stops being spent polishing
SOFT shape fails (crinkliness/proportion/size/width/edge-too-long/
staircase-volume) while HARD structural fails (missing space, wrong/
required level, level/circulation/vertical connectivity, adjacency,
stairs, covered-outside, storey limits, public access) remain unfixed.
fitness.classify_fail_tier/tier_counts classify every fail string emitted
across fitness.py and graph.py, raising on anything unrecognised so new
fail sites must declare a tier. Validated against all real fail strings in
the checked-in corpus plus every fail-emission call site read from source.
driver.Individual gains n_hard/n_soft (populated from innerloop.Result.
fail_lines); search(use_tiers=...) swaps the comparator when set (default
off, so existing runs are unaffected — inner-loop 0.5^n cliff untouched).
evolve.py exposes --use-tiers / HOMEMAKER_USE_TIERS.
experiments/tier_ab_2g7_3.py runs the acceptance A/B (harbor+maple, 3
seeds, 20k evals) in the background; results pending.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-02 16:00:39 +01:00
|
|
|
|
classify_fail_tier,
|
Make both failing tests assert their intent, not stale artefacts
The suite is green for the first time this session: 376 passed, 0 failed.
test_collapse_insearch_reproduces_94g_finish_time_result hard-coded both
endpoints of the 17 result -- 15 fails before collapse, 12 after. Those
were measured before 39.4, when harbor's effective programme was silently
32 instances because codes like cr1 were read as generic circulation; the
same layout now scores 82. But the guarantee the test exists to protect,
per its own docstring, is that in-search collapse reaches the SAME layout
as finish-time collapse on fixed geometry -- and two independent constants
never checked that. They can both drift and stay equal, or both hold and
mask an inequality.
Rewritten to compute both sides live and assert they agree, plus that
collapse does not make the layout worse. Measured: 82 -> 58 in-search, and
finish-time collapse independently reaches 58 at iters=3 and iters=6. The
invariant holds; only the constants were stale. Restating the reference
figure itself remains homemaker-py-ut5.
test_classify_fail_tier_covers_full_corpus globbed examples/**/*.fails and
asserted checked > 0. Git tracks ZERO .fails -- they are artefacts the
scorer writes beside a .dom -- so its docstring described files that by
design never exist in the repo, and it passed only on a machine that had
already run the scorer. Split into: a test that GENERATES fails by scoring
three corpus layouts picked for breadth (requiring >= 8 distinct kinds so
it cannot silently narrow); a test that an unclassifiable string actually
raises; and an opportunistic .fails sweep that never requires them.
Verified by moving every .fails out of the tree and re-running.
Closes homemaker-py-1ue.
Lint at parity (46).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-29 07:11:25 +00:00
|
|
|
|
load_config,
|
2026-06-18 22:33:29 +01:00
|
|
|
|
gaussian,
|
homemaker-py-2g7.3: hard/soft fail tiering behind --use-tiers flag
Splits the flat outer-search comparator (-n_fails, fitness) into a tiered
(-n_hard, -n_soft, fitness) so search budget stops being spent polishing
SOFT shape fails (crinkliness/proportion/size/width/edge-too-long/
staircase-volume) while HARD structural fails (missing space, wrong/
required level, level/circulation/vertical connectivity, adjacency,
stairs, covered-outside, storey limits, public access) remain unfixed.
fitness.classify_fail_tier/tier_counts classify every fail string emitted
across fitness.py and graph.py, raising on anything unrecognised so new
fail sites must declare a tier. Validated against all real fail strings in
the checked-in corpus plus every fail-emission call site read from source.
driver.Individual gains n_hard/n_soft (populated from innerloop.Result.
fail_lines); search(use_tiers=...) swaps the comparator when set (default
off, so existing runs are unaffected — inner-loop 0.5^n cliff untouched).
evolve.py exposes --use-tiers / HOMEMAKER_USE_TIERS.
experiments/tier_ab_2g7_3.py runs the acceptance A/B (harbor+maple, 3
seeds, 20k evals) in the background; results pending.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-02 16:00:39 +01:00
|
|
|
|
tier_counts,
|
2026-06-18 22:33:29 +01:00
|
|
|
|
)
|
Add unit tests for geometry and fitness modules
26 tests for geometry (area, angles, aspect, boundary ids, centroid,
offset, etc.) and 35 tests for fitness (gaussian, config lookup,
quality terms, value rates, costs, stair helpers). Suite: 175 passed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 00:06:02 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _leaf(type_: str, size: float = 4.0) -> Node:
|
|
|
|
|
|
"""Undivided level-root leaf with a square plot of side `size`."""
|
|
|
|
|
|
geometry.clear_cache()
|
|
|
|
|
|
return Node(
|
|
|
|
|
|
node=[[0.0, 0.0], [size, 0.0], [size, size], [0.0, size]],
|
|
|
|
|
|
type=type_,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
# gaussian
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gaussian_peak_returns_a():
|
|
|
|
|
|
assert gaussian(5.0, 1.0, 5.0, 1.0) == pytest.approx(1.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gaussian_peak_scales_by_a():
|
|
|
|
|
|
assert gaussian(3.0, 2.5, 3.0, 1.0) == pytest.approx(2.5)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gaussian_one_sigma_uses_truncated_e():
|
|
|
|
|
|
# Urb uses e=2.718281828, not math.e; at one sigma the factor is e^-0.5
|
|
|
|
|
|
e = 2.718281828
|
|
|
|
|
|
expected = e ** -0.5
|
|
|
|
|
|
assert gaussian(6.0, 1.0, 5.0, 1.0) == pytest.approx(expected, rel=1e-9)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gaussian_symmetry():
|
|
|
|
|
|
assert gaussian(4.0, 1.0, 5.0, 1.0) == pytest.approx(gaussian(6.0, 1.0, 5.0, 1.0))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
# Fitness.conf / cost
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_conf_falls_back_to_defaults():
|
|
|
|
|
|
assert Fitness().conf("value_inside") == CONF_DEFAULTS["value_inside"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_conf_override_wins():
|
|
|
|
|
|
assert Fitness(conf={"value_inside": 999.0}).conf("value_inside") == 999.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_conf_unknown_key_returns_none():
|
|
|
|
|
|
assert Fitness().conf("no_such_key") is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cost_falls_back_to_defaults():
|
|
|
|
|
|
assert Fitness().cost("inside") == COST_DEFAULTS["inside"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cost_override_wins():
|
|
|
|
|
|
assert Fitness(cost={"inside": 42.0}).cost("inside") == 42.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cost_unknown_key_returns_zero():
|
|
|
|
|
|
assert Fitness().cost("no_such_key") == 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
# get_space_params lookup chain
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_space_params_circulation_size():
|
|
|
|
|
|
assert Fitness().get_space_params("C", "size") == CONF_DEFAULTS["size_circulation"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_space_params_outside_width():
|
|
|
|
|
|
assert Fitness().get_space_params("O", "width") == CONF_DEFAULTS["width_outside"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_space_params_sahn_proportion():
|
|
|
|
|
|
assert Fitness().get_space_params("S", "proportion") == CONF_DEFAULTS["proportion_outside"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_space_params_inside_falls_back_to_inside_defaults():
|
|
|
|
|
|
assert Fitness().get_space_params("k1", "proportion") == CONF_DEFAULTS["proportion_inside"]
|
|
|
|
|
|
assert Fitness().get_space_params("k1", "size") == CONF_DEFAULTS["size_inside"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_space_params_named_space_overrides_default():
|
§39.7: access requirements become a declared `usage:` attribute (homemaker-py-sel)
Closes the second namespace sharing a first character with programme codes: the
usage prefixes b/t/l/k, under which a room silently inherited another room's
connectivity rules from its spelling.
usage is a plain, MANDATORY attribute of the space definition -- not a lookup
table. An interim design proposed a top-level usage_classes: table binding
author-coined names to behaviour; withdrawn, because an indirect name->behaviour
mapping living apart from the thing it describes is exactly the shape of the
prefix rule §39 exists to remove, it would be the only such table in a schema
where every other space property is a plain attribute, and the need it served
was already met -- "building specific" is about what a room is CALLED, and
name: is already free text.
Rule that settles it: a usage value exists iff the engine treats it differently
somewhere. Config selects among behaviours; it cannot invent them.
- programme.USAGES (living/kitchen/bedroom/toilet/utility/none) plus the
behaviour groupings PRIVATE_USAGES / PRIVATE_STRIPS / TOILET_STRIPS /
SOCIABLE_USAGES. Missing or unknown usage is a load error naming the code,
from BOTH parse paths.
- Code-level, never leaf-level: usage_of(leaf.type) is looked up fresh, so a
retype changes the class automatically. 51 sites assign leaf.type, and
share/share_type plus the r5a resurrection are the precedent for why
leaf-level attributes rot.
- graph.has_circulation takes the usage map and trims on declared class;
fitness.access and the public-access check likewise. fitness._t0 is DELETED --
no first-character type test remains anywhere in the codebase.
- utility is distinct from bedroom (same access requirements today) because it
is a different use and gives derive_interchange_classes an axis to relax on.
- A toilet now keeps its edge to a terminal room -- the Brand adjacency, which
the old b-before-t loop ordering severed.
- All 107 corpus entries migrated by experiments/migrate_usage_key.py, comments
and layout preserved.
MEASURED -- the connectivity model was ~4x too permissive. `none` is not
neutral: nothing is trimmed, so the graph may route THROUGH the room, and 34 of
52 codes had no class (Dental Surgery, Records Room, Utilities Closet all served
as corridors). Edges trimmed, prefix-inferred vs declared, 3 seeds each:
harbor-house 18 (9%) -> 79 (39%) inaccessible fails 0 -> 4
health-centre 12 (8%) -> 59 (40%) inaccessible fails 2 -> 3
maple-court 53 (17%) -> 123 (39%) inaccessible fails 1 -> 5
Re-baseline (seed 1, 20k, harbor): 58 fails (15h/43s) -> 61 (16h/45s), now
reporting 1-inaccessible-usable-space x2 plus level 0 and level 1 not connected.
The count rose because the objective got honest -- those failures were always
true of the layout and the old model could not see them. Every harbor number
before this was measured against a graph crediting routes through store
cupboards.
Sharpens §38.2: the objective pays x60-85 to delete circulation, and until now
the deleted corridors were not missed because storage stood in for them. With
that substitution gone, homemaker-py-2v1 is the remaining half -- and now
measurable, because the fails it should prevent actually fire.
350 passed (+5 new), same 7 pre-existing fixture failures, lint unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 13:39:41 +00:00
|
|
|
|
f = Fitness(conf={"spaces": with_usage({"k1": {"size": [20.0, 4.0]}})})
|
Add unit tests for geometry and fitness modules
26 tests for geometry (area, angles, aspect, boundary ids, centroid,
offset, etc.) and 35 tests for fitness (gaussian, config lookup,
quality terms, value rates, costs, stair helpers). Suite: 175 passed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 00:06:02 +01:00
|
|
|
|
assert f.get_space_params("k1", "size") == [20.0, 4.0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
# quality_proportion
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_quality_proportion_square_inside_returns_one():
|
|
|
|
|
|
# aspect=1.0 < proportion_inside[0]=1.5 → 1.0
|
|
|
|
|
|
assert Fitness().quality_proportion(_leaf("k1")) == pytest.approx(1.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_quality_proportion_square_outside_returns_one():
|
|
|
|
|
|
# aspect=1.0 < proportion_outside[0]=1.5 → 1.0
|
|
|
|
|
|
assert Fitness().quality_proportion(_leaf("O")) == pytest.approx(1.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_quality_proportion_square_circulation_returns_one():
|
|
|
|
|
|
assert Fitness().quality_proportion(_leaf("C")) == pytest.approx(1.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
# quality_size
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_quality_size_outside_always_one():
|
|
|
|
|
|
assert Fitness().quality_size(_leaf("O")) == 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_quality_size_sahn_always_one():
|
|
|
|
|
|
assert Fitness().quality_size(_leaf("S")) == 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_quality_size_inside_at_peak():
|
|
|
|
|
|
# size_inside=[16.0,3.5]; leaf is 4×4=16 m² → gaussian at peak → 1.0
|
|
|
|
|
|
leaf = _leaf("k1", size=4.0)
|
|
|
|
|
|
assert geometry.area(leaf) == pytest.approx(16.0)
|
|
|
|
|
|
assert Fitness().quality_size(leaf) == pytest.approx(1.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_quality_size_circulation_at_peak():
|
|
|
|
|
|
# size_circulation=[0.0,14.0]; peak at 0, gaussian(area,1,0,14) → always <1 for area>0
|
|
|
|
|
|
# Just verify it returns a value in [0,1]
|
|
|
|
|
|
f = Fitness().quality_size(_leaf("C", size=4.0))
|
|
|
|
|
|
assert 0.0 < f <= 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
# quality_width
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_quality_width_wide_inside_returns_one():
|
|
|
|
|
|
# width_inside=[4.0,1.0]; 10m side > 4.0 → 1.0
|
|
|
|
|
|
assert Fitness().quality_width(_leaf("k1", size=10.0)) == pytest.approx(1.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_quality_width_wide_circulation_returns_one():
|
|
|
|
|
|
# width_circulation=[2.4,0.2]; 10m > 2.4 → 1.0
|
|
|
|
|
|
assert Fitness().quality_width(_leaf("C", size=10.0)) == pytest.approx(1.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_quality_width_wide_outside_ground_uses_gaussian():
|
|
|
|
|
|
# outside at level 0 falls through to gaussian; 10m > width_outside[0]=3.0 → 1.0
|
|
|
|
|
|
assert Fitness().quality_width(_leaf("O", size=10.0)) == pytest.approx(1.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
# quality_perpendicular
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_quality_perpendicular_rectangle_near_one():
|
|
|
|
|
|
# All four corners of the square are pi/2; perpendicular formula gives ≈1
|
|
|
|
|
|
leaf = _leaf("k1", size=4.0)
|
|
|
|
|
|
result = Fitness().quality_perpendicular(leaf)
|
|
|
|
|
|
assert result == pytest.approx(1.0, abs=1e-6)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
# value_rate
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_value_rate_outside_ground():
|
|
|
|
|
|
leaf = _leaf("O")
|
|
|
|
|
|
assert dom.level_of(leaf) == 0
|
|
|
|
|
|
assert Fitness().value_rate(leaf) == pytest.approx(CONF_DEFAULTS["value_outside"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_value_rate_circulation():
|
|
|
|
|
|
assert Fitness().value_rate(_leaf("C")) == pytest.approx(CONF_DEFAULTS["value_circulation"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_value_rate_inside():
|
|
|
|
|
|
assert Fitness().value_rate(_leaf("k1")) == pytest.approx(CONF_DEFAULTS["value_inside"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
# leaf_cost
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_leaf_cost_outside_bare():
|
|
|
|
|
|
# not covered, not supported → outside rate × area
|
|
|
|
|
|
leaf = _leaf("O", size=4.0) # area = 16.0
|
|
|
|
|
|
assert Fitness().leaf_cost(leaf) == pytest.approx(COST_DEFAULTS["outside"] * 16.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_leaf_cost_inside():
|
|
|
|
|
|
leaf = _leaf("k1", size=4.0)
|
|
|
|
|
|
assert Fitness().leaf_cost(leaf) == pytest.approx(COST_DEFAULTS["inside"] * 16.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
hph/§13.8: share-aware edge-too-long cap — shared leaves no longer penalised for aggregate wall length
§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
2026-06-28 21:24:51 +01:00
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
# Share-aware edge-too-long cap (hph §13.7)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _shared_leaf(type_: str = "k1", k: int = 3) -> Node:
|
|
|
|
|
|
leaf = _leaf(type_)
|
|
|
|
|
|
leaf.share = k
|
|
|
|
|
|
leaf.share_type = type_
|
|
|
|
|
|
return leaf
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_edge_cap_flat_by_default():
|
|
|
|
|
|
# no leaf_sharing → flat 8 m regardless of any share stamp
|
|
|
|
|
|
fit = Fitness()
|
|
|
|
|
|
assert fit._edge_cap(_shared_leaf(k=3)) == pytest.approx(8.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_edge_cap_flat_when_lever_off_even_with_sharing():
|
2026-06-28 21:38:53 +01:00
|
|
|
|
# leaf_sharing on but the hph lever explicitly off → still flat (control arm).
|
|
|
|
|
|
# Post-§13.8 the lever defaults ON under sharing, so the control must pin it.
|
|
|
|
|
|
fit = Fitness(conf={"leaf_sharing": True, "share_edge_cap": False})
|
hph/§13.8: share-aware edge-too-long cap — shared leaves no longer penalised for aggregate wall length
§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
2026-06-28 21:24:51 +01:00
|
|
|
|
assert fit._edge_cap(_shared_leaf(k=3)) == pytest.approx(8.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_edge_cap_scales_by_share_when_lever_on():
|
|
|
|
|
|
fit = Fitness(conf={"leaf_sharing": True, "share_edge_cap": True})
|
|
|
|
|
|
assert fit._edge_cap(_shared_leaf(k=3)) == pytest.approx(24.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-28 21:38:53 +01:00
|
|
|
|
def test_edge_cap_defaults_on_under_leaf_sharing():
|
|
|
|
|
|
# §13.8 default flip: leaf_sharing on, lever unset → cap scales by share
|
|
|
|
|
|
fit = Fitness(conf={"leaf_sharing": True})
|
|
|
|
|
|
assert fit._edge_cap(_shared_leaf(k=3)) == pytest.approx(24.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
hph/§13.8: share-aware edge-too-long cap — shared leaves no longer penalised for aggregate wall length
§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
2026-06-28 21:24:51 +01:00
|
|
|
|
def test_edge_cap_unshared_leaf_keeps_flat_cap():
|
|
|
|
|
|
# a non-shared leaf (the narrow-sliver pathology) is never relaxed
|
|
|
|
|
|
fit = Fitness(conf={"leaf_sharing": True, "share_edge_cap": True})
|
|
|
|
|
|
assert fit._edge_cap(_leaf("k1")) == pytest.approx(8.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_edge_cap_stale_share_type_ignored():
|
|
|
|
|
|
# retyped leaf whose stamp no longer matches type → share invalid → flat
|
|
|
|
|
|
fit = Fitness(conf={"leaf_sharing": True, "share_edge_cap": True})
|
|
|
|
|
|
leaf = _shared_leaf("k1", k=3)
|
|
|
|
|
|
leaf.type = "b1" # retyped; share_type still "k1"
|
|
|
|
|
|
assert fit._edge_cap(leaf) == pytest.approx(8.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_edge_cap_uses_largest_share_among_adjoining_leaves():
|
|
|
|
|
|
# an interior wall takes the max share of the two leaves it separates
|
|
|
|
|
|
fit = Fitness(conf={"leaf_sharing": True, "share_edge_cap": True})
|
|
|
|
|
|
cap = fit._edge_cap(_leaf("k1"), _shared_leaf("b1", k=2))
|
|
|
|
|
|
assert cap == pytest.approx(16.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
Add unit tests for geometry and fitness modules
26 tests for geometry (area, angles, aspect, boundary ids, centroid,
offset, etc.) and 35 tests for fitness (gaussian, config lookup,
quality terms, value rates, costs, stair helpers). Suite: 175 passed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 00:06:02 +01:00
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
# Stair helpers
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_risers_number_exact_division():
|
|
|
|
|
|
# 2.0 / 0.25 = 8.0 exactly → returns 8
|
|
|
|
|
|
assert Fitness._risers_number(2.0, 0.25) == 8
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_risers_number_rounds_up():
|
|
|
|
|
|
# 3.0 / 0.19 ≈ 15.789 → rounds up to 16
|
|
|
|
|
|
assert Fitness._risers_number(3.0, 0.19) == 16
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_ideal_going_clamps_to_minimum():
|
|
|
|
|
|
# riser=0.25 → going=0.125 < 0.22 → clamp
|
|
|
|
|
|
assert Fitness._ideal_going(0.25) == 0.22
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_ideal_going_above_minimum():
|
|
|
|
|
|
# riser=0.15 → going=0.325 > 0.22; result should be in valid range
|
|
|
|
|
|
result = Fitness._ideal_going(0.15)
|
|
|
|
|
|
assert result >= 0.22
|
|
|
|
|
|
assert result <= 0.625
|
2026-06-18 22:33:29 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
# Graded high-fail objective (§11.4)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_leaf_grade_no_failing_factors_is_zero():
|
|
|
|
|
|
# All factors above FAIL_THRESHOLD → no proximity credit.
|
|
|
|
|
|
assert _leaf_grade({"size": 0.9, "width": 1.0, "access": 1.0}) == 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_leaf_grade_credits_only_failing_factors():
|
|
|
|
|
|
# Only size fails (0.05 < 0.1); credit = 0.05 / 0.1 = 0.5.
|
|
|
|
|
|
g = _leaf_grade({"size": 0.05, "width": 0.5, "proportion": 1.0})
|
|
|
|
|
|
assert g == pytest.approx(0.05 / FAIL_THRESHOLD)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_leaf_grade_monotone_in_proximity():
|
|
|
|
|
|
# A failing factor closer to the threshold scores higher (better).
|
|
|
|
|
|
deep = _leaf_grade({"size": 0.01})
|
|
|
|
|
|
shallow = _leaf_grade({"size": 0.09})
|
|
|
|
|
|
assert shallow > deep
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_leaf_grade_sums_over_failing_factors():
|
|
|
|
|
|
g = _leaf_grade({"size": 0.04, "width": 0.06, "access": 1.0})
|
|
|
|
|
|
assert g == pytest.approx((0.04 + 0.06) / FAIL_THRESHOLD)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_leaf_grade_ignores_non_graded_keys():
|
Remove two dead paths from the objective
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
2026-09-06 18:11:09 +00:00
|
|
|
|
# Only _GRADED_FACTORS contribute; anything else is ignored however low.
|
|
|
|
|
|
# (This used to name "daylight", a factor pinned to 1.0 since the
|
|
|
|
|
|
# URB_NO_OCCLUSION descope and removed entirely in §39.26.)
|
|
|
|
|
|
assert _leaf_grade({"not_a_factor": 0.0}) == 0.0
|
2026-06-28 22:04:35 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
# load_config overrides (homemaker-py-x3b)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_load_config_overrides_merge_last(tmp_path):
|
|
|
|
|
|
# The CLI/driver injects run-level knobs (leaf_sharing) without editing any
|
|
|
|
|
|
# on-disk patterns.config, so §13.3 example programmes stay reproducible.
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
|
|
from homemaker_layout.fitness import load_config
|
|
|
|
|
|
|
|
|
|
|
|
(tmp_path / "patterns.config").write_text(
|
§39.7: access requirements become a declared `usage:` attribute (homemaker-py-sel)
Closes the second namespace sharing a first character with programme codes: the
usage prefixes b/t/l/k, under which a room silently inherited another room's
connectivity rules from its spelling.
usage is a plain, MANDATORY attribute of the space definition -- not a lookup
table. An interim design proposed a top-level usage_classes: table binding
author-coined names to behaviour; withdrawn, because an indirect name->behaviour
mapping living apart from the thing it describes is exactly the shape of the
prefix rule §39 exists to remove, it would be the only such table in a schema
where every other space property is a plain attribute, and the need it served
was already met -- "building specific" is about what a room is CALLED, and
name: is already free text.
Rule that settles it: a usage value exists iff the engine treats it differently
somewhere. Config selects among behaviours; it cannot invent them.
- programme.USAGES (living/kitchen/bedroom/toilet/utility/none) plus the
behaviour groupings PRIVATE_USAGES / PRIVATE_STRIPS / TOILET_STRIPS /
SOCIABLE_USAGES. Missing or unknown usage is a load error naming the code,
from BOTH parse paths.
- Code-level, never leaf-level: usage_of(leaf.type) is looked up fresh, so a
retype changes the class automatically. 51 sites assign leaf.type, and
share/share_type plus the r5a resurrection are the precedent for why
leaf-level attributes rot.
- graph.has_circulation takes the usage map and trims on declared class;
fitness.access and the public-access check likewise. fitness._t0 is DELETED --
no first-character type test remains anywhere in the codebase.
- utility is distinct from bedroom (same access requirements today) because it
is a different use and gives derive_interchange_classes an axis to relax on.
- A toilet now keeps its edge to a terminal room -- the Brand adjacency, which
the old b-before-t loop ordering severed.
- All 107 corpus entries migrated by experiments/migrate_usage_key.py, comments
and layout preserved.
MEASURED -- the connectivity model was ~4x too permissive. `none` is not
neutral: nothing is trimmed, so the graph may route THROUGH the room, and 34 of
52 codes had no class (Dental Surgery, Records Room, Utilities Closet all served
as corridors). Edges trimmed, prefix-inferred vs declared, 3 seeds each:
harbor-house 18 (9%) -> 79 (39%) inaccessible fails 0 -> 4
health-centre 12 (8%) -> 59 (40%) inaccessible fails 2 -> 3
maple-court 53 (17%) -> 123 (39%) inaccessible fails 1 -> 5
Re-baseline (seed 1, 20k, harbor): 58 fails (15h/43s) -> 61 (16h/45s), now
reporting 1-inaccessible-usable-space x2 plus level 0 and level 1 not connected.
The count rose because the objective got honest -- those failures were always
true of the layout and the old model could not see them. Every harbor number
before this was measured against a graph crediting routes through store
cupboards.
Sharpens §38.2: the objective pays x60-85 to delete circulation, and until now
the deleted corridors were not missed because storage stood in for them. With
that substitution gone, homemaker-py-2v1 is the remaining half -- and now
measurable, because the fails it should prevent actually fire.
350 passed (+5 new), same 7 pre-existing fixture failures, lint unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 13:39:41 +00:00
|
|
|
|
yaml.safe_dump({"spaces": with_usage({"b": {"size": [12.0, 1.0]}})}))
|
2026-06-28 22:04:35 +01:00
|
|
|
|
|
|
|
|
|
|
conf, _ = load_config(tmp_path)
|
|
|
|
|
|
assert "leaf_sharing" not in conf # absent on disk
|
|
|
|
|
|
|
|
|
|
|
|
conf2, _ = load_config(tmp_path, overrides={"leaf_sharing": True})
|
|
|
|
|
|
assert conf2["leaf_sharing"] is True
|
§39.7: access requirements become a declared `usage:` attribute (homemaker-py-sel)
Closes the second namespace sharing a first character with programme codes: the
usage prefixes b/t/l/k, under which a room silently inherited another room's
connectivity rules from its spelling.
usage is a plain, MANDATORY attribute of the space definition -- not a lookup
table. An interim design proposed a top-level usage_classes: table binding
author-coined names to behaviour; withdrawn, because an indirect name->behaviour
mapping living apart from the thing it describes is exactly the shape of the
prefix rule §39 exists to remove, it would be the only such table in a schema
where every other space property is a plain attribute, and the need it served
was already met -- "building specific" is about what a room is CALLED, and
name: is already free text.
Rule that settles it: a usage value exists iff the engine treats it differently
somewhere. Config selects among behaviours; it cannot invent them.
- programme.USAGES (living/kitchen/bedroom/toilet/utility/none) plus the
behaviour groupings PRIVATE_USAGES / PRIVATE_STRIPS / TOILET_STRIPS /
SOCIABLE_USAGES. Missing or unknown usage is a load error naming the code,
from BOTH parse paths.
- Code-level, never leaf-level: usage_of(leaf.type) is looked up fresh, so a
retype changes the class automatically. 51 sites assign leaf.type, and
share/share_type plus the r5a resurrection are the precedent for why
leaf-level attributes rot.
- graph.has_circulation takes the usage map and trims on declared class;
fitness.access and the public-access check likewise. fitness._t0 is DELETED --
no first-character type test remains anywhere in the codebase.
- utility is distinct from bedroom (same access requirements today) because it
is a different use and gives derive_interchange_classes an axis to relax on.
- A toilet now keeps its edge to a terminal room -- the Brand adjacency, which
the old b-before-t loop ordering severed.
- All 107 corpus entries migrated by experiments/migrate_usage_key.py, comments
and layout preserved.
MEASURED -- the connectivity model was ~4x too permissive. `none` is not
neutral: nothing is trimmed, so the graph may route THROUGH the room, and 34 of
52 codes had no class (Dental Surgery, Records Room, Utilities Closet all served
as corridors). Edges trimmed, prefix-inferred vs declared, 3 seeds each:
harbor-house 18 (9%) -> 79 (39%) inaccessible fails 0 -> 4
health-centre 12 (8%) -> 59 (40%) inaccessible fails 2 -> 3
maple-court 53 (17%) -> 123 (39%) inaccessible fails 1 -> 5
Re-baseline (seed 1, 20k, harbor): 58 fails (15h/43s) -> 61 (16h/45s), now
reporting 1-inaccessible-usable-space x2 plus level 0 and level 1 not connected.
The count rose because the objective got honest -- those failures were always
true of the layout and the old model could not see them. Every harbor number
before this was measured against a graph crediting routes through store
cupboards.
Sharpens §38.2: the objective pays x60-85 to delete circulation, and until now
the deleted corridors were not missed because storage stood in for them. With
that substitution gone, homemaker-py-2v1 is the remaining half -- and now
measurable, because the fails it should prevent actually fire.
350 passed (+5 new), same 7 pre-existing fixture failures, lint unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 13:39:41 +00:00
|
|
|
|
assert conf2["spaces"]["b"] == with_usage({"b": {"size": [12.0, 1.0]}})["b"]
|
2026-06-28 22:04:35 +01:00
|
|
|
|
|
|
|
|
|
|
# None / empty overrides are a no-op (default-OFF parity).
|
|
|
|
|
|
assert "leaf_sharing" not in load_config(tmp_path, overrides=None)[0]
|
|
|
|
|
|
assert "leaf_sharing" not in load_config(tmp_path, overrides={})[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_programme_parses_per_code_share(tmp_path):
|
|
|
|
|
|
# homemaker-py-x3b: SpaceReq carries the optional per-code 'share' grain and a
|
|
|
|
|
|
# has_share flag distinguishing an explicit share:1 (opt out) from the default.
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
|
|
from homemaker_layout.programme import load_programme
|
|
|
|
|
|
|
|
|
|
|
|
p = tmp_path / "patterns.config"
|
§39.7: access requirements become a declared `usage:` attribute (homemaker-py-sel)
Closes the second namespace sharing a first character with programme codes: the
usage prefixes b/t/l/k, under which a room silently inherited another room's
connectivity rules from its spelling.
usage is a plain, MANDATORY attribute of the space definition -- not a lookup
table. An interim design proposed a top-level usage_classes: table binding
author-coined names to behaviour; withdrawn, because an indirect name->behaviour
mapping living apart from the thing it describes is exactly the shape of the
prefix rule §39 exists to remove, it would be the only such table in a schema
where every other space property is a plain attribute, and the need it served
was already met -- "building specific" is about what a room is CALLED, and
name: is already free text.
Rule that settles it: a usage value exists iff the engine treats it differently
somewhere. Config selects among behaviours; it cannot invent them.
- programme.USAGES (living/kitchen/bedroom/toilet/utility/none) plus the
behaviour groupings PRIVATE_USAGES / PRIVATE_STRIPS / TOILET_STRIPS /
SOCIABLE_USAGES. Missing or unknown usage is a load error naming the code,
from BOTH parse paths.
- Code-level, never leaf-level: usage_of(leaf.type) is looked up fresh, so a
retype changes the class automatically. 51 sites assign leaf.type, and
share/share_type plus the r5a resurrection are the precedent for why
leaf-level attributes rot.
- graph.has_circulation takes the usage map and trims on declared class;
fitness.access and the public-access check likewise. fitness._t0 is DELETED --
no first-character type test remains anywhere in the codebase.
- utility is distinct from bedroom (same access requirements today) because it
is a different use and gives derive_interchange_classes an axis to relax on.
- A toilet now keeps its edge to a terminal room -- the Brand adjacency, which
the old b-before-t loop ordering severed.
- All 107 corpus entries migrated by experiments/migrate_usage_key.py, comments
and layout preserved.
MEASURED -- the connectivity model was ~4x too permissive. `none` is not
neutral: nothing is trimmed, so the graph may route THROUGH the room, and 34 of
52 codes had no class (Dental Surgery, Records Room, Utilities Closet all served
as corridors). Edges trimmed, prefix-inferred vs declared, 3 seeds each:
harbor-house 18 (9%) -> 79 (39%) inaccessible fails 0 -> 4
health-centre 12 (8%) -> 59 (40%) inaccessible fails 2 -> 3
maple-court 53 (17%) -> 123 (39%) inaccessible fails 1 -> 5
Re-baseline (seed 1, 20k, harbor): 58 fails (15h/43s) -> 61 (16h/45s), now
reporting 1-inaccessible-usable-space x2 plus level 0 and level 1 not connected.
The count rose because the objective got honest -- those failures were always
true of the layout and the old model could not see them. Every harbor number
before this was measured against a graph crediting routes through store
cupboards.
Sharpens §38.2: the objective pays x60-85 to delete circulation, and until now
the deleted corridors were not missed because storage stood in for them. With
that substitution gone, homemaker-py-2v1 is the remaining half -- and now
measurable, because the fails it should prevent actually fire.
350 passed (+5 new), same 7 pre-existing fixture failures, lint unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 13:39:41 +00:00
|
|
|
|
p.write_text(yaml.safe_dump({"spaces": with_usage({
|
2026-06-28 22:04:35 +01:00
|
|
|
|
"b": {"size": [12.0, 1.0], "share": 3},
|
|
|
|
|
|
"k": {"size": [20.0, 1.0]}, # no share key
|
§39.7: access requirements become a declared `usage:` attribute (homemaker-py-sel)
Closes the second namespace sharing a first character with programme codes: the
usage prefixes b/t/l/k, under which a room silently inherited another room's
connectivity rules from its spelling.
usage is a plain, MANDATORY attribute of the space definition -- not a lookup
table. An interim design proposed a top-level usage_classes: table binding
author-coined names to behaviour; withdrawn, because an indirect name->behaviour
mapping living apart from the thing it describes is exactly the shape of the
prefix rule §39 exists to remove, it would be the only such table in a schema
where every other space property is a plain attribute, and the need it served
was already met -- "building specific" is about what a room is CALLED, and
name: is already free text.
Rule that settles it: a usage value exists iff the engine treats it differently
somewhere. Config selects among behaviours; it cannot invent them.
- programme.USAGES (living/kitchen/bedroom/toilet/utility/none) plus the
behaviour groupings PRIVATE_USAGES / PRIVATE_STRIPS / TOILET_STRIPS /
SOCIABLE_USAGES. Missing or unknown usage is a load error naming the code,
from BOTH parse paths.
- Code-level, never leaf-level: usage_of(leaf.type) is looked up fresh, so a
retype changes the class automatically. 51 sites assign leaf.type, and
share/share_type plus the r5a resurrection are the precedent for why
leaf-level attributes rot.
- graph.has_circulation takes the usage map and trims on declared class;
fitness.access and the public-access check likewise. fitness._t0 is DELETED --
no first-character type test remains anywhere in the codebase.
- utility is distinct from bedroom (same access requirements today) because it
is a different use and gives derive_interchange_classes an axis to relax on.
- A toilet now keeps its edge to a terminal room -- the Brand adjacency, which
the old b-before-t loop ordering severed.
- All 107 corpus entries migrated by experiments/migrate_usage_key.py, comments
and layout preserved.
MEASURED -- the connectivity model was ~4x too permissive. `none` is not
neutral: nothing is trimmed, so the graph may route THROUGH the room, and 34 of
52 codes had no class (Dental Surgery, Records Room, Utilities Closet all served
as corridors). Edges trimmed, prefix-inferred vs declared, 3 seeds each:
harbor-house 18 (9%) -> 79 (39%) inaccessible fails 0 -> 4
health-centre 12 (8%) -> 59 (40%) inaccessible fails 2 -> 3
maple-court 53 (17%) -> 123 (39%) inaccessible fails 1 -> 5
Re-baseline (seed 1, 20k, harbor): 58 fails (15h/43s) -> 61 (16h/45s), now
reporting 1-inaccessible-usable-space x2 plus level 0 and level 1 not connected.
The count rose because the objective got honest -- those failures were always
true of the layout and the old model could not see them. Every harbor number
before this was measured against a graph crediting routes through store
cupboards.
Sharpens §38.2: the objective pays x60-85 to delete circulation, and until now
the deleted corridors were not missed because storage stood in for them. With
that substitution gone, homemaker-py-2v1 is the remaining half -- and now
measurable, because the fails it should prevent actually fire.
350 passed (+5 new), same 7 pre-existing fixture failures, lint unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 13:39:41 +00:00
|
|
|
|
})}))
|
2026-06-28 22:04:35 +01:00
|
|
|
|
reqs = load_programme(str(p))
|
|
|
|
|
|
assert reqs["b"].share == 3 and reqs["b"].has_share is True
|
|
|
|
|
|
assert reqs["k"].share == 1 and reqs["k"].has_share is False
|
homemaker-py-2g7.3: hard/soft fail tiering behind --use-tiers flag
Splits the flat outer-search comparator (-n_fails, fitness) into a tiered
(-n_hard, -n_soft, fitness) so search budget stops being spent polishing
SOFT shape fails (crinkliness/proportion/size/width/edge-too-long/
staircase-volume) while HARD structural fails (missing space, wrong/
required level, level/circulation/vertical connectivity, adjacency,
stairs, covered-outside, storey limits, public access) remain unfixed.
fitness.classify_fail_tier/tier_counts classify every fail string emitted
across fitness.py and graph.py, raising on anything unrecognised so new
fail sites must declare a tier. Validated against all real fail strings in
the checked-in corpus plus every fail-emission call site read from source.
driver.Individual gains n_hard/n_soft (populated from innerloop.Result.
fail_lines); search(use_tiers=...) swaps the comparator when set (default
off, so existing runs are unaffected — inner-loop 0.5^n cliff untouched).
evolve.py exposes --use-tiers / HOMEMAKER_USE_TIERS.
experiments/tier_ab_2g7_3.py runs the acceptance A/B (harbor+maple, 3
seeds, 20k evals) in the background; results pending.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-02 16:00:39 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
# Hard/soft fail tiering (homemaker-py-2g7.3)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("fail_str", [
|
|
|
|
|
|
"missing required space: la1",
|
|
|
|
|
|
"missing required space: la1 (critical)",
|
|
|
|
|
|
"too many spaces: k (found 3, expected 2)",
|
|
|
|
|
|
"missing ef1: would need size check",
|
|
|
|
|
|
"missing ef1: would need width check",
|
|
|
|
|
|
"missing ef1: would need proportion check",
|
|
|
|
|
|
"missing m: would need adjacency to c",
|
|
|
|
|
|
"missing r: would need to be on level 1",
|
|
|
|
|
|
"missing t1: would need connection to c below",
|
|
|
|
|
|
"0/lr (cr1) not adjacent to c",
|
|
|
|
|
|
"li1 on wrong level (level 0, expected 1)",
|
|
|
|
|
|
"t1 not connected to c below",
|
|
|
|
|
|
"level 0 not connected",
|
|
|
|
|
|
"0 inaccessible usable space",
|
|
|
|
|
|
"level 0 no outside space",
|
|
|
|
|
|
"0/lr unsupported covered outside",
|
|
|
|
|
|
"0/lr covered outside above ground",
|
|
|
|
|
|
"too few stairs (0, min 1)",
|
|
|
|
|
|
"too many stairs (2, max 1)",
|
|
|
|
|
|
"storey limit",
|
|
|
|
|
|
"storey minimum",
|
|
|
|
|
|
"no outside public access",
|
|
|
|
|
|
])
|
|
|
|
|
|
def test_classify_fail_tier_hard(fail_str):
|
|
|
|
|
|
assert classify_fail_tier(fail_str) == "hard"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("fail_str", [
|
|
|
|
|
|
"0/lr perpendicular",
|
|
|
|
|
|
"0/lr proportion",
|
|
|
|
|
|
"0/lr size",
|
|
|
|
|
|
"0/lr width",
|
|
|
|
|
|
"0/lr crinkliness",
|
|
|
|
|
|
"0/lr access",
|
|
|
|
|
|
"0/lr lrr edge too long",
|
|
|
|
|
|
"lr outside edge too long",
|
|
|
|
|
|
"staircase volume",
|
|
|
|
|
|
])
|
|
|
|
|
|
def test_classify_fail_tier_soft(fail_str):
|
|
|
|
|
|
assert classify_fail_tier(fail_str) == "soft"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_classify_fail_tier_missing_cascade_is_hard_not_soft():
|
|
|
|
|
|
# "missing X: would need size check" contains the SOFT " size" substring,
|
|
|
|
|
|
# but is a consequence of a HARD missing-space fail, not a shape defect —
|
|
|
|
|
|
# the HARD markers must be checked first (fitness.py ordering).
|
|
|
|
|
|
assert classify_fail_tier("missing m#2: would need size check") == "hard"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_classify_fail_tier_unknown_raises():
|
|
|
|
|
|
with pytest.raises(ValueError):
|
|
|
|
|
|
classify_fail_tier("some brand new fail string nobody tiered yet")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_tier_counts_splits_hard_and_soft():
|
|
|
|
|
|
fails = ("level 0 not connected", "0/lr proportion", "0/lr crinkliness",
|
|
|
|
|
|
"missing required space: k1")
|
|
|
|
|
|
assert tier_counts(fails) == (2, 2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_tier_counts_empty():
|
|
|
|
|
|
assert tier_counts(()) == (0, 0)
|
|
|
|
|
|
|
|
|
|
|
|
|
Make both failing tests assert their intent, not stale artefacts
The suite is green for the first time this session: 376 passed, 0 failed.
test_collapse_insearch_reproduces_94g_finish_time_result hard-coded both
endpoints of the 17 result -- 15 fails before collapse, 12 after. Those
were measured before 39.4, when harbor's effective programme was silently
32 instances because codes like cr1 were read as generic circulation; the
same layout now scores 82. But the guarantee the test exists to protect,
per its own docstring, is that in-search collapse reaches the SAME layout
as finish-time collapse on fixed geometry -- and two independent constants
never checked that. They can both drift and stay equal, or both hold and
mask an inequality.
Rewritten to compute both sides live and assert they agree, plus that
collapse does not make the layout worse. Measured: 82 -> 58 in-search, and
finish-time collapse independently reaches 58 at iters=3 and iters=6. The
invariant holds; only the constants were stale. Restating the reference
figure itself remains homemaker-py-ut5.
test_classify_fail_tier_covers_full_corpus globbed examples/**/*.fails and
asserted checked > 0. Git tracks ZERO .fails -- they are artefacts the
scorer writes beside a .dom -- so its docstring described files that by
design never exist in the repo, and it passed only on a machine that had
already run the scorer. Split into: a test that GENERATES fails by scoring
three corpus layouts picked for breadth (requiring >= 8 distinct kinds so
it cannot silently narrow); a test that an unclassifiable string actually
raises; and an opportunistic .fails sweep that never requires them.
Verified by moving every .fails out of the tree and re-running.
Closes homemaker-py-1ue.
Lint at parity (46).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-29 07:11:25 +00:00
|
|
|
|
# Layouts chosen for BREADTH of failure kinds, not for being good designs --
|
|
|
|
|
|
# between them these emit size/width/proportion/crinkliness/access/adjacency,
|
|
|
|
|
|
# missing-space cascades, connectivity and volume fails.
|
|
|
|
|
|
_CORPUS_LAYOUTS = [
|
|
|
|
|
|
("harbor-house", "evolved-3M-nols-3.dom"),
|
|
|
|
|
|
("harbor-house", "generated.dom"),
|
|
|
|
|
|
("maple-court", "generated.dom"),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_classify_fail_tier_covers_every_fail_the_evaluator_emits():
|
|
|
|
|
|
"""Every fail string the evaluator can produce must classify into a tier.
|
|
|
|
|
|
|
|
|
|
|
|
Fails are GENERATED here by scoring corpus layouts. The previous version
|
|
|
|
|
|
globbed `examples/**/*.fails` and asserted it had checked something -- but
|
|
|
|
|
|
those are generated artefacts that `homemaker-fitness` writes beside a
|
|
|
|
|
|
`.dom`, absent from a clean checkout. So it passed only on a machine that
|
|
|
|
|
|
had already run the scorer, and in a fresh clone failed with `assert 0 > 0`:
|
|
|
|
|
|
it was asserting on the state of the developer's working tree, not on the
|
|
|
|
|
|
code (`homemaker-py-1ue`).
|
|
|
|
|
|
"""
|
|
|
|
|
|
import copy
|
|
|
|
|
|
|
|
|
|
|
|
from homemaker_layout import dom as dom_mod
|
|
|
|
|
|
|
|
|
|
|
|
repo_root = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
checked = kinds = 0
|
|
|
|
|
|
seen: set[str] = set()
|
|
|
|
|
|
for prog, name in _CORPUS_LAYOUTS:
|
|
|
|
|
|
path = repo_root / "examples" / prog / name
|
|
|
|
|
|
if not path.is_file():
|
|
|
|
|
|
continue
|
|
|
|
|
|
conf, cost = load_config(repo_root / "examples" / prog)
|
|
|
|
|
|
_, fails = Fitness(conf, cost).score_with_fails(
|
|
|
|
|
|
copy.deepcopy(dom_mod.load(str(path))))
|
|
|
|
|
|
for fail in fails:
|
|
|
|
|
|
classify_fail_tier(fail) # raises on an unclassified string
|
|
|
|
|
|
checked += 1
|
|
|
|
|
|
seen.add(fail.split()[-1])
|
|
|
|
|
|
kinds = len(seen)
|
|
|
|
|
|
assert checked > 0, "no corpus layout could be scored -- fixtures missing?"
|
|
|
|
|
|
assert kinds >= 8, f"only {kinds} distinct fail kinds exercised; too narrow"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_classify_fail_tier_rejects_an_unknown_fail_string():
|
|
|
|
|
|
"""The guard above is only worth anything if an unclassifiable string
|
|
|
|
|
|
actually raises."""
|
|
|
|
|
|
with pytest.raises(ValueError, match="unclassified fail string"):
|
|
|
|
|
|
classify_fail_tier("0/lr something nobody has ever emitted")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_classify_fail_tier_checks_any_native_fails_artefacts_present():
|
|
|
|
|
|
"""If a working tree happens to carry .fails artefacts, check them too --
|
|
|
|
|
|
but never require them to exist."""
|
homemaker-py-2g7.3: hard/soft fail tiering behind --use-tiers flag
Splits the flat outer-search comparator (-n_fails, fitness) into a tiered
(-n_hard, -n_soft, fitness) so search budget stops being spent polishing
SOFT shape fails (crinkliness/proportion/size/width/edge-too-long/
staircase-volume) while HARD structural fails (missing space, wrong/
required level, level/circulation/vertical connectivity, adjacency,
stairs, covered-outside, storey limits, public access) remain unfixed.
fitness.classify_fail_tier/tier_counts classify every fail string emitted
across fitness.py and graph.py, raising on anything unrecognised so new
fail sites must declare a tier. Validated against all real fail strings in
the checked-in corpus plus every fail-emission call site read from source.
driver.Individual gains n_hard/n_soft (populated from innerloop.Result.
fail_lines); search(use_tiers=...) swaps the comparator when set (default
off, so existing runs are unaffected — inner-loop 0.5^n cliff untouched).
evolve.py exposes --use-tiers / HOMEMAKER_USE_TIERS.
experiments/tier_ab_2g7_3.py runs the acceptance A/B (harbor+maple, 3
seeds, 20k evals) in the background; results pending.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-02 16:00:39 +01:00
|
|
|
|
import glob
|
|
|
|
|
|
|
|
|
|
|
|
repo_root = Path(__file__).resolve().parent.parent
|
Make both failing tests assert their intent, not stale artefacts
The suite is green for the first time this session: 376 passed, 0 failed.
test_collapse_insearch_reproduces_94g_finish_time_result hard-coded both
endpoints of the 17 result -- 15 fails before collapse, 12 after. Those
were measured before 39.4, when harbor's effective programme was silently
32 instances because codes like cr1 were read as generic circulation; the
same layout now scores 82. But the guarantee the test exists to protect,
per its own docstring, is that in-search collapse reaches the SAME layout
as finish-time collapse on fixed geometry -- and two independent constants
never checked that. They can both drift and stay equal, or both hold and
mask an inequality.
Rewritten to compute both sides live and assert they agree, plus that
collapse does not make the layout worse. Measured: 82 -> 58 in-search, and
finish-time collapse independently reaches 58 at iters=3 and iters=6. The
invariant holds; only the constants were stale. Restating the reference
figure itself remains homemaker-py-ut5.
test_classify_fail_tier_covers_full_corpus globbed examples/**/*.fails and
asserted checked > 0. Git tracks ZERO .fails -- they are artefacts the
scorer writes beside a .dom -- so its docstring described files that by
design never exist in the repo, and it passed only on a machine that had
already run the scorer. Split into: a test that GENERATES fails by scoring
three corpus layouts picked for breadth (requiring >= 8 distinct kinds so
it cannot silently narrow); a test that an unclassifiable string actually
raises; and an opportunistic .fails sweep that never requires them.
Verified by moving every .fails out of the tree and re-running.
Closes homemaker-py-1ue.
Lint at parity (46).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-29 07:11:25 +00:00
|
|
|
|
for path in glob.glob(str(repo_root / "examples" / "**" / "*.fails"),
|
|
|
|
|
|
recursive=True):
|
homemaker-py-2g7.3: hard/soft fail tiering behind --use-tiers flag
Splits the flat outer-search comparator (-n_fails, fitness) into a tiered
(-n_hard, -n_soft, fitness) so search budget stops being spent polishing
SOFT shape fails (crinkliness/proportion/size/width/edge-too-long/
staircase-volume) while HARD structural fails (missing space, wrong/
required level, level/circulation/vertical connectivity, adjacency,
stairs, covered-outside, storey limits, public access) remain unfixed.
fitness.classify_fail_tier/tier_counts classify every fail string emitted
across fitness.py and graph.py, raising on anything unrecognised so new
fail sites must declare a tier. Validated against all real fail strings in
the checked-in corpus plus every fail-emission call site read from source.
driver.Individual gains n_hard/n_soft (populated from innerloop.Result.
fail_lines); search(use_tiers=...) swaps the comparator when set (default
off, so existing runs are unaffected — inner-loop 0.5^n cliff untouched).
evolve.py exposes --use-tiers / HOMEMAKER_USE_TIERS.
experiments/tier_ab_2g7_3.py runs the acceptance A/B (harbor+maple, 3
seeds, 20k evals) in the background; results pending.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-02 16:00:39 +01:00
|
|
|
|
with open(path) as f:
|
|
|
|
|
|
first = f.readline()
|
|
|
|
|
|
if first.startswith("---"):
|
Make both failing tests assert their intent, not stale artefacts
The suite is green for the first time this session: 376 passed, 0 failed.
test_collapse_insearch_reproduces_94g_finish_time_result hard-coded both
endpoints of the 17 result -- 15 fails before collapse, 12 after. Those
were measured before 39.4, when harbor's effective programme was silently
32 instances because codes like cr1 were read as generic circulation; the
same layout now scores 82. But the guarantee the test exists to protect,
per its own docstring, is that in-search collapse reaches the SAME layout
as finish-time collapse on fixed geometry -- and two independent constants
never checked that. They can both drift and stay equal, or both hold and
mask an inequality.
Rewritten to compute both sides live and assert they agree, plus that
collapse does not make the layout worse. Measured: 82 -> 58 in-search, and
finish-time collapse independently reaches 58 at iters=3 and iters=6. The
invariant holds; only the constants were stale. Restating the reference
figure itself remains homemaker-py-ut5.
test_classify_fail_tier_covers_full_corpus globbed examples/**/*.fails and
asserted checked > 0. Git tracks ZERO .fails -- they are artefacts the
scorer writes beside a .dom -- so its docstring described files that by
design never exist in the repo, and it passed only on a machine that had
already run the scorer. Split into: a test that GENERATES fails by scoring
three corpus layouts picked for breadth (requiring >= 8 distinct kinds so
it cannot silently narrow); a test that an unclassifiable string actually
raises; and an opportunistic .fails sweep that never requires them.
Verified by moving every .fails out of the tree and re-running.
Closes homemaker-py-1ue.
Lint at parity (46).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-29 07:11:25 +00:00
|
|
|
|
continue # legacy Perl-oracle YAML, not this evaluator
|
homemaker-py-2g7.3: hard/soft fail tiering behind --use-tiers flag
Splits the flat outer-search comparator (-n_fails, fitness) into a tiered
(-n_hard, -n_soft, fitness) so search budget stops being spent polishing
SOFT shape fails (crinkliness/proportion/size/width/edge-too-long/
staircase-volume) while HARD structural fails (missing space, wrong/
required level, level/circulation/vertical connectivity, adjacency,
stairs, covered-outside, storey limits, public access) remain unfixed.
fitness.classify_fail_tier/tier_counts classify every fail string emitted
across fitness.py and graph.py, raising on anything unrecognised so new
fail sites must declare a tier. Validated against all real fail strings in
the checked-in corpus plus every fail-emission call site read from source.
driver.Individual gains n_hard/n_soft (populated from innerloop.Result.
fail_lines); search(use_tiers=...) swaps the comparator when set (default
off, so existing runs are unaffected — inner-loop 0.5^n cliff untouched).
evolve.py exposes --use-tiers / HOMEMAKER_USE_TIERS.
experiments/tier_ab_2g7_3.py runs the acceptance A/B (harbor+maple, 3
seeds, 20k evals) in the background; results pending.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-02 16:00:39 +01:00
|
|
|
|
lines = [first.rstrip("\n")] + [ln.rstrip("\n") for ln in f]
|
|
|
|
|
|
for line in lines:
|
Make both failing tests assert their intent, not stale artefacts
The suite is green for the first time this session: 376 passed, 0 failed.
test_collapse_insearch_reproduces_94g_finish_time_result hard-coded both
endpoints of the 17 result -- 15 fails before collapse, 12 after. Those
were measured before 39.4, when harbor's effective programme was silently
32 instances because codes like cr1 were read as generic circulation; the
same layout now scores 82. But the guarantee the test exists to protect,
per its own docstring, is that in-search collapse reaches the SAME layout
as finish-time collapse on fixed geometry -- and two independent constants
never checked that. They can both drift and stay equal, or both hold and
mask an inequality.
Rewritten to compute both sides live and assert they agree, plus that
collapse does not make the layout worse. Measured: 82 -> 58 in-search, and
finish-time collapse independently reaches 58 at iters=3 and iters=6. The
invariant holds; only the constants were stale. Restating the reference
figure itself remains homemaker-py-ut5.
test_classify_fail_tier_covers_full_corpus globbed examples/**/*.fails and
asserted checked > 0. Git tracks ZERO .fails -- they are artefacts the
scorer writes beside a .dom -- so its docstring described files that by
design never exist in the repo, and it passed only on a machine that had
already run the scorer. Split into: a test that GENERATES fails by scoring
three corpus layouts picked for breadth (requiring >= 8 distinct kinds so
it cannot silently narrow); a test that an unclassifiable string actually
raises; and an opportunistic .fails sweep that never requires them.
Verified by moving every .fails out of the tree and re-running.
Closes homemaker-py-1ue.
Lint at parity (46).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-29 07:11:25 +00:00
|
|
|
|
if line:
|
|
|
|
|
|
classify_fail_tier(line)
|
§38.2 refinement: connectivity is under-priced ~3x, not just a crinkliness bug
Follow-up measurement corrects the first draft of §38 in two ways.
1. Harbor-house's floor is 15 fails (evolved-3M-nols-3, 1.7M evals), not the
30-40 I quoted from §13.11's 20k-budget runs. Frontage deficit predicts the
COST of solving, not impossibility: ~150x budget gap between a
frontage-short and a frontage-surplus programme. Table corrected.
2. Zero-exposure is only half the mechanism, and not the dominant half.
Splitting the deletion test by lit vs buried shows a WELL-DAYLIT corridor
(q_crink=0.736) is still worth x4.06 to delete. Cause: value_circulation=50
vs value_inside=300, so merging corridor into room is a flat x6 gain, while
'level N not connected' costs only x0.5. Break-even needs 0.5^k < 50/300,
i.e. k > 2.58 -- severing must cost at least 3 fails and costs 1. Net x3.0
predicted, x4.06 measured. The objective is net-positive on severing the
spine even when the circulation is perfectly lit, which explains why both
'level N not connected' fails survive in the best layout after 1.7M evals.
Adds fitness.quality_uncrinkliness crinkliness_mode (EXPERIMENTAL, default
"urb" = stock hard 0.0, byte-identical: 336 passed vs 331 before, same 7
pre-existing fixture failures). A/B harness ab_crinkliness_mode_ssz.py shows
none of the three modes removes the incentive, and the lit column is 3/8 under
every mode including stock -- clean isolation of the two mechanisms.
Filed homemaker-py-2v1 (P0) for the pricing fix; ssz/hxi now depend on it.
Acceptance test recorded up front: harbor must reach 15 fails in materially
fewer than 1.7M evals AND without either not-connected fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 07:40:37 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
# homemaker-py-ssz / DESIGN.md §38.1 — crinkliness_mode (EXPERIMENTAL)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class _StubCrink(Fitness):
|
|
|
|
|
|
"""Fitness with ``crinkliness`` stubbed, so the modes can be tested without
|
|
|
|
|
|
building a real tree/graph (the value under test is the branch, not the
|
|
|
|
|
|
geometry)."""
|
|
|
|
|
|
|
|
|
|
|
|
_stub = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
def crinkliness(self, leaf, G, groups): # noqa: D102 - test stub
|
|
|
|
|
|
return self._stub
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _stub_fit(mode=None, stub=0.0, type_="t1"):
|
|
|
|
|
|
conf = dict(CONF_DEFAULTS)
|
|
|
|
|
|
if mode is not None:
|
|
|
|
|
|
conf["crinkliness_mode"] = mode
|
|
|
|
|
|
f = _StubCrink(conf, dict(COST_DEFAULTS))
|
|
|
|
|
|
f._stub = stub
|
|
|
|
|
|
return f, _leaf(type_)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_crinkliness_mode_defaults_to_urb_and_reproduces_hard_zero():
|
|
|
|
|
|
"""Default must be byte-identical to stock Urb: buried leaf -> exactly 0.0."""
|
|
|
|
|
|
f, leaf = _stub_fit()
|
|
|
|
|
|
assert f._crinkliness_mode == "urb"
|
|
|
|
|
|
assert f.quality_uncrinkliness(leaf, None, {}) == 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_crinkliness_floor_restores_gradient_but_keeps_the_failure():
|
|
|
|
|
|
"""The floor must stay BELOW FAIL_THRESHOLD: it restores a value gradient
|
|
|
|
|
|
without silently deleting a whole fail category."""
|
|
|
|
|
|
f, leaf = _stub_fit("floor")
|
|
|
|
|
|
q = f.quality_uncrinkliness(leaf, None, {})
|
|
|
|
|
|
assert q > 0.0, "buried leaf should no longer be worth exactly nothing"
|
|
|
|
|
|
assert q < FAIL_THRESHOLD, "buried leaf must still emit its crinkliness fail"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_crinkliness_compact_ok_clips_on_the_compact_side_only():
|
|
|
|
|
|
"""Being more compact than target is not a defect; being over-exposed is."""
|
|
|
|
|
|
target = CONF_DEFAULTS["uncrinkliness"][0]
|
|
|
|
|
|
# 1/crink > target => more compact than target => clipped to 1.0
|
|
|
|
|
|
f, leaf = _stub_fit("compact_ok", stub=1.0 / (target * 2))
|
|
|
|
|
|
assert f.quality_uncrinkliness(leaf, None, {}) == 1.0
|
|
|
|
|
|
# 1/crink < target => over-exposed => still decays
|
|
|
|
|
|
f, leaf = _stub_fit("compact_ok", stub=1.0 / (target / 2))
|
|
|
|
|
|
assert f.quality_uncrinkliness(leaf, None, {}) < 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_crinkliness_exempt_circulation_only_exempts_circulation():
|
|
|
|
|
|
f, circ = _stub_fit("exempt_circulation", type_="C")
|
|
|
|
|
|
assert f.quality_uncrinkliness(circ, None, {}) == 1.0
|
|
|
|
|
|
f, room = _stub_fit("exempt_circulation", type_="t1")
|
|
|
|
|
|
assert f.quality_uncrinkliness(room, None, {}) == 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
ssz: daylight is required of rooms that do not need it
DESIGN.md 38.6 concluded the three crinkliness modes were inert against the
circulation-deletion incentive. Two things were wrong with that measurement.
Its premise, 38.2, is retracted. And its script selected leaves with the
pre-39.4 prefix rule `type[:1].upper() in ("C","O")`, which sweeps every
programme room starting with c or o -- cr1, of1 -- in as circulation.
The simpler problem is that none of the three modes ever touched the leaves
ssz is about. quality_uncrinkliness reaches `if not crink` before any mode
logic that matters, so for a zero-exposure leaf: floor returns 0.01 (one
percent of a unit quality, multiplied into a product and weighed against a
whole leaf's cost -- inert); compact_ok is self-contradictory, announcing
that compact is not a defect and then returning the floor for the most
compact case of all; exempt_circulation reaches at most a third of them.
Measured: 0% / 0% / 0% / 21-33% of buried leaves rescued.
What the buried leaves are, now that 39.7 gives every space a usage: two
thirds of them are spaces that architecturally do not want a window --
stores, WCs, plant, corridors, covered courtyards -- scored identically
with a windowless bedroom. harbor 22/33, maple 33/46, health 9/18.
- crinkliness_mode="usage_daylight": daylight required of the uses a
person occupies (programme.DAYLIGHT_USAGES) and nothing else. Elsewhere
the factor is clipped on the compact side only, so being buried stops
being a defect while over-exposure still costs -- a crinkly leaf costs
envelope whatever it is used for. A windowless bedroom stays the hard
zero it is under stock: 11/11, 13/13, 9/9 still failing.
- compact_ok repaired to score the buried limit as compact, the behaviour
its name always claimed. It now rescues 100% including bedrooms, and is
kept as the upper-bound control, not a candidate.
- ab_ssz_search.py: the fixed-budget search A/B ssz's acceptance criteria
actually asks for. Every arm is optimised under its own objective and
re-scored under stock urb, because the permissive modes return 1.0
where stock fails and would otherwise win by deleting a fail category.
- ab_crinkliness_mode_ssz.py: prefix rule fixed, retracted premise
flagged in its docstring.
- 38.7's remaining claims from the retracted 38.2/38.3 corrected.
Default is unchanged ("urb"), byte-identical to all prior runs. Lint at
parity (46 pre-existing); tests 366 passed, 10 new, same 7 pre-existing
fixture failures (homemaker-py-bdf).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 16:45:10 +00:00
|
|
|
|
def test_crinkliness_compact_ok_scores_the_buried_limit_as_compact():
|
|
|
|
|
|
"""Regression (§38.8): a zero-exposure leaf IS the compact limit.
|
|
|
|
|
|
|
|
|
|
|
|
The first `compact_ok` returned the floor here, i.e. it announced that
|
|
|
|
|
|
being compact is not a defect and then punished the most compact case of
|
|
|
|
|
|
all hardest -- which is why it measured inert on buried leaves.
|
|
|
|
|
|
"""
|
|
|
|
|
|
f, leaf = _stub_fit("compact_ok", stub=0.0)
|
|
|
|
|
|
assert f.quality_uncrinkliness(leaf, None, {}) == 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
ssz: crinkliness is declared per space; there is no daylight attribute
Owner's ruling, and it corrects the design not just the classification: the
daylight requirement is already defined in the crinkliness. The gaussian's
compact side IS "too little exposed wall per unit floor"; its exposed side
is envelope cost. 38.9's proposed daylight: axis was redundant, and keying
it off usage: was worse than redundant.
What was actually missing: crinkliness is the only leaf quality factor with
no per-space target. size, width and proportion are all declared by the
space; crinkliness was one global number for every room in every building.
crinkliness: none -> no minimum-exposure requirement, may be buried
crinkliness: [t, s] -> this space's own target
key absent -> the global uncrinkliness target, as today
`none` clips the factor on the compact side, it does not switch it off:
over-exposure is still penalised, because a crinkly leaf costs envelope
whatever it holds. A store may be buried; a store may not be a starfish.
The mechanism is backward compatible -- an absent key resolves to the
global target, so shipping it changes no score. Behaviour changes only
where a config declares something, which keeps the objective change
visible per programme in config rather than hidden in a default.
Owner's classification: everything a person occupies wants a window, WCs
and reception/waiting/foyer included; only stores, plant, records and
laundry do not. migrate_crinkliness_key.py declared crinkliness: none on 18
corpus spaces. Crinkliness fails 271 -> 243, of which not-defects 136 (50%)
-> 108 (44%); the 28 that went are exactly the utility fails.
usage_daylight and needs_daylight are removed as mis-keyed, and
DAYLIGHT_USAGES with them -- a vocabulary value should exist only where the
engine treats it differently. The historical crinkliness_mode modes stay,
default off, so 38.6/38.8 remain reproducible.
uncrinkliness_circulation is now settable to none like any space, but its
default is left unchanged pending a ruling: corridors were not among the
groups ruled on and are 63% of the remaining phantom fails.
Lint at parity (46); tests 364 passed, same 7 pre-existing fixture failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-28 22:38:54 +00:00
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
# homemaker-py-ssz / DESIGN.md §38.10 — per-space crinkliness (the SHIPPING fix)
|
|
|
|
|
|
#
|
|
|
|
|
|
# The compact side of the crinkliness gaussian IS the daylight requirement, so
|
|
|
|
|
|
# a space declares it in its own `crinkliness:` target, like `size:` or
|
|
|
|
|
|
# `width:`. There is no separate daylight attribute -- see §38.9 for why
|
|
|
|
|
|
# keying it off `usage:` (an ACCESS class) was wrong.
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
def _declared_fit(stub, space=None, conf_extra=None, code="x1"):
|
|
|
|
|
|
"""Stub Fitness with a one-space programme, optionally declaring
|
|
|
|
|
|
`crinkliness:`, so `crinkliness_params` resolves off real config."""
|
ssz: daylight is required of rooms that do not need it
DESIGN.md 38.6 concluded the three crinkliness modes were inert against the
circulation-deletion incentive. Two things were wrong with that measurement.
Its premise, 38.2, is retracted. And its script selected leaves with the
pre-39.4 prefix rule `type[:1].upper() in ("C","O")`, which sweeps every
programme room starting with c or o -- cr1, of1 -- in as circulation.
The simpler problem is that none of the three modes ever touched the leaves
ssz is about. quality_uncrinkliness reaches `if not crink` before any mode
logic that matters, so for a zero-exposure leaf: floor returns 0.01 (one
percent of a unit quality, multiplied into a product and weighed against a
whole leaf's cost -- inert); compact_ok is self-contradictory, announcing
that compact is not a defect and then returning the floor for the most
compact case of all; exempt_circulation reaches at most a third of them.
Measured: 0% / 0% / 0% / 21-33% of buried leaves rescued.
What the buried leaves are, now that 39.7 gives every space a usage: two
thirds of them are spaces that architecturally do not want a window --
stores, WCs, plant, corridors, covered courtyards -- scored identically
with a windowless bedroom. harbor 22/33, maple 33/46, health 9/18.
- crinkliness_mode="usage_daylight": daylight required of the uses a
person occupies (programme.DAYLIGHT_USAGES) and nothing else. Elsewhere
the factor is clipped on the compact side only, so being buried stops
being a defect while over-exposure still costs -- a crinkly leaf costs
envelope whatever it is used for. A windowless bedroom stays the hard
zero it is under stock: 11/11, 13/13, 9/9 still failing.
- compact_ok repaired to score the buried limit as compact, the behaviour
its name always claimed. It now rescues 100% including bedrooms, and is
kept as the upper-bound control, not a candidate.
- ab_ssz_search.py: the fixed-budget search A/B ssz's acceptance criteria
actually asks for. Every arm is optimised under its own objective and
re-scored under stock urb, because the permissive modes return 1.0
where stock fails and would otherwise win by deleting a fail category.
- ab_crinkliness_mode_ssz.py: prefix rule fixed, retracted premise
flagged in its docstring.
- 38.7's remaining claims from the retracted 38.2/38.3 corrected.
Default is unchanged ("urb"), byte-identical to all prior runs. Lint at
parity (46 pre-existing); tests 366 passed, 10 new, same 7 pre-existing
fixture failures (homemaker-py-bdf).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 16:45:10 +00:00
|
|
|
|
conf = dict(CONF_DEFAULTS)
|
ssz: crinkliness is declared per space; there is no daylight attribute
Owner's ruling, and it corrects the design not just the classification: the
daylight requirement is already defined in the crinkliness. The gaussian's
compact side IS "too little exposed wall per unit floor"; its exposed side
is envelope cost. 38.9's proposed daylight: axis was redundant, and keying
it off usage: was worse than redundant.
What was actually missing: crinkliness is the only leaf quality factor with
no per-space target. size, width and proportion are all declared by the
space; crinkliness was one global number for every room in every building.
crinkliness: none -> no minimum-exposure requirement, may be buried
crinkliness: [t, s] -> this space's own target
key absent -> the global uncrinkliness target, as today
`none` clips the factor on the compact side, it does not switch it off:
over-exposure is still penalised, because a crinkly leaf costs envelope
whatever it holds. A store may be buried; a store may not be a starfish.
The mechanism is backward compatible -- an absent key resolves to the
global target, so shipping it changes no score. Behaviour changes only
where a config declares something, which keeps the objective change
visible per programme in config rather than hidden in a default.
Owner's classification: everything a person occupies wants a window, WCs
and reception/waiting/foyer included; only stores, plant, records and
laundry do not. migrate_crinkliness_key.py declared crinkliness: none on 18
corpus spaces. Crinkliness fails 271 -> 243, of which not-defects 136 (50%)
-> 108 (44%); the 28 that went are exactly the utility fails.
usage_daylight and needs_daylight are removed as mis-keyed, and
DAYLIGHT_USAGES with them -- a vocabulary value should exist only where the
engine treats it differently. The historical crinkliness_mode modes stay,
default off, so 38.6/38.8 remain reproducible.
uncrinkliness_circulation is now settable to none like any space, but its
default is left unchanged pending a ruling: corridors were not among the
groups ruled on and are 63% of the remaining phantom fails.
Lint at parity (46); tests 364 passed, same 7 pre-existing fixture failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-28 22:38:54 +00:00
|
|
|
|
conf["spaces"] = {code: dict({"usage": "living", "size": [4.0, 1.0]},
|
|
|
|
|
|
**(space or {}))}
|
|
|
|
|
|
conf.update(conf_extra or {})
|
ssz: daylight is required of rooms that do not need it
DESIGN.md 38.6 concluded the three crinkliness modes were inert against the
circulation-deletion incentive. Two things were wrong with that measurement.
Its premise, 38.2, is retracted. And its script selected leaves with the
pre-39.4 prefix rule `type[:1].upper() in ("C","O")`, which sweeps every
programme room starting with c or o -- cr1, of1 -- in as circulation.
The simpler problem is that none of the three modes ever touched the leaves
ssz is about. quality_uncrinkliness reaches `if not crink` before any mode
logic that matters, so for a zero-exposure leaf: floor returns 0.01 (one
percent of a unit quality, multiplied into a product and weighed against a
whole leaf's cost -- inert); compact_ok is self-contradictory, announcing
that compact is not a defect and then returning the floor for the most
compact case of all; exempt_circulation reaches at most a third of them.
Measured: 0% / 0% / 0% / 21-33% of buried leaves rescued.
What the buried leaves are, now that 39.7 gives every space a usage: two
thirds of them are spaces that architecturally do not want a window --
stores, WCs, plant, corridors, covered courtyards -- scored identically
with a windowless bedroom. harbor 22/33, maple 33/46, health 9/18.
- crinkliness_mode="usage_daylight": daylight required of the uses a
person occupies (programme.DAYLIGHT_USAGES) and nothing else. Elsewhere
the factor is clipped on the compact side only, so being buried stops
being a defect while over-exposure still costs -- a crinkly leaf costs
envelope whatever it is used for. A windowless bedroom stays the hard
zero it is under stock: 11/11, 13/13, 9/9 still failing.
- compact_ok repaired to score the buried limit as compact, the behaviour
its name always claimed. It now rescues 100% including bedrooms, and is
kept as the upper-bound control, not a candidate.
- ab_ssz_search.py: the fixed-budget search A/B ssz's acceptance criteria
actually asks for. Every arm is optimised under its own objective and
re-scored under stock urb, because the permissive modes return 1.0
where stock fails and would otherwise win by deleting a fail category.
- ab_crinkliness_mode_ssz.py: prefix rule fixed, retracted premise
flagged in its docstring.
- 38.7's remaining claims from the retracted 38.2/38.3 corrected.
Default is unchanged ("urb"), byte-identical to all prior runs. Lint at
parity (46 pre-existing); tests 366 passed, 10 new, same 7 pre-existing
fixture failures (homemaker-py-bdf).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 16:45:10 +00:00
|
|
|
|
f = _StubCrink(conf, dict(COST_DEFAULTS))
|
|
|
|
|
|
f._stub = stub
|
|
|
|
|
|
return f, _leaf(code)
|
|
|
|
|
|
|
|
|
|
|
|
|
ssz: crinkliness is declared per space; there is no daylight attribute
Owner's ruling, and it corrects the design not just the classification: the
daylight requirement is already defined in the crinkliness. The gaussian's
compact side IS "too little exposed wall per unit floor"; its exposed side
is envelope cost. 38.9's proposed daylight: axis was redundant, and keying
it off usage: was worse than redundant.
What was actually missing: crinkliness is the only leaf quality factor with
no per-space target. size, width and proportion are all declared by the
space; crinkliness was one global number for every room in every building.
crinkliness: none -> no minimum-exposure requirement, may be buried
crinkliness: [t, s] -> this space's own target
key absent -> the global uncrinkliness target, as today
`none` clips the factor on the compact side, it does not switch it off:
over-exposure is still penalised, because a crinkly leaf costs envelope
whatever it holds. A store may be buried; a store may not be a starfish.
The mechanism is backward compatible -- an absent key resolves to the
global target, so shipping it changes no score. Behaviour changes only
where a config declares something, which keeps the objective change
visible per programme in config rather than hidden in a default.
Owner's classification: everything a person occupies wants a window, WCs
and reception/waiting/foyer included; only stores, plant, records and
laundry do not. migrate_crinkliness_key.py declared crinkliness: none on 18
corpus spaces. Crinkliness fails 271 -> 243, of which not-defects 136 (50%)
-> 108 (44%); the 28 that went are exactly the utility fails.
usage_daylight and needs_daylight are removed as mis-keyed, and
DAYLIGHT_USAGES with them -- a vocabulary value should exist only where the
engine treats it differently. The historical crinkliness_mode modes stay,
default off, so 38.6/38.8 remain reproducible.
uncrinkliness_circulation is now settable to none like any space, but its
default is left unchanged pending a ruling: corridors were not among the
groups ruled on and are 63% of the remaining phantom fails.
Lint at parity (46); tests 364 passed, same 7 pre-existing fixture failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-28 22:38:54 +00:00
|
|
|
|
def test_declared_crinkliness_absent_keeps_stock_behaviour():
|
|
|
|
|
|
"""No `crinkliness:` key -> the global target, unchanged: buried = 0.0.
|
ssz: daylight is required of rooms that do not need it
DESIGN.md 38.6 concluded the three crinkliness modes were inert against the
circulation-deletion incentive. Two things were wrong with that measurement.
Its premise, 38.2, is retracted. And its script selected leaves with the
pre-39.4 prefix rule `type[:1].upper() in ("C","O")`, which sweeps every
programme room starting with c or o -- cr1, of1 -- in as circulation.
The simpler problem is that none of the three modes ever touched the leaves
ssz is about. quality_uncrinkliness reaches `if not crink` before any mode
logic that matters, so for a zero-exposure leaf: floor returns 0.01 (one
percent of a unit quality, multiplied into a product and weighed against a
whole leaf's cost -- inert); compact_ok is self-contradictory, announcing
that compact is not a defect and then returning the floor for the most
compact case of all; exempt_circulation reaches at most a third of them.
Measured: 0% / 0% / 0% / 21-33% of buried leaves rescued.
What the buried leaves are, now that 39.7 gives every space a usage: two
thirds of them are spaces that architecturally do not want a window --
stores, WCs, plant, corridors, covered courtyards -- scored identically
with a windowless bedroom. harbor 22/33, maple 33/46, health 9/18.
- crinkliness_mode="usage_daylight": daylight required of the uses a
person occupies (programme.DAYLIGHT_USAGES) and nothing else. Elsewhere
the factor is clipped on the compact side only, so being buried stops
being a defect while over-exposure still costs -- a crinkly leaf costs
envelope whatever it is used for. A windowless bedroom stays the hard
zero it is under stock: 11/11, 13/13, 9/9 still failing.
- compact_ok repaired to score the buried limit as compact, the behaviour
its name always claimed. It now rescues 100% including bedrooms, and is
kept as the upper-bound control, not a candidate.
- ab_ssz_search.py: the fixed-budget search A/B ssz's acceptance criteria
actually asks for. Every arm is optimised under its own objective and
re-scored under stock urb, because the permissive modes return 1.0
where stock fails and would otherwise win by deleting a fail category.
- ab_crinkliness_mode_ssz.py: prefix rule fixed, retracted premise
flagged in its docstring.
- 38.7's remaining claims from the retracted 38.2/38.3 corrected.
Default is unchanged ("urb"), byte-identical to all prior runs. Lint at
parity (46 pre-existing); tests 366 passed, 10 new, same 7 pre-existing
fixture failures (homemaker-py-bdf).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 16:45:10 +00:00
|
|
|
|
|
ssz: crinkliness is declared per space; there is no daylight attribute
Owner's ruling, and it corrects the design not just the classification: the
daylight requirement is already defined in the crinkliness. The gaussian's
compact side IS "too little exposed wall per unit floor"; its exposed side
is envelope cost. 38.9's proposed daylight: axis was redundant, and keying
it off usage: was worse than redundant.
What was actually missing: crinkliness is the only leaf quality factor with
no per-space target. size, width and proportion are all declared by the
space; crinkliness was one global number for every room in every building.
crinkliness: none -> no minimum-exposure requirement, may be buried
crinkliness: [t, s] -> this space's own target
key absent -> the global uncrinkliness target, as today
`none` clips the factor on the compact side, it does not switch it off:
over-exposure is still penalised, because a crinkly leaf costs envelope
whatever it holds. A store may be buried; a store may not be a starfish.
The mechanism is backward compatible -- an absent key resolves to the
global target, so shipping it changes no score. Behaviour changes only
where a config declares something, which keeps the objective change
visible per programme in config rather than hidden in a default.
Owner's classification: everything a person occupies wants a window, WCs
and reception/waiting/foyer included; only stores, plant, records and
laundry do not. migrate_crinkliness_key.py declared crinkliness: none on 18
corpus spaces. Crinkliness fails 271 -> 243, of which not-defects 136 (50%)
-> 108 (44%); the 28 that went are exactly the utility fails.
usage_daylight and needs_daylight are removed as mis-keyed, and
DAYLIGHT_USAGES with them -- a vocabulary value should exist only where the
engine treats it differently. The historical crinkliness_mode modes stay,
default off, so 38.6/38.8 remain reproducible.
uncrinkliness_circulation is now settable to none like any space, but its
default is left unchanged pending a ruling: corridors were not among the
groups ruled on and are 63% of the remaining phantom fails.
Lint at parity (46); tests 364 passed, same 7 pre-existing fixture failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-28 22:38:54 +00:00
|
|
|
|
This is what makes the mechanism backward compatible -- shipping it
|
|
|
|
|
|
changes no score until a config actually declares something.
|
|
|
|
|
|
"""
|
|
|
|
|
|
f, leaf = _declared_fit(0.0)
|
|
|
|
|
|
assert f.crinkliness_params(leaf) == tuple(CONF_DEFAULTS["uncrinkliness"])
|
ssz: daylight is required of rooms that do not need it
DESIGN.md 38.6 concluded the three crinkliness modes were inert against the
circulation-deletion incentive. Two things were wrong with that measurement.
Its premise, 38.2, is retracted. And its script selected leaves with the
pre-39.4 prefix rule `type[:1].upper() in ("C","O")`, which sweeps every
programme room starting with c or o -- cr1, of1 -- in as circulation.
The simpler problem is that none of the three modes ever touched the leaves
ssz is about. quality_uncrinkliness reaches `if not crink` before any mode
logic that matters, so for a zero-exposure leaf: floor returns 0.01 (one
percent of a unit quality, multiplied into a product and weighed against a
whole leaf's cost -- inert); compact_ok is self-contradictory, announcing
that compact is not a defect and then returning the floor for the most
compact case of all; exempt_circulation reaches at most a third of them.
Measured: 0% / 0% / 0% / 21-33% of buried leaves rescued.
What the buried leaves are, now that 39.7 gives every space a usage: two
thirds of them are spaces that architecturally do not want a window --
stores, WCs, plant, corridors, covered courtyards -- scored identically
with a windowless bedroom. harbor 22/33, maple 33/46, health 9/18.
- crinkliness_mode="usage_daylight": daylight required of the uses a
person occupies (programme.DAYLIGHT_USAGES) and nothing else. Elsewhere
the factor is clipped on the compact side only, so being buried stops
being a defect while over-exposure still costs -- a crinkly leaf costs
envelope whatever it is used for. A windowless bedroom stays the hard
zero it is under stock: 11/11, 13/13, 9/9 still failing.
- compact_ok repaired to score the buried limit as compact, the behaviour
its name always claimed. It now rescues 100% including bedrooms, and is
kept as the upper-bound control, not a candidate.
- ab_ssz_search.py: the fixed-budget search A/B ssz's acceptance criteria
actually asks for. Every arm is optimised under its own objective and
re-scored under stock urb, because the permissive modes return 1.0
where stock fails and would otherwise win by deleting a fail category.
- ab_crinkliness_mode_ssz.py: prefix rule fixed, retracted premise
flagged in its docstring.
- 38.7's remaining claims from the retracted 38.2/38.3 corrected.
Default is unchanged ("urb"), byte-identical to all prior runs. Lint at
parity (46 pre-existing); tests 366 passed, 10 new, same 7 pre-existing
fixture failures (homemaker-py-bdf).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 16:45:10 +00:00
|
|
|
|
assert f.quality_uncrinkliness(leaf, None, {}) == 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
ssz: crinkliness is declared per space; there is no daylight attribute
Owner's ruling, and it corrects the design not just the classification: the
daylight requirement is already defined in the crinkliness. The gaussian's
compact side IS "too little exposed wall per unit floor"; its exposed side
is envelope cost. 38.9's proposed daylight: axis was redundant, and keying
it off usage: was worse than redundant.
What was actually missing: crinkliness is the only leaf quality factor with
no per-space target. size, width and proportion are all declared by the
space; crinkliness was one global number for every room in every building.
crinkliness: none -> no minimum-exposure requirement, may be buried
crinkliness: [t, s] -> this space's own target
key absent -> the global uncrinkliness target, as today
`none` clips the factor on the compact side, it does not switch it off:
over-exposure is still penalised, because a crinkly leaf costs envelope
whatever it holds. A store may be buried; a store may not be a starfish.
The mechanism is backward compatible -- an absent key resolves to the
global target, so shipping it changes no score. Behaviour changes only
where a config declares something, which keeps the objective change
visible per programme in config rather than hidden in a default.
Owner's classification: everything a person occupies wants a window, WCs
and reception/waiting/foyer included; only stores, plant, records and
laundry do not. migrate_crinkliness_key.py declared crinkliness: none on 18
corpus spaces. Crinkliness fails 271 -> 243, of which not-defects 136 (50%)
-> 108 (44%); the 28 that went are exactly the utility fails.
usage_daylight and needs_daylight are removed as mis-keyed, and
DAYLIGHT_USAGES with them -- a vocabulary value should exist only where the
engine treats it differently. The historical crinkliness_mode modes stay,
default off, so 38.6/38.8 remain reproducible.
uncrinkliness_circulation is now settable to none like any space, but its
default is left unchanged pending a ruling: corridors were not among the
groups ruled on and are 63% of the remaining phantom fails.
Lint at parity (46); tests 364 passed, same 7 pre-existing fixture failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-28 22:38:54 +00:00
|
|
|
|
def test_declared_crinkliness_none_lets_a_space_be_buried():
|
|
|
|
|
|
"""`crinkliness: none` says this space needs no window. Fully buried --
|
|
|
|
|
|
the compact limit -- is then not a defect."""
|
|
|
|
|
|
f, leaf = _declared_fit(0.0, {"crinkliness": None})
|
|
|
|
|
|
assert f.crinkliness_params(leaf) is None
|
|
|
|
|
|
assert f.quality_uncrinkliness(leaf, None, {}) == 1.0
|
ssz: daylight is required of rooms that do not need it
DESIGN.md 38.6 concluded the three crinkliness modes were inert against the
circulation-deletion incentive. Two things were wrong with that measurement.
Its premise, 38.2, is retracted. And its script selected leaves with the
pre-39.4 prefix rule `type[:1].upper() in ("C","O")`, which sweeps every
programme room starting with c or o -- cr1, of1 -- in as circulation.
The simpler problem is that none of the three modes ever touched the leaves
ssz is about. quality_uncrinkliness reaches `if not crink` before any mode
logic that matters, so for a zero-exposure leaf: floor returns 0.01 (one
percent of a unit quality, multiplied into a product and weighed against a
whole leaf's cost -- inert); compact_ok is self-contradictory, announcing
that compact is not a defect and then returning the floor for the most
compact case of all; exempt_circulation reaches at most a third of them.
Measured: 0% / 0% / 0% / 21-33% of buried leaves rescued.
What the buried leaves are, now that 39.7 gives every space a usage: two
thirds of them are spaces that architecturally do not want a window --
stores, WCs, plant, corridors, covered courtyards -- scored identically
with a windowless bedroom. harbor 22/33, maple 33/46, health 9/18.
- crinkliness_mode="usage_daylight": daylight required of the uses a
person occupies (programme.DAYLIGHT_USAGES) and nothing else. Elsewhere
the factor is clipped on the compact side only, so being buried stops
being a defect while over-exposure still costs -- a crinkly leaf costs
envelope whatever it is used for. A windowless bedroom stays the hard
zero it is under stock: 11/11, 13/13, 9/9 still failing.
- compact_ok repaired to score the buried limit as compact, the behaviour
its name always claimed. It now rescues 100% including bedrooms, and is
kept as the upper-bound control, not a candidate.
- ab_ssz_search.py: the fixed-budget search A/B ssz's acceptance criteria
actually asks for. Every arm is optimised under its own objective and
re-scored under stock urb, because the permissive modes return 1.0
where stock fails and would otherwise win by deleting a fail category.
- ab_crinkliness_mode_ssz.py: prefix rule fixed, retracted premise
flagged in its docstring.
- 38.7's remaining claims from the retracted 38.2/38.3 corrected.
Default is unchanged ("urb"), byte-identical to all prior runs. Lint at
parity (46 pre-existing); tests 366 passed, 10 new, same 7 pre-existing
fixture failures (homemaker-py-bdf).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 16:45:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
ssz: crinkliness is declared per space; there is no daylight attribute
Owner's ruling, and it corrects the design not just the classification: the
daylight requirement is already defined in the crinkliness. The gaussian's
compact side IS "too little exposed wall per unit floor"; its exposed side
is envelope cost. 38.9's proposed daylight: axis was redundant, and keying
it off usage: was worse than redundant.
What was actually missing: crinkliness is the only leaf quality factor with
no per-space target. size, width and proportion are all declared by the
space; crinkliness was one global number for every room in every building.
crinkliness: none -> no minimum-exposure requirement, may be buried
crinkliness: [t, s] -> this space's own target
key absent -> the global uncrinkliness target, as today
`none` clips the factor on the compact side, it does not switch it off:
over-exposure is still penalised, because a crinkly leaf costs envelope
whatever it holds. A store may be buried; a store may not be a starfish.
The mechanism is backward compatible -- an absent key resolves to the
global target, so shipping it changes no score. Behaviour changes only
where a config declares something, which keeps the objective change
visible per programme in config rather than hidden in a default.
Owner's classification: everything a person occupies wants a window, WCs
and reception/waiting/foyer included; only stores, plant, records and
laundry do not. migrate_crinkliness_key.py declared crinkliness: none on 18
corpus spaces. Crinkliness fails 271 -> 243, of which not-defects 136 (50%)
-> 108 (44%); the 28 that went are exactly the utility fails.
usage_daylight and needs_daylight are removed as mis-keyed, and
DAYLIGHT_USAGES with them -- a vocabulary value should exist only where the
engine treats it differently. The historical crinkliness_mode modes stay,
default off, so 38.6/38.8 remain reproducible.
uncrinkliness_circulation is now settable to none like any space, but its
default is left unchanged pending a ruling: corridors were not among the
groups ruled on and are 63% of the remaining phantom fails.
Lint at parity (46); tests 364 passed, same 7 pre-existing fixture failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-28 22:38:54 +00:00
|
|
|
|
def test_declared_crinkliness_none_accepts_the_literal_string():
|
|
|
|
|
|
"""`crinkliness: none` reads the same as a YAML null, so the corpus can
|
|
|
|
|
|
spell it the way it spells `usage: none`."""
|
|
|
|
|
|
f, leaf = _declared_fit(0.0, {"crinkliness": "none"})
|
|
|
|
|
|
assert f.crinkliness_params(leaf) is None
|
|
|
|
|
|
assert f.quality_uncrinkliness(leaf, None, {}) == 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_declared_crinkliness_none_still_penalises_over_exposure():
|
|
|
|
|
|
"""Needing no window is not exemption from envelope cost. The factor is
|
|
|
|
|
|
clipped on the compact side only, never switched off -- a crinkly store
|
|
|
|
|
|
still costs wall."""
|
ssz: daylight is required of rooms that do not need it
DESIGN.md 38.6 concluded the three crinkliness modes were inert against the
circulation-deletion incentive. Two things were wrong with that measurement.
Its premise, 38.2, is retracted. And its script selected leaves with the
pre-39.4 prefix rule `type[:1].upper() in ("C","O")`, which sweeps every
programme room starting with c or o -- cr1, of1 -- in as circulation.
The simpler problem is that none of the three modes ever touched the leaves
ssz is about. quality_uncrinkliness reaches `if not crink` before any mode
logic that matters, so for a zero-exposure leaf: floor returns 0.01 (one
percent of a unit quality, multiplied into a product and weighed against a
whole leaf's cost -- inert); compact_ok is self-contradictory, announcing
that compact is not a defect and then returning the floor for the most
compact case of all; exempt_circulation reaches at most a third of them.
Measured: 0% / 0% / 0% / 21-33% of buried leaves rescued.
What the buried leaves are, now that 39.7 gives every space a usage: two
thirds of them are spaces that architecturally do not want a window --
stores, WCs, plant, corridors, covered courtyards -- scored identically
with a windowless bedroom. harbor 22/33, maple 33/46, health 9/18.
- crinkliness_mode="usage_daylight": daylight required of the uses a
person occupies (programme.DAYLIGHT_USAGES) and nothing else. Elsewhere
the factor is clipped on the compact side only, so being buried stops
being a defect while over-exposure still costs -- a crinkly leaf costs
envelope whatever it is used for. A windowless bedroom stays the hard
zero it is under stock: 11/11, 13/13, 9/9 still failing.
- compact_ok repaired to score the buried limit as compact, the behaviour
its name always claimed. It now rescues 100% including bedrooms, and is
kept as the upper-bound control, not a candidate.
- ab_ssz_search.py: the fixed-budget search A/B ssz's acceptance criteria
actually asks for. Every arm is optimised under its own objective and
re-scored under stock urb, because the permissive modes return 1.0
where stock fails and would otherwise win by deleting a fail category.
- ab_crinkliness_mode_ssz.py: prefix rule fixed, retracted premise
flagged in its docstring.
- 38.7's remaining claims from the retracted 38.2/38.3 corrected.
Default is unchanged ("urb"), byte-identical to all prior runs. Lint at
parity (46 pre-existing); tests 366 passed, 10 new, same 7 pre-existing
fixture failures (homemaker-py-bdf).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 16:45:10 +00:00
|
|
|
|
target = CONF_DEFAULTS["uncrinkliness"][0]
|
ssz: crinkliness is declared per space; there is no daylight attribute
Owner's ruling, and it corrects the design not just the classification: the
daylight requirement is already defined in the crinkliness. The gaussian's
compact side IS "too little exposed wall per unit floor"; its exposed side
is envelope cost. 38.9's proposed daylight: axis was redundant, and keying
it off usage: was worse than redundant.
What was actually missing: crinkliness is the only leaf quality factor with
no per-space target. size, width and proportion are all declared by the
space; crinkliness was one global number for every room in every building.
crinkliness: none -> no minimum-exposure requirement, may be buried
crinkliness: [t, s] -> this space's own target
key absent -> the global uncrinkliness target, as today
`none` clips the factor on the compact side, it does not switch it off:
over-exposure is still penalised, because a crinkly leaf costs envelope
whatever it holds. A store may be buried; a store may not be a starfish.
The mechanism is backward compatible -- an absent key resolves to the
global target, so shipping it changes no score. Behaviour changes only
where a config declares something, which keeps the objective change
visible per programme in config rather than hidden in a default.
Owner's classification: everything a person occupies wants a window, WCs
and reception/waiting/foyer included; only stores, plant, records and
laundry do not. migrate_crinkliness_key.py declared crinkliness: none on 18
corpus spaces. Crinkliness fails 271 -> 243, of which not-defects 136 (50%)
-> 108 (44%); the 28 that went are exactly the utility fails.
usage_daylight and needs_daylight are removed as mis-keyed, and
DAYLIGHT_USAGES with them -- a vocabulary value should exist only where the
engine treats it differently. The historical crinkliness_mode modes stay,
default off, so 38.6/38.8 remain reproducible.
uncrinkliness_circulation is now settable to none like any space, but its
default is left unchanged pending a ruling: corridors were not among the
groups ruled on and are 63% of the remaining phantom fails.
Lint at parity (46); tests 364 passed, same 7 pre-existing fixture failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-28 22:38:54 +00:00
|
|
|
|
f, leaf = _declared_fit(1.0 / (target / 2), {"crinkliness": None})
|
ssz: daylight is required of rooms that do not need it
DESIGN.md 38.6 concluded the three crinkliness modes were inert against the
circulation-deletion incentive. Two things were wrong with that measurement.
Its premise, 38.2, is retracted. And its script selected leaves with the
pre-39.4 prefix rule `type[:1].upper() in ("C","O")`, which sweeps every
programme room starting with c or o -- cr1, of1 -- in as circulation.
The simpler problem is that none of the three modes ever touched the leaves
ssz is about. quality_uncrinkliness reaches `if not crink` before any mode
logic that matters, so for a zero-exposure leaf: floor returns 0.01 (one
percent of a unit quality, multiplied into a product and weighed against a
whole leaf's cost -- inert); compact_ok is self-contradictory, announcing
that compact is not a defect and then returning the floor for the most
compact case of all; exempt_circulation reaches at most a third of them.
Measured: 0% / 0% / 0% / 21-33% of buried leaves rescued.
What the buried leaves are, now that 39.7 gives every space a usage: two
thirds of them are spaces that architecturally do not want a window --
stores, WCs, plant, corridors, covered courtyards -- scored identically
with a windowless bedroom. harbor 22/33, maple 33/46, health 9/18.
- crinkliness_mode="usage_daylight": daylight required of the uses a
person occupies (programme.DAYLIGHT_USAGES) and nothing else. Elsewhere
the factor is clipped on the compact side only, so being buried stops
being a defect while over-exposure still costs -- a crinkly leaf costs
envelope whatever it is used for. A windowless bedroom stays the hard
zero it is under stock: 11/11, 13/13, 9/9 still failing.
- compact_ok repaired to score the buried limit as compact, the behaviour
its name always claimed. It now rescues 100% including bedrooms, and is
kept as the upper-bound control, not a candidate.
- ab_ssz_search.py: the fixed-budget search A/B ssz's acceptance criteria
actually asks for. Every arm is optimised under its own objective and
re-scored under stock urb, because the permissive modes return 1.0
where stock fails and would otherwise win by deleting a fail category.
- ab_crinkliness_mode_ssz.py: prefix rule fixed, retracted premise
flagged in its docstring.
- 38.7's remaining claims from the retracted 38.2/38.3 corrected.
Default is unchanged ("urb"), byte-identical to all prior runs. Lint at
parity (46 pre-existing); tests 366 passed, 10 new, same 7 pre-existing
fixture failures (homemaker-py-bdf).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 16:45:10 +00:00
|
|
|
|
assert f.quality_uncrinkliness(leaf, None, {}) < 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
ssz: crinkliness is declared per space; there is no daylight attribute
Owner's ruling, and it corrects the design not just the classification: the
daylight requirement is already defined in the crinkliness. The gaussian's
compact side IS "too little exposed wall per unit floor"; its exposed side
is envelope cost. 38.9's proposed daylight: axis was redundant, and keying
it off usage: was worse than redundant.
What was actually missing: crinkliness is the only leaf quality factor with
no per-space target. size, width and proportion are all declared by the
space; crinkliness was one global number for every room in every building.
crinkliness: none -> no minimum-exposure requirement, may be buried
crinkliness: [t, s] -> this space's own target
key absent -> the global uncrinkliness target, as today
`none` clips the factor on the compact side, it does not switch it off:
over-exposure is still penalised, because a crinkly leaf costs envelope
whatever it holds. A store may be buried; a store may not be a starfish.
The mechanism is backward compatible -- an absent key resolves to the
global target, so shipping it changes no score. Behaviour changes only
where a config declares something, which keeps the objective change
visible per programme in config rather than hidden in a default.
Owner's classification: everything a person occupies wants a window, WCs
and reception/waiting/foyer included; only stores, plant, records and
laundry do not. migrate_crinkliness_key.py declared crinkliness: none on 18
corpus spaces. Crinkliness fails 271 -> 243, of which not-defects 136 (50%)
-> 108 (44%); the 28 that went are exactly the utility fails.
usage_daylight and needs_daylight are removed as mis-keyed, and
DAYLIGHT_USAGES with them -- a vocabulary value should exist only where the
engine treats it differently. The historical crinkliness_mode modes stay,
default off, so 38.6/38.8 remain reproducible.
uncrinkliness_circulation is now settable to none like any space, but its
default is left unchanged pending a ruling: corridors were not among the
groups ruled on and are 63% of the remaining phantom fails.
Lint at parity (46); tests 364 passed, same 7 pre-existing fixture failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-28 22:38:54 +00:00
|
|
|
|
def test_declared_crinkliness_pair_is_used_verbatim():
|
|
|
|
|
|
"""A space may instead ask for its own target, as it does for size."""
|
|
|
|
|
|
f, leaf = _declared_fit(0.0, {"crinkliness": [2.0, 0.5]})
|
|
|
|
|
|
assert f.crinkliness_params(leaf) == (2.0, 0.5)
|
|
|
|
|
|
assert f.quality_uncrinkliness(leaf, None, {}) == 0.0 # still wants light
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_circulation_target_is_separately_declarable():
|
|
|
|
|
|
"""A generic corridor takes `uncrinkliness_circulation`, and that key can
|
|
|
|
|
|
say `none` -- an internal corridor with no windows is ordinary
|
|
|
|
|
|
architecture, not a failure (this was 63% of the phantom fails, §38.10)."""
|
|
|
|
|
|
f, _ = _declared_fit(0.0, conf_extra={"uncrinkliness_circulation": None})
|
|
|
|
|
|
assert f.crinkliness_params(_leaf("C")) is None
|
|
|
|
|
|
assert f.quality_uncrinkliness(_leaf("C"), None, {}) == 1.0
|
|
|
|
|
|
# a room is untouched by the circulation key
|
|
|
|
|
|
f2, room = _declared_fit(0.0, conf_extra={"uncrinkliness_circulation": None})
|
|
|
|
|
|
assert f2.quality_uncrinkliness(room, None, {}) == 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_circulation_keeps_its_pair_when_declared():
|
|
|
|
|
|
f, _ = _declared_fit(0.0, conf_extra={"uncrinkliness_circulation": [1.0, 0.3]})
|
|
|
|
|
|
assert f.crinkliness_params(_leaf("C")) == (1.0, 0.3)
|
ssz: daylight is required of rooms that do not need it
DESIGN.md 38.6 concluded the three crinkliness modes were inert against the
circulation-deletion incentive. Two things were wrong with that measurement.
Its premise, 38.2, is retracted. And its script selected leaves with the
pre-39.4 prefix rule `type[:1].upper() in ("C","O")`, which sweeps every
programme room starting with c or o -- cr1, of1 -- in as circulation.
The simpler problem is that none of the three modes ever touched the leaves
ssz is about. quality_uncrinkliness reaches `if not crink` before any mode
logic that matters, so for a zero-exposure leaf: floor returns 0.01 (one
percent of a unit quality, multiplied into a product and weighed against a
whole leaf's cost -- inert); compact_ok is self-contradictory, announcing
that compact is not a defect and then returning the floor for the most
compact case of all; exempt_circulation reaches at most a third of them.
Measured: 0% / 0% / 0% / 21-33% of buried leaves rescued.
What the buried leaves are, now that 39.7 gives every space a usage: two
thirds of them are spaces that architecturally do not want a window --
stores, WCs, plant, corridors, covered courtyards -- scored identically
with a windowless bedroom. harbor 22/33, maple 33/46, health 9/18.
- crinkliness_mode="usage_daylight": daylight required of the uses a
person occupies (programme.DAYLIGHT_USAGES) and nothing else. Elsewhere
the factor is clipped on the compact side only, so being buried stops
being a defect while over-exposure still costs -- a crinkly leaf costs
envelope whatever it is used for. A windowless bedroom stays the hard
zero it is under stock: 11/11, 13/13, 9/9 still failing.
- compact_ok repaired to score the buried limit as compact, the behaviour
its name always claimed. It now rescues 100% including bedrooms, and is
kept as the upper-bound control, not a candidate.
- ab_ssz_search.py: the fixed-budget search A/B ssz's acceptance criteria
actually asks for. Every arm is optimised under its own objective and
re-scored under stock urb, because the permissive modes return 1.0
where stock fails and would otherwise win by deleting a fail category.
- ab_crinkliness_mode_ssz.py: prefix rule fixed, retracted premise
flagged in its docstring.
- 38.7's remaining claims from the retracted 38.2/38.3 corrected.
Default is unchanged ("urb"), byte-identical to all prior runs. Lint at
parity (46 pre-existing); tests 366 passed, 10 new, same 7 pre-existing
fixture failures (homemaker-py-bdf).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 16:45:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
§38.2 refinement: connectivity is under-priced ~3x, not just a crinkliness bug
Follow-up measurement corrects the first draft of §38 in two ways.
1. Harbor-house's floor is 15 fails (evolved-3M-nols-3, 1.7M evals), not the
30-40 I quoted from §13.11's 20k-budget runs. Frontage deficit predicts the
COST of solving, not impossibility: ~150x budget gap between a
frontage-short and a frontage-surplus programme. Table corrected.
2. Zero-exposure is only half the mechanism, and not the dominant half.
Splitting the deletion test by lit vs buried shows a WELL-DAYLIT corridor
(q_crink=0.736) is still worth x4.06 to delete. Cause: value_circulation=50
vs value_inside=300, so merging corridor into room is a flat x6 gain, while
'level N not connected' costs only x0.5. Break-even needs 0.5^k < 50/300,
i.e. k > 2.58 -- severing must cost at least 3 fails and costs 1. Net x3.0
predicted, x4.06 measured. The objective is net-positive on severing the
spine even when the circulation is perfectly lit, which explains why both
'level N not connected' fails survive in the best layout after 1.7M evals.
Adds fitness.quality_uncrinkliness crinkliness_mode (EXPERIMENTAL, default
"urb" = stock hard 0.0, byte-identical: 336 passed vs 331 before, same 7
pre-existing fixture failures). A/B harness ab_crinkliness_mode_ssz.py shows
none of the three modes removes the incentive, and the lit column is 3/8 under
every mode including stock -- clean isolation of the two mechanisms.
Filed homemaker-py-2v1 (P0) for the pricing fix; ssz/hxi now depend on it.
Acceptance test recorded up front: harbor must reach 15 fails in materially
fewer than 1.7M evals AND without either not-connected fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 07:40:37 +00:00
|
|
|
|
def test_crinkliness_mode_unknown_raises():
|
|
|
|
|
|
with pytest.raises(ValueError, match="crinkliness_mode"):
|
|
|
|
|
|
_stub_fit("nonsense")
|
§39.8: homemaker-py-2v1 connectivity weighting — MEASURED NULL, premise retracted
§38.2 concluded the objective is net-positive on severing a level's
circulation: merging a corridor into a habitable sibling gains x6
(value_inside/value_circulation), while "level N not connected" costs x0.5, so
break-even needs 0.5^w < 50/300, w > 2.58 -- "severing must cost at least 3
fails and costs 1". The arithmetic is right. The premise is wrong.
Shipped anyway, EXPERIMENTAL and default off (byte-identical):
fitness.connectivity_weight_for(value_inside, value_circulation) returns the
smallest weight making severing net-negative -- 3.0 at the defaults, DERIVED
from the rates rather than hard-coded so it tracks them if either is retuned.
conf["connectivity_weight"] takes 1.0 / "auto" / a number and counts each
connectivity failure as w failures in the 0.5^n penalty.
MEASUREMENT: at auto (=3) the §38.2 deletion test does not move at all -- 5/25
rewarded either way, median x0.26 vs x0.27. Reason: the connectivity fail count
is UNCHANGED in every rewarded deletion (115->107 fails but 5->5 connectivity;
107->99 but 3->3; 78->71 but 3->3). Weighting a fail that never fires changes
nothing.
And when a deletion DOES break connectivity, it is already punished. Every such
case, 4 seeds per programme: harbor-house 2 of 32 sampled deletions, both
punished (x0.00, x0.01); maple-court 5 of 32, all punished (x0.58 .. x0.07).
Severing costs 1-2 connectivity fails PLUS the cascade after them, which
already outweighs the x6 gain. The flat rule was never the problem.
Where §38.2 went wrong: the x4.06 "well-daylit circulation leaf" that motivated
the bead was a deletion that did NOT change the connectivity fail count. It was
rewarded for removing the leaf's own quality failures -- §38.1's zero-value
finding -- and I misread it as a pricing mechanism. §38.2 now carries the
retraction inline. Two lessons recorded: a plausible closed-form arithmetic is
not a measurement, and when a fix produces exactly no effect, suspect the
premise before the implementation.
Still standing from §38: §38.1 (buried leaves score zero quality and contribute
no value) and §38.3 (frontage budget) are direct measurements. §39.7 remains
the better lever on the same symptom -- it made the connectivity fails FIRE,
where this would only have made them cost more.
Re-opened as homemaker-py-yql: why level-not-connected persists in the best
layout when severing is already punished. Evidence now points at reachability,
not incentive, and it is newly measurable because §39.7 stopped store cupboards
standing in for corridors.
353 passed (+3 new), same 7 pre-existing fixture failures, lint unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 14:15:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
# homemaker-py-2v1 / DESIGN.md §39.8 — connectivity_weight (EXPERIMENTAL, NULL)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
def test_connectivity_weight_defaults_to_flat_rule():
|
|
|
|
|
|
"""Default must reproduce the flat 0.5^n penalty exactly."""
|
|
|
|
|
|
assert Fitness(conf={})._connectivity_weight == 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_connectivity_weight_auto_is_derived_from_the_value_gap():
|
|
|
|
|
|
"""Not a magic number: the smallest w making 0.5^w < value_circulation /
|
|
|
|
|
|
value_inside, so it tracks the rates if either is retuned."""
|
|
|
|
|
|
from homemaker_layout.fitness import connectivity_weight_for
|
|
|
|
|
|
assert connectivity_weight_for(300.0, 50.0) == 3.0 # 0.5^3 < 1/6 < 0.5^2
|
|
|
|
|
|
assert connectivity_weight_for(100.0, 100.0) == 1.0 # no gap, no extra weight
|
|
|
|
|
|
assert connectivity_weight_for(400.0, 50.0) == 3.0 # 1/8 -> exactly 3
|
|
|
|
|
|
assert Fitness(conf={"connectivity_weight": "auto"})._connectivity_weight == 3.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_is_connectivity_fail_matches_both_strings():
|
|
|
|
|
|
from homemaker_layout.fitness import is_connectivity_fail
|
|
|
|
|
|
assert is_connectivity_fail("level 0 not connected")
|
|
|
|
|
|
assert is_connectivity_fail("1 inaccessible usable space")
|
|
|
|
|
|
assert not is_connectivity_fail("0/llr crinkliness")
|
|
|
|
|
|
assert not is_connectivity_fail("missing required space: b1")
|