homemaker-layout/tests/test_programme.py

173 lines
7.5 KiB
Python
Raw Normal View History

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
"""Tests for programme.py parsing and validation."""
from pathlib import Path
import pytest
§39.4: tighten generic-type matching, reverting the harbor rename Supersedes the previous commit's approach. Renaming harbor's four colliding codes fixed one programme; tightening the matching rule fixes the rule, so a room may be called anything. cr1/of/st1/st2 are restored and the examples are byte-identical to their pre-§39 state -- which also means existing .dom artefacts (evolved-3M*) stay valid, so migrate_ju3_rename.py is deleted. The rule: Urb has exactly three GENERIC structural types (get_space_types: qw/C O S/), the leaves the search creates. Measured across the corpus: 154 C, 110 O, 1 S, not one lowercase generic -- while every programme code is lowercase, including single-character ones (r, t, m, n). Case is the discriminator, not length. Every generic test was type[0].lower() in (...), a case-insensitive PREFIX that swept up any programme code starting with those letters; they now match the generic set exactly. 30 sites across dom, fitness, graph, operators, programme, shapecurve and bubble. NOT applied to the SEMANTIC prefixes: l/k/b/t classify programme codes by first letter (graph.py builds bedroom<->toilet and kitchen<->living relations from them) and stay prefix-based. Where the namespaces were mixed in one expression they were split -- has_circulation's ("b","l","k","c") is three semantic prefixes plus dom.is_circulation; access()'s ("l","c","s") is semantic l plus the generic circulation set. New: dom.GENERIC_{CIRCULATION,OUTSIDE,TYPES} + is_generic(); fitness. _generic_class(), replacing the _t0 dispatch in quality_size/quality_width/ quality_proportion/value_rate -- the four terms that mattered most and that a first sweep missed, since they dispatch through a t0 variable rather than an inline test. graph._adjacency_target resolves a generic adjacency requirement (programmes write "adjacency: [c, o]") to the generic set while every other requirement keeps Perl's prefix semantics. Two subtleties: S is in both generic sets but takes the OUTSIDE parameter families -- a first translation tested circulation first and silently gave S the circulation params, caught by test_get_space_params_sahn_proportion. And validate_codes survives, narrowed to a code spelled exactly C/O/S, which is a genuine ambiguity; merely starting with c/o/s is now fine. Invariant asserted as a test: test_scoring_is_invariant_under_programme_code_ spelling relabels one tree and its config together and re-scores. Bit-identical across 12 comparisons (6 seeds x collapse on/off). Re-baseline (seed 1, 20k, original names): 58 fails (15 hard / 43 soft) against the real 37-instance programme, with cr1 at 79.1 m2 vs declared 80 (was 32.9 and 17.1), of/st1/st2 all present and in band, and one fail naming any of them. 57 -> 58 on a 5-instance-harder programme is within noise: "did not regress". Fallout (§39.5): 2g7.5's CP-SAT seeder win does not survive. Over 6 seeds -- harbor real 102/114 (cpsat loses), harbor old-effective 98/99 (tie, so the win was already marginal), maple-court 156/144 (cpsat wins). maple is the control: the solver did not regress, harbor's programme changed. Test xfail'd with that reason plus a maple companion; both assign_solver flags stay default off. Filed homemaker-py-w6x to re-check other narrow-margin harbor A/Bs. 345 passed, 1 xfailed, same 7 pre-existing fixture failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 09:45:28 +00:00
from homemaker_layout import dom, fitness, programme
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
# --------------------------------------------------------------------------- #
# homemaker-py-ju3 / DESIGN.md §39.2 — reserved generic type prefixes
# --------------------------------------------------------------------------- #
§39.4: tighten generic-type matching, reverting the harbor rename Supersedes the previous commit's approach. Renaming harbor's four colliding codes fixed one programme; tightening the matching rule fixes the rule, so a room may be called anything. cr1/of/st1/st2 are restored and the examples are byte-identical to their pre-§39 state -- which also means existing .dom artefacts (evolved-3M*) stay valid, so migrate_ju3_rename.py is deleted. The rule: Urb has exactly three GENERIC structural types (get_space_types: qw/C O S/), the leaves the search creates. Measured across the corpus: 154 C, 110 O, 1 S, not one lowercase generic -- while every programme code is lowercase, including single-character ones (r, t, m, n). Case is the discriminator, not length. Every generic test was type[0].lower() in (...), a case-insensitive PREFIX that swept up any programme code starting with those letters; they now match the generic set exactly. 30 sites across dom, fitness, graph, operators, programme, shapecurve and bubble. NOT applied to the SEMANTIC prefixes: l/k/b/t classify programme codes by first letter (graph.py builds bedroom<->toilet and kitchen<->living relations from them) and stay prefix-based. Where the namespaces were mixed in one expression they were split -- has_circulation's ("b","l","k","c") is three semantic prefixes plus dom.is_circulation; access()'s ("l","c","s") is semantic l plus the generic circulation set. New: dom.GENERIC_{CIRCULATION,OUTSIDE,TYPES} + is_generic(); fitness. _generic_class(), replacing the _t0 dispatch in quality_size/quality_width/ quality_proportion/value_rate -- the four terms that mattered most and that a first sweep missed, since they dispatch through a t0 variable rather than an inline test. graph._adjacency_target resolves a generic adjacency requirement (programmes write "adjacency: [c, o]") to the generic set while every other requirement keeps Perl's prefix semantics. Two subtleties: S is in both generic sets but takes the OUTSIDE parameter families -- a first translation tested circulation first and silently gave S the circulation params, caught by test_get_space_params_sahn_proportion. And validate_codes survives, narrowed to a code spelled exactly C/O/S, which is a genuine ambiguity; merely starting with c/o/s is now fine. Invariant asserted as a test: test_scoring_is_invariant_under_programme_code_ spelling relabels one tree and its config together and re-scores. Bit-identical across 12 comparisons (6 seeds x collapse on/off). Re-baseline (seed 1, 20k, original names): 58 fails (15 hard / 43 soft) against the real 37-instance programme, with cr1 at 79.1 m2 vs declared 80 (was 32.9 and 17.1), of/st1/st2 all present and in band, and one fail naming any of them. 57 -> 58 on a 5-instance-harder programme is within noise: "did not regress". Fallout (§39.5): 2g7.5's CP-SAT seeder win does not survive. Over 6 seeds -- harbor real 102/114 (cpsat loses), harbor old-effective 98/99 (tie, so the win was already marginal), maple-court 156/144 (cpsat wins). maple is the control: the solver did not regress, harbor's programme changed. Test xfail'd with that reason plus a maple companion; both assign_solver flags stay default off. Filed homemaker-py-w6x to re-check other narrow-margin harbor A/Bs. 345 passed, 1 xfailed, same 7 pre-existing fixture failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 09:45:28 +00:00
def test_validate_codes_accepts_codes_that_merely_start_with_c_o_s():
"""§39.4: the generic-type tests match C/O/S EXACTLY, so a programme code
may start with any letter. This used to raise that was the bug, not the
rule."""
programme.validate_codes(["cr1", "of", "st1", "st2", "b1", "k1", "la1"])
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
§39.4: tighten generic-type matching, reverting the harbor rename Supersedes the previous commit's approach. Renaming harbor's four colliding codes fixed one programme; tightening the matching rule fixes the rule, so a room may be called anything. cr1/of/st1/st2 are restored and the examples are byte-identical to their pre-§39 state -- which also means existing .dom artefacts (evolved-3M*) stay valid, so migrate_ju3_rename.py is deleted. The rule: Urb has exactly three GENERIC structural types (get_space_types: qw/C O S/), the leaves the search creates. Measured across the corpus: 154 C, 110 O, 1 S, not one lowercase generic -- while every programme code is lowercase, including single-character ones (r, t, m, n). Case is the discriminator, not length. Every generic test was type[0].lower() in (...), a case-insensitive PREFIX that swept up any programme code starting with those letters; they now match the generic set exactly. 30 sites across dom, fitness, graph, operators, programme, shapecurve and bubble. NOT applied to the SEMANTIC prefixes: l/k/b/t classify programme codes by first letter (graph.py builds bedroom<->toilet and kitchen<->living relations from them) and stay prefix-based. Where the namespaces were mixed in one expression they were split -- has_circulation's ("b","l","k","c") is three semantic prefixes plus dom.is_circulation; access()'s ("l","c","s") is semantic l plus the generic circulation set. New: dom.GENERIC_{CIRCULATION,OUTSIDE,TYPES} + is_generic(); fitness. _generic_class(), replacing the _t0 dispatch in quality_size/quality_width/ quality_proportion/value_rate -- the four terms that mattered most and that a first sweep missed, since they dispatch through a t0 variable rather than an inline test. graph._adjacency_target resolves a generic adjacency requirement (programmes write "adjacency: [c, o]") to the generic set while every other requirement keeps Perl's prefix semantics. Two subtleties: S is in both generic sets but takes the OUTSIDE parameter families -- a first translation tested circulation first and silently gave S the circulation params, caught by test_get_space_params_sahn_proportion. And validate_codes survives, narrowed to a code spelled exactly C/O/S, which is a genuine ambiguity; merely starting with c/o/s is now fine. Invariant asserted as a test: test_scoring_is_invariant_under_programme_code_ spelling relabels one tree and its config together and re-scores. Bit-identical across 12 comparisons (6 seeds x collapse on/off). Re-baseline (seed 1, 20k, original names): 58 fails (15 hard / 43 soft) against the real 37-instance programme, with cr1 at 79.1 m2 vs declared 80 (was 32.9 and 17.1), of/st1/st2 all present and in band, and one fail naming any of them. 57 -> 58 on a 5-instance-harder programme is within noise: "did not regress". Fallout (§39.5): 2g7.5's CP-SAT seeder win does not survive. Over 6 seeds -- harbor real 102/114 (cpsat loses), harbor old-effective 98/99 (tie, so the win was already marginal), maple-court 156/144 (cpsat wins). maple is the control: the solver did not regress, harbor's programme changed. Test xfail'd with that reason plus a maple companion; both assign_solver flags stay default off. Filed homemaker-py-w6x to re-check other narrow-margin harbor A/Bs. 345 passed, 1 xfailed, same 7 pre-existing fixture failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 09:45:28 +00:00
@pytest.mark.parametrize("code", ["C", "O", "S"])
def test_validate_codes_rejects_exact_generic_types(code):
"""A code spelled exactly like a generic structural type is a genuine
ambiguity no matching rule can resolve, so it still fails loudly."""
with pytest.raises(ValueError, match="generic structural types"):
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
programme.validate_codes([code])
§39.4: tighten generic-type matching, reverting the harbor rename Supersedes the previous commit's approach. Renaming harbor's four colliding codes fixed one programme; tightening the matching rule fixes the rule, so a room may be called anything. cr1/of/st1/st2 are restored and the examples are byte-identical to their pre-§39 state -- which also means existing .dom artefacts (evolved-3M*) stay valid, so migrate_ju3_rename.py is deleted. The rule: Urb has exactly three GENERIC structural types (get_space_types: qw/C O S/), the leaves the search creates. Measured across the corpus: 154 C, 110 O, 1 S, not one lowercase generic -- while every programme code is lowercase, including single-character ones (r, t, m, n). Case is the discriminator, not length. Every generic test was type[0].lower() in (...), a case-insensitive PREFIX that swept up any programme code starting with those letters; they now match the generic set exactly. 30 sites across dom, fitness, graph, operators, programme, shapecurve and bubble. NOT applied to the SEMANTIC prefixes: l/k/b/t classify programme codes by first letter (graph.py builds bedroom<->toilet and kitchen<->living relations from them) and stay prefix-based. Where the namespaces were mixed in one expression they were split -- has_circulation's ("b","l","k","c") is three semantic prefixes plus dom.is_circulation; access()'s ("l","c","s") is semantic l plus the generic circulation set. New: dom.GENERIC_{CIRCULATION,OUTSIDE,TYPES} + is_generic(); fitness. _generic_class(), replacing the _t0 dispatch in quality_size/quality_width/ quality_proportion/value_rate -- the four terms that mattered most and that a first sweep missed, since they dispatch through a t0 variable rather than an inline test. graph._adjacency_target resolves a generic adjacency requirement (programmes write "adjacency: [c, o]") to the generic set while every other requirement keeps Perl's prefix semantics. Two subtleties: S is in both generic sets but takes the OUTSIDE parameter families -- a first translation tested circulation first and silently gave S the circulation params, caught by test_get_space_params_sahn_proportion. And validate_codes survives, narrowed to a code spelled exactly C/O/S, which is a genuine ambiguity; merely starting with c/o/s is now fine. Invariant asserted as a test: test_scoring_is_invariant_under_programme_code_ spelling relabels one tree and its config together and re-scores. Bit-identical across 12 comparisons (6 seeds x collapse on/off). Re-baseline (seed 1, 20k, original names): 58 fails (15 hard / 43 soft) against the real 37-instance programme, with cr1 at 79.1 m2 vs declared 80 (was 32.9 and 17.1), of/st1/st2 all present and in band, and one fail naming any of them. 57 -> 58 on a 5-instance-harder programme is within noise: "did not regress". Fallout (§39.5): 2g7.5's CP-SAT seeder win does not survive. Over 6 seeds -- harbor real 102/114 (cpsat loses), harbor old-effective 98/99 (tie, so the win was already marginal), maple-court 156/144 (cpsat wins). maple is the control: the solver did not regress, harbor's programme changed. Test xfail'd with that reason plus a maple companion; both assign_solver flags stay default off. Filed homemaker-py-w6x to re-check other narrow-margin harbor A/Bs. 345 passed, 1 xfailed, same 7 pre-existing fixture failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 09:45:28 +00:00
def test_exact_generic_rejected_by_both_parse_paths():
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
"""programme._parse_spaces and fitness.Fitness._load_programme parse
conf["spaces"] independently validating only one would leave the other
door open."""
§39.4: tighten generic-type matching, reverting the harbor rename Supersedes the previous commit's approach. Renaming harbor's four colliding codes fixed one programme; tightening the matching rule fixes the rule, so a room may be called anything. cr1/of/st1/st2 are restored and the examples are byte-identical to their pre-§39 state -- which also means existing .dom artefacts (evolved-3M*) stay valid, so migrate_ju3_rename.py is deleted. The rule: Urb has exactly three GENERIC structural types (get_space_types: qw/C O S/), the leaves the search creates. Measured across the corpus: 154 C, 110 O, 1 S, not one lowercase generic -- while every programme code is lowercase, including single-character ones (r, t, m, n). Case is the discriminator, not length. Every generic test was type[0].lower() in (...), a case-insensitive PREFIX that swept up any programme code starting with those letters; they now match the generic set exactly. 30 sites across dom, fitness, graph, operators, programme, shapecurve and bubble. NOT applied to the SEMANTIC prefixes: l/k/b/t classify programme codes by first letter (graph.py builds bedroom<->toilet and kitchen<->living relations from them) and stay prefix-based. Where the namespaces were mixed in one expression they were split -- has_circulation's ("b","l","k","c") is three semantic prefixes plus dom.is_circulation; access()'s ("l","c","s") is semantic l plus the generic circulation set. New: dom.GENERIC_{CIRCULATION,OUTSIDE,TYPES} + is_generic(); fitness. _generic_class(), replacing the _t0 dispatch in quality_size/quality_width/ quality_proportion/value_rate -- the four terms that mattered most and that a first sweep missed, since they dispatch through a t0 variable rather than an inline test. graph._adjacency_target resolves a generic adjacency requirement (programmes write "adjacency: [c, o]") to the generic set while every other requirement keeps Perl's prefix semantics. Two subtleties: S is in both generic sets but takes the OUTSIDE parameter families -- a first translation tested circulation first and silently gave S the circulation params, caught by test_get_space_params_sahn_proportion. And validate_codes survives, narrowed to a code spelled exactly C/O/S, which is a genuine ambiguity; merely starting with c/o/s is now fine. Invariant asserted as a test: test_scoring_is_invariant_under_programme_code_ spelling relabels one tree and its config together and re-scores. Bit-identical across 12 comparisons (6 seeds x collapse on/off). Re-baseline (seed 1, 20k, original names): 58 fails (15 hard / 43 soft) against the real 37-instance programme, with cr1 at 79.1 m2 vs declared 80 (was 32.9 and 17.1), of/st1/st2 all present and in band, and one fail naming any of them. 57 -> 58 on a 5-instance-harder programme is within noise: "did not regress". Fallout (§39.5): 2g7.5's CP-SAT seeder win does not survive. Over 6 seeds -- harbor real 102/114 (cpsat loses), harbor old-effective 98/99 (tie, so the win was already marginal), maple-court 156/144 (cpsat wins). maple is the control: the solver did not regress, harbor's programme changed. Test xfail'd with that reason plus a maple companion; both assign_solver flags stay default off. Filed homemaker-py-w6x to re-check other narrow-margin harbor A/Bs. 345 passed, 1 xfailed, same 7 pre-existing fixture failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 09:45:28 +00:00
conf = {"spaces": {"C": {"size": [80.0, 10.0]}}}
with pytest.raises(ValueError, match="generic structural types"):
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
programme._parse_spaces(conf)
§39.4: tighten generic-type matching, reverting the harbor rename Supersedes the previous commit's approach. Renaming harbor's four colliding codes fixed one programme; tightening the matching rule fixes the rule, so a room may be called anything. cr1/of/st1/st2 are restored and the examples are byte-identical to their pre-§39 state -- which also means existing .dom artefacts (evolved-3M*) stay valid, so migrate_ju3_rename.py is deleted. The rule: Urb has exactly three GENERIC structural types (get_space_types: qw/C O S/), the leaves the search creates. Measured across the corpus: 154 C, 110 O, 1 S, not one lowercase generic -- while every programme code is lowercase, including single-character ones (r, t, m, n). Case is the discriminator, not length. Every generic test was type[0].lower() in (...), a case-insensitive PREFIX that swept up any programme code starting with those letters; they now match the generic set exactly. 30 sites across dom, fitness, graph, operators, programme, shapecurve and bubble. NOT applied to the SEMANTIC prefixes: l/k/b/t classify programme codes by first letter (graph.py builds bedroom<->toilet and kitchen<->living relations from them) and stay prefix-based. Where the namespaces were mixed in one expression they were split -- has_circulation's ("b","l","k","c") is three semantic prefixes plus dom.is_circulation; access()'s ("l","c","s") is semantic l plus the generic circulation set. New: dom.GENERIC_{CIRCULATION,OUTSIDE,TYPES} + is_generic(); fitness. _generic_class(), replacing the _t0 dispatch in quality_size/quality_width/ quality_proportion/value_rate -- the four terms that mattered most and that a first sweep missed, since they dispatch through a t0 variable rather than an inline test. graph._adjacency_target resolves a generic adjacency requirement (programmes write "adjacency: [c, o]") to the generic set while every other requirement keeps Perl's prefix semantics. Two subtleties: S is in both generic sets but takes the OUTSIDE parameter families -- a first translation tested circulation first and silently gave S the circulation params, caught by test_get_space_params_sahn_proportion. And validate_codes survives, narrowed to a code spelled exactly C/O/S, which is a genuine ambiguity; merely starting with c/o/s is now fine. Invariant asserted as a test: test_scoring_is_invariant_under_programme_code_ spelling relabels one tree and its config together and re-scores. Bit-identical across 12 comparisons (6 seeds x collapse on/off). Re-baseline (seed 1, 20k, original names): 58 fails (15 hard / 43 soft) against the real 37-instance programme, with cr1 at 79.1 m2 vs declared 80 (was 32.9 and 17.1), of/st1/st2 all present and in band, and one fail naming any of them. 57 -> 58 on a 5-instance-harder programme is within noise: "did not regress". Fallout (§39.5): 2g7.5's CP-SAT seeder win does not survive. Over 6 seeds -- harbor real 102/114 (cpsat loses), harbor old-effective 98/99 (tie, so the win was already marginal), maple-court 156/144 (cpsat wins). maple is the control: the solver did not regress, harbor's programme changed. Test xfail'd with that reason plus a maple companion; both assign_solver flags stay default off. Filed homemaker-py-w6x to re-check other narrow-margin harbor A/Bs. 345 passed, 1 xfailed, same 7 pre-existing fixture failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 09:45:28 +00:00
with pytest.raises(ValueError, match="generic structural types"):
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
fitness.Fitness(conf=conf)
§39.4: tighten generic-type matching, reverting the harbor rename Supersedes the previous commit's approach. Renaming harbor's four colliding codes fixed one programme; tightening the matching rule fixes the rule, so a room may be called anything. cr1/of/st1/st2 are restored and the examples are byte-identical to their pre-§39 state -- which also means existing .dom artefacts (evolved-3M*) stay valid, so migrate_ju3_rename.py is deleted. The rule: Urb has exactly three GENERIC structural types (get_space_types: qw/C O S/), the leaves the search creates. Measured across the corpus: 154 C, 110 O, 1 S, not one lowercase generic -- while every programme code is lowercase, including single-character ones (r, t, m, n). Case is the discriminator, not length. Every generic test was type[0].lower() in (...), a case-insensitive PREFIX that swept up any programme code starting with those letters; they now match the generic set exactly. 30 sites across dom, fitness, graph, operators, programme, shapecurve and bubble. NOT applied to the SEMANTIC prefixes: l/k/b/t classify programme codes by first letter (graph.py builds bedroom<->toilet and kitchen<->living relations from them) and stay prefix-based. Where the namespaces were mixed in one expression they were split -- has_circulation's ("b","l","k","c") is three semantic prefixes plus dom.is_circulation; access()'s ("l","c","s") is semantic l plus the generic circulation set. New: dom.GENERIC_{CIRCULATION,OUTSIDE,TYPES} + is_generic(); fitness. _generic_class(), replacing the _t0 dispatch in quality_size/quality_width/ quality_proportion/value_rate -- the four terms that mattered most and that a first sweep missed, since they dispatch through a t0 variable rather than an inline test. graph._adjacency_target resolves a generic adjacency requirement (programmes write "adjacency: [c, o]") to the generic set while every other requirement keeps Perl's prefix semantics. Two subtleties: S is in both generic sets but takes the OUTSIDE parameter families -- a first translation tested circulation first and silently gave S the circulation params, caught by test_get_space_params_sahn_proportion. And validate_codes survives, narrowed to a code spelled exactly C/O/S, which is a genuine ambiguity; merely starting with c/o/s is now fine. Invariant asserted as a test: test_scoring_is_invariant_under_programme_code_ spelling relabels one tree and its config together and re-scores. Bit-identical across 12 comparisons (6 seeds x collapse on/off). Re-baseline (seed 1, 20k, original names): 58 fails (15 hard / 43 soft) against the real 37-instance programme, with cr1 at 79.1 m2 vs declared 80 (was 32.9 and 17.1), of/st1/st2 all present and in band, and one fail naming any of them. 57 -> 58 on a 5-instance-harder programme is within noise: "did not regress". Fallout (§39.5): 2g7.5's CP-SAT seeder win does not survive. Over 6 seeds -- harbor real 102/114 (cpsat loses), harbor old-effective 98/99 (tie, so the win was already marginal), maple-court 156/144 (cpsat wins). maple is the control: the solver did not regress, harbor's programme changed. Test xfail'd with that reason plus a maple companion; both assign_solver flags stay default off. Filed homemaker-py-w6x to re-check other narrow-margin harbor A/Bs. 345 passed, 1 xfailed, same 7 pre-existing fixture failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 09:45:28 +00:00
def test_colliding_code_is_a_full_requirement_not_a_generic():
"""The §39.2 damage in one assertion: a c-prefixed code must keep its
declared targets and stay in the required set."""
conf = {"spaces": {"cr1": {"size": [80.0, 10.0], "width": [6.0, 1.5],
§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
"proportion": [2.0, 0.5], "count": 1,
"usage": "living"}}}
§39.4: tighten generic-type matching, reverting the harbor rename Supersedes the previous commit's approach. Renaming harbor's four colliding codes fixed one programme; tightening the matching rule fixes the rule, so a room may be called anything. cr1/of/st1/st2 are restored and the examples are byte-identical to their pre-§39 state -- which also means existing .dom artefacts (evolved-3M*) stay valid, so migrate_ju3_rename.py is deleted. The rule: Urb has exactly three GENERIC structural types (get_space_types: qw/C O S/), the leaves the search creates. Measured across the corpus: 154 C, 110 O, 1 S, not one lowercase generic -- while every programme code is lowercase, including single-character ones (r, t, m, n). Case is the discriminator, not length. Every generic test was type[0].lower() in (...), a case-insensitive PREFIX that swept up any programme code starting with those letters; they now match the generic set exactly. 30 sites across dom, fitness, graph, operators, programme, shapecurve and bubble. NOT applied to the SEMANTIC prefixes: l/k/b/t classify programme codes by first letter (graph.py builds bedroom<->toilet and kitchen<->living relations from them) and stay prefix-based. Where the namespaces were mixed in one expression they were split -- has_circulation's ("b","l","k","c") is three semantic prefixes plus dom.is_circulation; access()'s ("l","c","s") is semantic l plus the generic circulation set. New: dom.GENERIC_{CIRCULATION,OUTSIDE,TYPES} + is_generic(); fitness. _generic_class(), replacing the _t0 dispatch in quality_size/quality_width/ quality_proportion/value_rate -- the four terms that mattered most and that a first sweep missed, since they dispatch through a t0 variable rather than an inline test. graph._adjacency_target resolves a generic adjacency requirement (programmes write "adjacency: [c, o]") to the generic set while every other requirement keeps Perl's prefix semantics. Two subtleties: S is in both generic sets but takes the OUTSIDE parameter families -- a first translation tested circulation first and silently gave S the circulation params, caught by test_get_space_params_sahn_proportion. And validate_codes survives, narrowed to a code spelled exactly C/O/S, which is a genuine ambiguity; merely starting with c/o/s is now fine. Invariant asserted as a test: test_scoring_is_invariant_under_programme_code_ spelling relabels one tree and its config together and re-scores. Bit-identical across 12 comparisons (6 seeds x collapse on/off). Re-baseline (seed 1, 20k, original names): 58 fails (15 hard / 43 soft) against the real 37-instance programme, with cr1 at 79.1 m2 vs declared 80 (was 32.9 and 17.1), of/st1/st2 all present and in band, and one fail naming any of them. 57 -> 58 on a 5-instance-harder programme is within noise: "did not regress". Fallout (§39.5): 2g7.5's CP-SAT seeder win does not survive. Over 6 seeds -- harbor real 102/114 (cpsat loses), harbor old-effective 98/99 (tie, so the win was already marginal), maple-court 156/144 (cpsat wins). maple is the control: the solver did not regress, harbor's programme changed. Test xfail'd with that reason plus a maple companion; both assign_solver flags stay default off. Filed homemaker-py-w6x to re-check other narrow-margin harbor A/Bs. 345 passed, 1 xfailed, same 7 pre-existing fixture failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 09:45:28 +00:00
fit = fitness.Fitness(conf=conf)
assert fit.get_space_params("cr1", "size") == [80.0, 10.0]
assert fit.get_space_params("cr1", "width") == [6.0, 1.5]
assert not dom.is_generic("cr1")
assert not dom.is_circulation(dom.Node(type="cr1"))
assert not dom.is_outside(dom.Node(type="of"))
# ...while the genuine generics still classify as before
assert dom.is_circulation(dom.Node(type="C"))
assert dom.is_outside(dom.Node(type="O"))
assert dom.is_outside(dom.Node(type="S")) and dom.is_circulation(dom.Node(type="S"))
§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
def test_a_codes_first_letter_no_longer_decides_anything():
"""§39.7: usage is DECLARED, so a code's spelling carries no meaning at all.
`b1` was a bedroom purely because it started with "b"; here it declares
`utility` and that is what it is. This is the property the old prefix
convention could not offer, and the reason `la1` "Laundry Room" was being
trimmed as a living room.
"""
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
reqs = programme._parse_spaces({"spaces": {
§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
"l1": {"size": [20.0, 4.0], "usage": "utility"},
"k1": {"size": [12.0, 3.0], "usage": "none"},
"b1": {"size": [16.0, 4.0], "usage": "utility"},
"zzz": {"size": [3.0, 1.0], "usage": "bedroom"},
}})
assert reqs["l1"].usage == "utility"
assert reqs["k1"].usage == "none"
assert reqs["b1"].usage == "utility"
assert reqs["zzz"].usage == "bedroom"
def test_usage_is_mandatory_and_reported_by_code():
with pytest.raises(ValueError, match=r"declare no .usage.*\['b1'\]|\['b1'\].*declare no"):
programme._parse_spaces({"spaces": {"b1": {"size": [16.0, 4.0]}}})
def test_unknown_usage_is_rejected_not_silently_ignored():
with pytest.raises(ValueError, match="unknown usage value"):
programme._parse_spaces(
{"spaces": {"b1": {"size": [16.0, 4.0], "usage": "craft"}}})
def test_usage_is_rejected_by_both_parse_paths():
"""Fitness parses conf["spaces"] independently of programme._parse_spaces."""
conf = {"spaces": {"b1": {"size": [16.0, 4.0]}}}
with pytest.raises(ValueError, match="declare no"):
programme._parse_spaces(conf)
with pytest.raises(ValueError, match="declare no"):
fitness.Fitness(conf=conf)
def test_usage_survives_a_retype_because_it_is_code_level():
"""The mutation-safety property: usage is looked up from the CODE, so a
retype changes the class automatically and nothing can go stale."""
fit = fitness.Fitness(conf={"spaces": {
"b1": {"size": [16.0, 4.0], "usage": "bedroom"},
"s9": {"size": [16.0, 4.0], "usage": "utility"},
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
}})
§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
leaf = dom.Node(type="b1")
assert fit.usage_of(leaf) == "bedroom"
leaf.type = "s9" # exactly what mutate_retype does
assert fit.usage_of(leaf) == "utility"
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
def test_corpus_programmes_are_namespace_clean():
"""Every checked-in example must load — a regression here means a corpus
programme reintroduced a colliding code."""
for d in sorted(Path("examples").iterdir()):
if (d / "patterns.config").is_file():
programme.load_programme_dir(str(d))
§39.4: tighten generic-type matching, reverting the harbor rename Supersedes the previous commit's approach. Renaming harbor's four colliding codes fixed one programme; tightening the matching rule fixes the rule, so a room may be called anything. cr1/of/st1/st2 are restored and the examples are byte-identical to their pre-§39 state -- which also means existing .dom artefacts (evolved-3M*) stay valid, so migrate_ju3_rename.py is deleted. The rule: Urb has exactly three GENERIC structural types (get_space_types: qw/C O S/), the leaves the search creates. Measured across the corpus: 154 C, 110 O, 1 S, not one lowercase generic -- while every programme code is lowercase, including single-character ones (r, t, m, n). Case is the discriminator, not length. Every generic test was type[0].lower() in (...), a case-insensitive PREFIX that swept up any programme code starting with those letters; they now match the generic set exactly. 30 sites across dom, fitness, graph, operators, programme, shapecurve and bubble. NOT applied to the SEMANTIC prefixes: l/k/b/t classify programme codes by first letter (graph.py builds bedroom<->toilet and kitchen<->living relations from them) and stay prefix-based. Where the namespaces were mixed in one expression they were split -- has_circulation's ("b","l","k","c") is three semantic prefixes plus dom.is_circulation; access()'s ("l","c","s") is semantic l plus the generic circulation set. New: dom.GENERIC_{CIRCULATION,OUTSIDE,TYPES} + is_generic(); fitness. _generic_class(), replacing the _t0 dispatch in quality_size/quality_width/ quality_proportion/value_rate -- the four terms that mattered most and that a first sweep missed, since they dispatch through a t0 variable rather than an inline test. graph._adjacency_target resolves a generic adjacency requirement (programmes write "adjacency: [c, o]") to the generic set while every other requirement keeps Perl's prefix semantics. Two subtleties: S is in both generic sets but takes the OUTSIDE parameter families -- a first translation tested circulation first and silently gave S the circulation params, caught by test_get_space_params_sahn_proportion. And validate_codes survives, narrowed to a code spelled exactly C/O/S, which is a genuine ambiguity; merely starting with c/o/s is now fine. Invariant asserted as a test: test_scoring_is_invariant_under_programme_code_ spelling relabels one tree and its config together and re-scores. Bit-identical across 12 comparisons (6 seeds x collapse on/off). Re-baseline (seed 1, 20k, original names): 58 fails (15 hard / 43 soft) against the real 37-instance programme, with cr1 at 79.1 m2 vs declared 80 (was 32.9 and 17.1), of/st1/st2 all present and in band, and one fail naming any of them. 57 -> 58 on a 5-instance-harder programme is within noise: "did not regress". Fallout (§39.5): 2g7.5's CP-SAT seeder win does not survive. Over 6 seeds -- harbor real 102/114 (cpsat loses), harbor old-effective 98/99 (tie, so the win was already marginal), maple-court 156/144 (cpsat wins). maple is the control: the solver did not regress, harbor's programme changed. Test xfail'd with that reason plus a maple companion; both assign_solver flags stay default off. Filed homemaker-py-w6x to re-check other narrow-margin harbor A/Bs. 345 passed, 1 xfailed, same 7 pre-existing fixture failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 09:45:28 +00:00
def test_scoring_is_invariant_under_programme_code_spelling(tmp_path):
"""§39.4's headline invariant: renaming a programme code must not change
what a layout scores.
Builds one layout from harbor-house (whose codes ``cr1``/``of``/``st1``/
``st2`` all begin with a reserved generic letter), then relabels that exact
tree AND its config together and re-scores. Same geometry, same topology,
only the spelling differs so any difference is the generic-type rule
leaking into the programme namespace, which is the bug this guards.
"""
import copy
import re
import shutil
import numpy as np
from homemaker_layout import driver, operators
rename = {"cr1": "fr1", "of": "ao", "st1": "gs1", "st2": "gs2"}
src = Path("examples/harbor-house")
shutil.copytree(src, tmp_path / "hh")
cfg = tmp_path / "hh" / "patterns.config"
text = cfg.read_text()
for old, new in rename.items():
text = re.sub(rf"^( ){re.escape(old)}:$", rf"\g<1>{new}:", text, flags=re.M)
cfg.write_text(text)
def evaluator(directory):
overrides = driver._overrides_for(True, False, None, False, True, False)
conf, cost = fitness.load_config(str(directory), overrides=dict(overrides or {}))
return fitness.Fitness(conf, cost)
def relabel(root):
for lvl in dom.levels(root):
for leaf in lvl.leaves():
leaf.type = rename.get(leaf.type, leaf.type)
leaf.share_type = rename.get(leaf.share_type, leaf.share_type)
return root
reqs = programme.load_programme_dir(str(src))
before, after = evaluator(src), evaluator(tmp_path / "hh")
for seed in range(3):
root = operators.constructive_topology(
dom.load(str(src / "init.dom")), reqs, np.random.default_rng(seed),
sorted(reqs) + ["C", "O"],
min_storeys=programme.storey_minimum(str(src)),
adjacency_aware=True, proportion_aware=True, circ_divisor=3,
leaf_sharing=True, leaf_share_factor=3, depth_balanced=True,
interior_outside=True, outside_divisor=3)
score_a, fails_a = before.score_with_fails(copy.deepcopy(root))
score_b, fails_b = after.score_with_fails(relabel(copy.deepcopy(root)))
normalised = tuple(sorted(
re.sub(r"\b(%s)\b" % "|".join(rename), lambda m: rename[m.group(1)], f)
for f in fails_a))
assert f"{score_a:.12g}" == f"{score_b:.12g}", f"seed {seed}: score differs"
assert normalised == tuple(sorted(fails_b)), f"seed {seed}: fails differ"