2026-08-03 11:03:02 +01:00
|
|
|
"""homemaker-py-2g7.1: plan->dom composer tests.
|
|
|
|
|
|
|
|
|
|
Synthetic fixtures only -- no real human trace exists yet (see DESIGN.md sec
|
|
|
|
|
37.x). These exercise the two acceptance-criteria halves: a slicible
|
|
|
|
|
partition round-trips to a scoring .dom, and a non-slicible one is reported
|
|
|
|
|
with the offending region rather than mis-parsed.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import textwrap
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from homemaker_layout import dom, geometry
|
|
|
|
|
from homemaker_layout.compose import LabelError, NonSlicible, StoreyTrace, compose, parse_svg
|
|
|
|
|
|
|
|
|
|
BOUNDARY_YAML = textwrap.dedent(
|
|
|
|
|
"""\
|
|
|
|
|
node: [[0.0, 0.0], [10.0, 0.0], [10.0, 8.0], [0.0, 8.0]]
|
|
|
|
|
perimeter: {a: null, b: null, c: null, d: null}
|
|
|
|
|
height: 3.0
|
|
|
|
|
elevation: 0.0
|
|
|
|
|
wall_inner: 0.08
|
|
|
|
|
wall_outer: 0.25
|
|
|
|
|
rotation: 0
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Plot is 10x8. Cut 1 (axis 0, vertical) at x=4 splits into a left column
|
|
|
|
|
# (x:0-4, full height) and a right column (x:4-10). Cut 2 (axis 1,
|
|
|
|
|
# horizontal) at y=5 splits the right column into a bottom room (y:0-5) and
|
|
|
|
|
# a top room (y:5-8) -- exercises both axes and depth-2 recursion.
|
|
|
|
|
GOOD_SVG = textwrap.dedent(
|
|
|
|
|
"""\
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
|
|
|
|
|
<g inkscape:groupmode="layer" inkscape:label="storey-0">
|
|
|
|
|
<path d="M 4,0 L 4,8"/>
|
|
|
|
|
<path d="M 4,5 L 10,5"/>
|
ju3: reject programme codes colliding with the c/o/s generic type prefixes
Closes homemaker-py-ju3. DESIGN.md §39.3.
The class: key from the bead's design was deliberately NOT built. Auditing
every use of the prefix rule first showed it runs deeper than c/o/s -- l/k/b/t
carry real adjacency semantics (graph.py builds bedroom<->toilet and
kitchen<->living relations from first characters) -- so re-plumbing the type
system would invalidate the whole corpus and every baseline, for a problem
whose damage is the silence, not the convention. Two findings made the smaller
fix sufficient: no corpus programme has ever declared a bare c/o/s code, so
check_space_counts' skip only ever discarded declared rooms; and nothing
references harbor's four codes in any adjacency or co_locate list.
- programme.validate_codes raises on a reserved-prefix code, with the full
explanation. Called from BOTH parse paths (programme._parse_spaces and
fitness.Fitness._load_programme parse conf["spaces"] independently, so
validating one would leave the other door open). l/k/b/t stay unreserved.
- harbor-house and harbor-house-l0 renamed: cr1->fr1, of->ao, st1->gs1,
st2->gs2. New prefixes are unused in harbor and semantically neutral, and
the two storage codes still share a prefix, preserving the structure
evaluate_building's per-code plot-ratio term depends on. name: unchanged.
- experiments/migrate_ju3_rename.py migrates .dom files written before the
rename (--check dry-runs). Pre-rename artefacts, notably evolved-3M*.dom,
must be migrated or their leaves read as unmatched generics.
- test_collapse_global's c/o/s exclusion test now uses a generic C leaf, which
is what the exclusion is actually for; it previously relied on a programme
code colliding, which is no longer possible.
Re-baseline (seed 1, 20k evals, same settings as §38's run): 57 fails against
the 32-instance effective programme -> 55 against the real 37-instance one,
with all five previously-lost room instances now placed inside their declared
sigma bands (fr1 87.2 vs declared 80, was 32.9/17.1; ao/gs1/gs2 were absent
entirely) and no failure naming any of the four codes. At one seed each,
57 vs 55 is within noise -- the robust result is the room placement, not the
count. Historical harbor numbers are not comparable to post-ju3 ones; filed
homemaker-py-t3s to restate 2v1's acceptance figure once evolved-3M is
migrated.
346 passed (+10 new), same 7 pre-existing fixture failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 09:06:08 +00:00
|
|
|
<text x="2" y="4">fr1</text>
|
2026-08-03 11:03:02 +01:00
|
|
|
<text x="7" y="2.5">k1</text>
|
|
|
|
|
<text x="7" y="6.5">b1</text>
|
|
|
|
|
</g>
|
|
|
|
|
</svg>
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Same partition, endpoints perturbed by < 0.15 (default tol) to exercise
|
|
|
|
|
# snapping: a hand-drawn line that overlaps/undershoots slightly.
|
|
|
|
|
SLOPPY_SVG = textwrap.dedent(
|
|
|
|
|
"""\
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
|
|
|
|
|
<g inkscape:groupmode="layer" inkscape:label="storey-0">
|
|
|
|
|
<path d="M 4.06,-0.05 L 3.95,8.07"/>
|
|
|
|
|
<path d="M 3.96,5.04 L 10.06,4.93"/>
|
ju3: reject programme codes colliding with the c/o/s generic type prefixes
Closes homemaker-py-ju3. DESIGN.md §39.3.
The class: key from the bead's design was deliberately NOT built. Auditing
every use of the prefix rule first showed it runs deeper than c/o/s -- l/k/b/t
carry real adjacency semantics (graph.py builds bedroom<->toilet and
kitchen<->living relations from first characters) -- so re-plumbing the type
system would invalidate the whole corpus and every baseline, for a problem
whose damage is the silence, not the convention. Two findings made the smaller
fix sufficient: no corpus programme has ever declared a bare c/o/s code, so
check_space_counts' skip only ever discarded declared rooms; and nothing
references harbor's four codes in any adjacency or co_locate list.
- programme.validate_codes raises on a reserved-prefix code, with the full
explanation. Called from BOTH parse paths (programme._parse_spaces and
fitness.Fitness._load_programme parse conf["spaces"] independently, so
validating one would leave the other door open). l/k/b/t stay unreserved.
- harbor-house and harbor-house-l0 renamed: cr1->fr1, of->ao, st1->gs1,
st2->gs2. New prefixes are unused in harbor and semantically neutral, and
the two storage codes still share a prefix, preserving the structure
evaluate_building's per-code plot-ratio term depends on. name: unchanged.
- experiments/migrate_ju3_rename.py migrates .dom files written before the
rename (--check dry-runs). Pre-rename artefacts, notably evolved-3M*.dom,
must be migrated or their leaves read as unmatched generics.
- test_collapse_global's c/o/s exclusion test now uses a generic C leaf, which
is what the exclusion is actually for; it previously relied on a programme
code colliding, which is no longer possible.
Re-baseline (seed 1, 20k evals, same settings as §38's run): 57 fails against
the 32-instance effective programme -> 55 against the real 37-instance one,
with all five previously-lost room instances now placed inside their declared
sigma bands (fr1 87.2 vs declared 80, was 32.9/17.1; ao/gs1/gs2 were absent
entirely) and no failure naming any of the four codes. At one seed each,
57 vs 55 is within noise -- the robust result is the room placement, not the
count. Historical harbor numbers are not comparable to post-ju3 ones; filed
homemaker-py-t3s to restate 2v1's acceptance figure once evolved-3M is
migrated.
346 passed (+10 new), same 7 pre-existing fixture failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 09:06:08 +00:00
|
|
|
<text x="2" y="4">fr1</text>
|
2026-08-03 11:03:02 +01:00
|
|
|
<text x="7" y="2.5">k1</text>
|
|
|
|
|
<text x="7" y="6.5">b1</text>
|
|
|
|
|
</g>
|
|
|
|
|
</svg>
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# One dangling interior line that touches neither pair of opposite edges --
|
|
|
|
|
# not a guillotine cut of the plot, and there is no other line to try.
|
|
|
|
|
NON_SLICIBLE_SVG = textwrap.dedent(
|
|
|
|
|
"""\
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
|
|
|
|
|
<g inkscape:groupmode="layer" inkscape:label="storey-0">
|
|
|
|
|
<path d="M 3,3 L 7,3"/>
|
|
|
|
|
</g>
|
|
|
|
|
</svg>
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _write(tmp_path, name, content):
|
|
|
|
|
p = tmp_path / name
|
|
|
|
|
p.write_text(content)
|
|
|
|
|
return p
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_composes_synthetic_partition_and_scores(tmp_path):
|
|
|
|
|
boundary_path = _write(tmp_path, "boundary.dom", BOUNDARY_YAML)
|
|
|
|
|
svg_path = _write(tmp_path, "plan.svg", GOOD_SVG)
|
|
|
|
|
|
|
|
|
|
boundary = dom.load(str(boundary_path))
|
|
|
|
|
storeys = parse_svg(str(svg_path))
|
|
|
|
|
root = compose(boundary, storeys)
|
|
|
|
|
|
|
|
|
|
leaves = root.leaves()
|
ju3: reject programme codes colliding with the c/o/s generic type prefixes
Closes homemaker-py-ju3. DESIGN.md §39.3.
The class: key from the bead's design was deliberately NOT built. Auditing
every use of the prefix rule first showed it runs deeper than c/o/s -- l/k/b/t
carry real adjacency semantics (graph.py builds bedroom<->toilet and
kitchen<->living relations from first characters) -- so re-plumbing the type
system would invalidate the whole corpus and every baseline, for a problem
whose damage is the silence, not the convention. Two findings made the smaller
fix sufficient: no corpus programme has ever declared a bare c/o/s code, so
check_space_counts' skip only ever discarded declared rooms; and nothing
references harbor's four codes in any adjacency or co_locate list.
- programme.validate_codes raises on a reserved-prefix code, with the full
explanation. Called from BOTH parse paths (programme._parse_spaces and
fitness.Fitness._load_programme parse conf["spaces"] independently, so
validating one would leave the other door open). l/k/b/t stay unreserved.
- harbor-house and harbor-house-l0 renamed: cr1->fr1, of->ao, st1->gs1,
st2->gs2. New prefixes are unused in harbor and semantically neutral, and
the two storage codes still share a prefix, preserving the structure
evaluate_building's per-code plot-ratio term depends on. name: unchanged.
- experiments/migrate_ju3_rename.py migrates .dom files written before the
rename (--check dry-runs). Pre-rename artefacts, notably evolved-3M*.dom,
must be migrated or their leaves read as unmatched generics.
- test_collapse_global's c/o/s exclusion test now uses a generic C leaf, which
is what the exclusion is actually for; it previously relied on a programme
code colliding, which is no longer possible.
Re-baseline (seed 1, 20k evals, same settings as §38's run): 57 fails against
the 32-instance effective programme -> 55 against the real 37-instance one,
with all five previously-lost room instances now placed inside their declared
sigma bands (fr1 87.2 vs declared 80, was 32.9/17.1; ao/gs1/gs2 were absent
entirely) and no failure naming any of the four codes. At one seed each,
57 vs 55 is within noise -- the robust result is the room placement, not the
count. Historical harbor numbers are not comparable to post-ju3 ones; filed
homemaker-py-t3s to restate 2v1's acceptance figure once evolved-3M is
migrated.
346 passed (+10 new), same 7 pre-existing fixture failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 09:06:08 +00:00
|
|
|
assert sorted(leaf.type for leaf in leaves) == ["b1", "fr1", "k1"]
|
2026-08-03 11:03:02 +01:00
|
|
|
|
|
|
|
|
# round-trips through the .dom text format
|
|
|
|
|
out_path = tmp_path / "plan.dom"
|
|
|
|
|
dom.dump(root, str(out_path))
|
|
|
|
|
reloaded = dom.load(str(out_path))
|
|
|
|
|
reloaded_types = sorted(leaf.type for leaf in reloaded.leaves())
|
ju3: reject programme codes colliding with the c/o/s generic type prefixes
Closes homemaker-py-ju3. DESIGN.md §39.3.
The class: key from the bead's design was deliberately NOT built. Auditing
every use of the prefix rule first showed it runs deeper than c/o/s -- l/k/b/t
carry real adjacency semantics (graph.py builds bedroom<->toilet and
kitchen<->living relations from first characters) -- so re-plumbing the type
system would invalidate the whole corpus and every baseline, for a problem
whose damage is the silence, not the convention. Two findings made the smaller
fix sufficient: no corpus programme has ever declared a bare c/o/s code, so
check_space_counts' skip only ever discarded declared rooms; and nothing
references harbor's four codes in any adjacency or co_locate list.
- programme.validate_codes raises on a reserved-prefix code, with the full
explanation. Called from BOTH parse paths (programme._parse_spaces and
fitness.Fitness._load_programme parse conf["spaces"] independently, so
validating one would leave the other door open). l/k/b/t stay unreserved.
- harbor-house and harbor-house-l0 renamed: cr1->fr1, of->ao, st1->gs1,
st2->gs2. New prefixes are unused in harbor and semantically neutral, and
the two storage codes still share a prefix, preserving the structure
evaluate_building's per-code plot-ratio term depends on. name: unchanged.
- experiments/migrate_ju3_rename.py migrates .dom files written before the
rename (--check dry-runs). Pre-rename artefacts, notably evolved-3M*.dom,
must be migrated or their leaves read as unmatched generics.
- test_collapse_global's c/o/s exclusion test now uses a generic C leaf, which
is what the exclusion is actually for; it previously relied on a programme
code colliding, which is no longer possible.
Re-baseline (seed 1, 20k evals, same settings as §38's run): 57 fails against
the 32-instance effective programme -> 55 against the real 37-instance one,
with all five previously-lost room instances now placed inside their declared
sigma bands (fr1 87.2 vs declared 80, was 32.9/17.1; ao/gs1/gs2 were absent
entirely) and no failure naming any of the four codes. At one seed each,
57 vs 55 is within noise -- the robust result is the room placement, not the
count. Historical harbor numbers are not comparable to post-ju3 ones; filed
homemaker-py-t3s to restate 2v1's acceptance figure once evolved-3M is
migrated.
346 passed (+10 new), same 7 pre-existing fixture failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 09:06:08 +00:00
|
|
|
assert reloaded_types == ["b1", "fr1", "k1"]
|
2026-08-03 11:03:02 +01:00
|
|
|
|
|
|
|
|
# geometry is sane: leaf areas sum to the (wall-inset) plot area
|
|
|
|
|
total = sum(geometry.area(leaf) for leaf in reloaded.leaves())
|
|
|
|
|
assert total == pytest.approx(geometry.area(reloaded), rel=1e-9)
|
|
|
|
|
|
|
|
|
|
# scores cleanly through the native fitness engine (no config on disk:
|
|
|
|
|
# unconstrained score, just confirms it runs end-to-end without raising)
|
|
|
|
|
from homemaker_layout.fitness import Fitness
|
|
|
|
|
|
|
|
|
|
fitness = Fitness({}, {})
|
|
|
|
|
score, failures = fitness.score_with_fails(reloaded)
|
|
|
|
|
assert isinstance(score, float)
|
|
|
|
|
assert len(failures) > 0 # unconstrained rooms + no programme: expected fails
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_snaps_sloppy_hand_traced_lines(tmp_path):
|
|
|
|
|
boundary_path = _write(tmp_path, "boundary.dom", BOUNDARY_YAML)
|
|
|
|
|
svg_path = _write(tmp_path, "plan.svg", SLOPPY_SVG)
|
|
|
|
|
|
|
|
|
|
boundary = dom.load(str(boundary_path))
|
|
|
|
|
storeys = parse_svg(str(svg_path))
|
|
|
|
|
root = compose(boundary, storeys, tol=0.15)
|
|
|
|
|
|
ju3: reject programme codes colliding with the c/o/s generic type prefixes
Closes homemaker-py-ju3. DESIGN.md §39.3.
The class: key from the bead's design was deliberately NOT built. Auditing
every use of the prefix rule first showed it runs deeper than c/o/s -- l/k/b/t
carry real adjacency semantics (graph.py builds bedroom<->toilet and
kitchen<->living relations from first characters) -- so re-plumbing the type
system would invalidate the whole corpus and every baseline, for a problem
whose damage is the silence, not the convention. Two findings made the smaller
fix sufficient: no corpus programme has ever declared a bare c/o/s code, so
check_space_counts' skip only ever discarded declared rooms; and nothing
references harbor's four codes in any adjacency or co_locate list.
- programme.validate_codes raises on a reserved-prefix code, with the full
explanation. Called from BOTH parse paths (programme._parse_spaces and
fitness.Fitness._load_programme parse conf["spaces"] independently, so
validating one would leave the other door open). l/k/b/t stay unreserved.
- harbor-house and harbor-house-l0 renamed: cr1->fr1, of->ao, st1->gs1,
st2->gs2. New prefixes are unused in harbor and semantically neutral, and
the two storage codes still share a prefix, preserving the structure
evaluate_building's per-code plot-ratio term depends on. name: unchanged.
- experiments/migrate_ju3_rename.py migrates .dom files written before the
rename (--check dry-runs). Pre-rename artefacts, notably evolved-3M*.dom,
must be migrated or their leaves read as unmatched generics.
- test_collapse_global's c/o/s exclusion test now uses a generic C leaf, which
is what the exclusion is actually for; it previously relied on a programme
code colliding, which is no longer possible.
Re-baseline (seed 1, 20k evals, same settings as §38's run): 57 fails against
the 32-instance effective programme -> 55 against the real 37-instance one,
with all five previously-lost room instances now placed inside their declared
sigma bands (fr1 87.2 vs declared 80, was 32.9/17.1; ao/gs1/gs2 were absent
entirely) and no failure naming any of the four codes. At one seed each,
57 vs 55 is within noise -- the robust result is the room placement, not the
count. Historical harbor numbers are not comparable to post-ju3 ones; filed
homemaker-py-t3s to restate 2v1's acceptance figure once evolved-3M is
migrated.
346 passed (+10 new), same 7 pre-existing fixture failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 09:06:08 +00:00
|
|
|
assert sorted(leaf.type for leaf in root.leaves()) == ["b1", "fr1", "k1"]
|
2026-08-03 11:03:02 +01:00
|
|
|
|
|
|
|
|
# a tighter tolerance than the sketch's slop should fail to find the cuts
|
|
|
|
|
boundary2 = dom.load(str(boundary_path))
|
|
|
|
|
with pytest.raises((NonSlicible, LabelError)):
|
|
|
|
|
compose(boundary2, storeys, tol=0.01)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_non_slicible_region_is_reported(tmp_path):
|
|
|
|
|
boundary_path = _write(tmp_path, "boundary.dom", BOUNDARY_YAML)
|
|
|
|
|
svg_path = _write(tmp_path, "plan.svg", NON_SLICIBLE_SVG)
|
|
|
|
|
|
|
|
|
|
boundary = dom.load(str(boundary_path))
|
|
|
|
|
storeys = parse_svg(str(svg_path))
|
|
|
|
|
|
|
|
|
|
with pytest.raises(NonSlicible) as excinfo:
|
|
|
|
|
compose(boundary, storeys)
|
|
|
|
|
|
|
|
|
|
exc = excinfo.value
|
|
|
|
|
assert exc.storey == 0
|
|
|
|
|
# names the offending region: should be the whole plot (wall-inset
|
|
|
|
|
# corners), since the dangling line doesn't localise to a sub-quad
|
|
|
|
|
assert len(exc.corners) == 4
|
|
|
|
|
assert "region around" in str(exc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_label_count_mismatch_is_reported(tmp_path):
|
|
|
|
|
svg = textwrap.dedent(
|
|
|
|
|
"""\
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
|
|
|
|
|
<g inkscape:groupmode="layer" inkscape:label="storey-0">
|
|
|
|
|
</g>
|
|
|
|
|
</svg>
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
boundary_path = _write(tmp_path, "boundary.dom", BOUNDARY_YAML)
|
|
|
|
|
svg_path = _write(tmp_path, "plan.svg", svg)
|
|
|
|
|
|
|
|
|
|
boundary = dom.load(str(boundary_path))
|
|
|
|
|
storeys = parse_svg(str(svg_path))
|
|
|
|
|
|
|
|
|
|
with pytest.raises(LabelError) as excinfo:
|
|
|
|
|
compose(boundary, storeys)
|
|
|
|
|
assert excinfo.value.storey == 0
|
|
|
|
|
assert excinfo.value.labels == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_svg_rejects_missing_storey_layers(tmp_path):
|
|
|
|
|
svg = textwrap.dedent(
|
|
|
|
|
"""\
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
|
|
|
|
|
<g inkscape:groupmode="layer" inkscape:label="not-a-storey"/>
|
|
|
|
|
</svg>
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
svg_path = _write(tmp_path, "plan.svg", svg)
|
|
|
|
|
with pytest.raises(ValueError):
|
|
|
|
|
parse_svg(str(svg_path))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_compose_rejects_storey_count_mismatch(tmp_path):
|
|
|
|
|
boundary_path = _write(tmp_path, "boundary.dom", BOUNDARY_YAML)
|
|
|
|
|
boundary = dom.load(str(boundary_path))
|
|
|
|
|
with pytest.raises(ValueError):
|
|
|
|
|
compose(boundary, [StoreyTrace(), StoreyTrace()])
|