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
This commit is contained in:
parent
fd9802e499
commit
697914fac8
12 changed files with 262 additions and 34 deletions
|
|
@ -1,5 +1,6 @@
|
|||
{"_type":"issue","id":"homemaker-py-ju3","title":"Programme codes share a namespace with the generic c/o/s type prefixes: 14% of harbor-house is silently optional and cr1's declared targets are all discarded","description":"Urb's type system is prefix-based (a type starting with 'c' is circulation, 'o'/'s' is outside) and programme room codes live in the SAME namespace, so any code whose name happens to start with c, o or s is silently reinterpreted as a generic type. Three separate consequences, none announced anywhere in the output:\\n\\n1. graph.check_space_counts line ~530 does 'if code[0].lower() in (\"c\",\"o\",\"s\"): continue' -- the code is SKIPPED ENTIRELY. Never required, never counted, no missing fail, no too-many fail.\\n2. Fitness.get_space_params returns the generic *_circulation / *_outside params BEFORE consulting self.spaces, so declared size/width/proportion are overridden.\\n3. dom.is_circulation / is_outside become true, changing the leaf's value rate, exempting it from crinkliness, and making it supply daylight to neighbours.\\n\\nharbor-house is affected; maple-court, health-centre and programme-house are namespace-clean.\\n\\n cr1 'Common Room with Fireplace' (c): size 80.0 -\u003e 0.0/14.0, width 6.0 -\u003e 2.4, proportion 2.0 -\u003e 1.5, ALL THREE overridden; is_circulation=True so value_rate 50 not 300.\\n of 'Staff Office' x2 (o): width/proportion overridden; is_outside=True; value_rate 100.\\n st1/st2 'Storage' (s): width/proportion overridden; is_outside AND is_circulation True; value_rate 100.\\n\\n5 of 37 room instances (14%) are silently optional. MEASURED CONSEQUENCE: in a 20k-eval run the two cr1 leaves converged to 32.9 and 17.1 m2 against a declared 80 m2, and produced no too-many-spaces fail despite count:1; of/st1/st2 are absent from the result entirely with zero fails, because nothing ever asked for them. Compounds with homemaker-py-2v1: cr1 is the single largest room in the programme and is classified circulation, so the x6 value gap pays the search to shrink it.","design":"Separate the namespaces. Cleanest is an explicit per-space 'class:' key in patterns.config (inside/circulation/outside) defaulting to inside, with the prefix rule used ONLY for untyped generic leaves the search creates -- programme codes then never collide regardless of spelling. A cheaper stopgap is a load-time validation error in programme.load_programme_dir that refuses a programme code starting with c/o/s, which at least converts a silent misread into a loud one. Renaming harbor's four codes would fix that one programme but leaves the trap armed for the next author.","acceptance_criteria":"A programme declaring a code starting with c/o/s either honours its declared params and count, or fails loudly at load. harbor-house re-baselined against its real 37-instance programme, and every DESIGN.md harbor fail count re-stated or annotated as measured against the 32-instance effective programme.","status":"open","priority":0,"issue_type":"bug","owner":"noreply@anthropic.com","created_at":"2026-08-26T08:26:37Z","created_by":"Claude","updated_at":"2026-08-26T08:26:37Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"homemaker-py-ju3","title":"Programme codes share a namespace with the generic c/o/s type prefixes: 14% of harbor-house is silently optional and cr1's declared targets are all discarded","description":"Urb's type system is prefix-based (a type starting with 'c' is circulation, 'o'/'s' is outside) and programme room codes live in the SAME namespace, so any code whose name happens to start with c, o or s is silently reinterpreted as a generic type. Three separate consequences, none announced anywhere in the output:\\n\\n1. graph.check_space_counts line ~530 does 'if code[0].lower() in (\"c\",\"o\",\"s\"): continue' -- the code is SKIPPED ENTIRELY. Never required, never counted, no missing fail, no too-many fail.\\n2. Fitness.get_space_params returns the generic *_circulation / *_outside params BEFORE consulting self.spaces, so declared size/width/proportion are overridden.\\n3. dom.is_circulation / is_outside become true, changing the leaf's value rate, exempting it from crinkliness, and making it supply daylight to neighbours.\\n\\nharbor-house is affected; maple-court, health-centre and programme-house are namespace-clean.\\n\\n cr1 'Common Room with Fireplace' (c): size 80.0 -\u003e 0.0/14.0, width 6.0 -\u003e 2.4, proportion 2.0 -\u003e 1.5, ALL THREE overridden; is_circulation=True so value_rate 50 not 300.\\n of 'Staff Office' x2 (o): width/proportion overridden; is_outside=True; value_rate 100.\\n st1/st2 'Storage' (s): width/proportion overridden; is_outside AND is_circulation True; value_rate 100.\\n\\n5 of 37 room instances (14%) are silently optional. MEASURED CONSEQUENCE: in a 20k-eval run the two cr1 leaves converged to 32.9 and 17.1 m2 against a declared 80 m2, and produced no too-many-spaces fail despite count:1; of/st1/st2 are absent from the result entirely with zero fails, because nothing ever asked for them. Compounds with homemaker-py-2v1: cr1 is the single largest room in the programme and is classified circulation, so the x6 value gap pays the search to shrink it.","design":"Separate the namespaces. Cleanest is an explicit per-space 'class:' key in patterns.config (inside/circulation/outside) defaulting to inside, with the prefix rule used ONLY for untyped generic leaves the search creates -- programme codes then never collide regardless of spelling. A cheaper stopgap is a load-time validation error in programme.load_programme_dir that refuses a programme code starting with c/o/s, which at least converts a silent misread into a loud one. Renaming harbor's four codes would fix that one programme but leaves the trap armed for the next author.","acceptance_criteria":"A programme declaring a code starting with c/o/s either honours its declared params and count, or fails loudly at load. harbor-house re-baselined against its real 37-instance programme, and every DESIGN.md harbor fail count re-stated or annotated as measured against the 32-instance effective programme.","status":"closed","priority":0,"issue_type":"bug","assignee":"Claude","owner":"noreply@anthropic.com","created_at":"2026-08-26T08:26:37Z","created_by":"Claude","updated_at":"2026-08-26T09:05:11Z","started_at":"2026-08-26T08:55:30Z","closed_at":"2026-08-26T09:05:11Z","close_reason":"Shipped as loud validation + harbor rename rather than the class: key (DESIGN.md §39.3). The class: key was deliberately not built: auditing the prefix rule showed l/k/b/t carry adjacency semantics too, so re-plumbing the type system would invalidate the whole corpus for a problem whose damage is the silence, not the convention. programme.validate_codes raises on c/o/s codes from both parse paths; harbor cr1/of/st1/st2 renamed to fr1/ao/gs1/gs2 (neutral unused prefixes, prefix-sharing preserved); migrate_ju3_rename.py migrates pre-rename .dom files. Re-baselined at seed 1/20k: 57 fails on the 32-instance effective programme -\u003e 55 on the real 37-instance one, with all five previously-lost room instances now placed inside their declared sigma bands and zero fails naming them.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"homemaker-py-2v1","title":"Connectivity is under-priced ~3x against the circulation-\u003ehabitable value gap: the objective is net-positive on severing the spine","description":"Sharper root cause than homemaker-py-ssz, isolated by the 38.6 A/B (which showed none of the three crinkliness modes removes the deletion incentive). Deleting a circulation leaf merges it into its sibling, converting corridor into habitable area. value_circulation=50 vs value_inside=300, so that is a flat 6x value gain. The only counter-pressure is the 'level N not connected' fail, worth 0.5x under value *= 0.5**len(failures). Break-even needs 0.5^k \u003c 50/300, i.e. k \u003e 2.58 -- severing must cost AT LEAST 3 fails to be net-negative, and it costs 1. Net incentive to sever = 6 * 0.5 = 3.0x in favour; measured 4.06x on a well-lit (q_crink=0.736) circulation leaf, so this is NOT the zero-exposure effect and is not fixable inside quality_uncrinkliness. This is the cleanest explanation of why 'level 0 not connected' and 'level 1 not connected' are still present in evolved-3M-nols-3, the best layout found after 1.7M evals: the search is being paid 3-4x to create them.","design":"Options: (a) emit connectivity fails with a multiplicity \u003e= 3 (cheapest, but stacks with the 1i8 cascade-weighting problem and is a magic number); (b) make the connectivity penalty multiplicative and explicit rather than riding the generic 0.5^n (a dedicated building_factor term, sized from the value-rate gap so it tracks value_circulation/value_inside instead of being hard-coded); (c) revalue circulation as infrastructure -- its worth is that it makes other rooms reachable, which the current per-leaf value rate cannot express; the principled version credits circulation with the access it provides rather than its own floor area. (c) is the architecturally correct one and the biggest change. Recommend measuring (a) first purely to confirm the mechanism (does the 3x threshold flip the deletion test?), then designing (b) or (c) properly.","acceptance_criteria":"Deletion test (experiments/diag_exposure_frontage.py value + the ssz A/B harness) shows lit and buried C/O deletions are no longer rewarded; then harbor-house reaches the 15-fail floor in materially fewer than 1.7M evals AND without 'level 0/1 not connected' in the result.","status":"open","priority":0,"issue_type":"bug","owner":"noreply@anthropic.com","created_at":"2026-08-26T07:37:39Z","created_by":"Claude","updated_at":"2026-08-26T07:37:39Z","dependency_count":0,"dependent_count":2,"comment_count":0}
|
||||
{"_type":"issue","id":"homemaker-py-ut5","title":"Update the 2v1 acceptance target: harbor's 15-fail reference was measured pre-ju3 against the 32-instance effective programme","description":"DESIGN.md 38.7 fixes the acceptance test for homemaker-py-2v1 as 'harbor-house reaches its known 15-fail floor in materially fewer than 1.7M evals, and without level 0/1 not connected'. That 15-fail figure comes from evolved-3M-nols-3, measured before homemaker-py-ju3 against the 32-instance EFFECTIVE programme (cr1/of/st1/st2 silently dropped or mis-parameterised). Against the real 37-instance programme the number will differ, so the target as written is not measurable any more.\\n\\nNeeded: migrate evolved-3M*.dom with experiments/migrate_ju3_rename.py, rescore against the renamed programme, and restate the 2v1 acceptance figure. The 39.3 re-baseline (55 fails, seed 1, 20k evals) is the new near-term reference but is not the long-budget floor.","acceptance_criteria":"evolved-3M*.dom committed and migrated; its post-ju3 fail count recorded in DESIGN.md; the 2v1 acceptance figure restated against it.","status":"open","priority":1,"issue_type":"task","owner":"noreply@anthropic.com","created_at":"2026-08-26T09:05:23Z","created_by":"Claude","updated_at":"2026-08-26T09:05:23Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"homemaker-py-hxi","title":"Buried circulation and outside space are negative-value: search is rewarded for deleting the circulation spine","description":"Direct consequence of the zero-exposure bug. Measured on a constructed harbor-house seed: deleting a buried O leaf improved the raw score 85x and removed 7 fails; deleting a buried C leaf improved it 62x and removed 6 fails. Programme rooms are held in place ONLY by the missing-space fail cascade, not by contributing value -- deleting a buried k1/da1/m costs +15 fails, so they stay, but nothing positive keeps them. Circulation and outside leaves carry no missing-space requirement, so nothing keeps them at all. Observed live: in a 20k-eval harbor-house run, undivide/core_undivide account for a large share of recorded improvements (16 occurrences in the log) -- the search is literally deleting circulation to score better. This explains three prior negative results as a single mechanism: 18 graded circulation-connectivity (a tie-break signal cannot beat a 60x scalar gradient), 21/22 bridge_circulation (the operator inserts corridor leaves the objective immediately punishes), and the 'level N not connected' hard fails surviving \u003e1M evals in the 3M run.","notes":"Depends on the zero-exposure fix; may need no separate fix if (a)/(c) there restores a value gradient for circulation. Worth re-running the 18 and 21/22 A/Bs afterwards -- both may have been measuring a broken gradient rather than a bad idea.","status":"open","priority":1,"issue_type":"bug","owner":"noreply@anthropic.com","created_at":"2026-08-25T23:14:40Z","created_by":"Claude","updated_at":"2026-08-25T23:14:40Z","dependencies":[{"issue_id":"homemaker-py-hxi","depends_on_id":"homemaker-py-ssz","type":"blocks","created_at":"2026-08-25T23:15:12Z","created_by":"Claude","metadata":"{}"},{"issue_id":"homemaker-py-hxi","depends_on_id":"homemaker-py-2v1","type":"blocks","created_at":"2026-08-26T07:37:47Z","created_by":"Claude","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"homemaker-py-ssz","title":"Zero-exposure leaves score hard quality=0: the objective assigns no value to any interior room","description":"fitness.quality_uncrinkliness returns a hard 0.0 when a leaf has no daylit wall (area_outside==0 =\u003e crink==0 =\u003e 'if not crink: return 0.0'). Because evaluate_leaf MULTIPLIES factors into quality, and process_storey accumulates 'value += quality * rate * area', such a leaf contributes EXACTLY ZERO value while still adding cost. Measured on the full default construction stack (leaf_sharing, depth_balanced, interior_outside, collapse_insearch), 3 seeds each: harbor-house 46% of interior leaves, health-centre 45%, maple-court 56% are zero-exposure. On a converged 20k-eval harbor-house run (seed 1, 57 fails) 14 of 17 crinkliness fails are zero-exposure, and ~470 m2 of the 721 m2 ground floor plate sits at zero value. This is the mathematically consistent limit of the gaussian (1/crink -\u003e inf), so it is a faithful port, not a porting bug -- but it means the objective's gradient does not describe a good building.","design":"Options, none yet chosen: (a) floor the factor at a small epsilon instead of 0 so buried leaves keep a value gradient and remain rankable; (b) make the gaussian one-sided (clip to 1.0 on the compact side) so being LESS exposed than target is not punished as hard as being over-exposed -- architecturally, a compact well-insulated room is not a defect; (c) exempt circulation/store types from the daylight requirement entirely (uncrinkliness_circulation currently uses the same [5/6, 1.1/3] as habitable rooms, so internal corridors -- completely normal architecture -- are guaranteed failures). Any change here invalidates prior fail-count baselines, so it needs its own A/B and a DESIGN.md section.","acceptance_criteria":"A/B on harbor-house + maple-court at fixed budget showing the chosen variant lowers hard-fail count without inflating soft; DESIGN.md section recording the result; prior baselines re-stated under the new objective.","status":"open","priority":1,"issue_type":"bug","owner":"noreply@anthropic.com","created_at":"2026-08-25T23:14:40Z","created_by":"Claude","updated_at":"2026-08-25T23:14:40Z","dependencies":[{"issue_id":"homemaker-py-ssz","depends_on_id":"homemaker-py-2v1","type":"blocks","created_at":"2026-08-26T07:37:47Z","created_by":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0}
|
||||
{"_type":"issue","id":"homemaker-py-tdp","title":"Frontage-budget feasibility bound: harbor-house and maple-court are ~3x short of the exposed wall their own crinkliness constraint demands","description":"Closed-form bound, no search needed. Crinkliness fails when 1/crink \u003e 1.6202 (solving gaussian(x,1,5/6,1.1/3)=FAIL_THRESHOLD), and crink = L*h/A, so every interior leaf needs exposed wall length L \u003e= A/(1.6202*h) -- per storey, A_storey/4.86 metres at h=3. Supply vs demand per storey: harbor-house 54m supplied / 148m needed (2.7x short); maple-court 56m / 162m (2.9x short); health-centre 43m / 41m (feasible); programme-house 24m / 12m (2x surplus). The deficit comes from half the plot perimeter being marked 'private' in harbor/maple -- area_outside skips private/fortified edges. This PREDICTS the corpus fail-count plateau: the two programmes that are frontage-short are exactly the two that plateau (harbor 30-40, maple 74-84 fails), and the two with surplus are the two that effectively solve (programme-house reached 1 fail in a 12k-eval run here). Causal check: relabelling harbor's two private edges as open (identical geometry and programme, perimeter labels only) cut zero-exposure leaves 52%-\u003e19% and seeder crinkliness fails 16.5-\u003e12.0 over 6 seeds.","design":"Ship the bound as a pre-flight check (a 'homemaker-programme-check' style report, or a warning from evolve.py at startup): given a plot and programme, report required vs available frontage and the courtyard area needed to close the gap. This is the cheap, no-ground-truth-needed version of what 2g7.2 objective calibration wanted -- it answers 'is this programme even satisfiable' without needing traced human plans.","acceptance_criteria":"A tool/check that reports the frontage budget per programme dir; DESIGN.md section with the derivation and the corpus table; the plateau programmes flagged as frontage-infeasible-as-specified.","status":"open","priority":1,"issue_type":"task","owner":"noreply@anthropic.com","created_at":"2026-08-25T23:14:40Z","created_by":"Claude","updated_at":"2026-08-25T23:14:40Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
|
|
|
|||
67
DESIGN.md
67
DESIGN.md
|
|
@ -5156,11 +5156,62 @@ document was measured against a **32-instance effective programme, not the
|
|||
harbor-house (§13.9, §13.11, §17, §20, §23, §37.1, §37.7) are internally
|
||||
consistent but are not measuring the programme as written.
|
||||
|
||||
**Fix direction** (`homemaker-py-ju3`): separate the namespaces — an explicit
|
||||
per-space `class:` key (`inside`/`circulation`/`outside`, defaulting to
|
||||
`inside`), with the prefix rule used *only* for the untyped generic leaves the
|
||||
search itself creates. A cheaper stopgap is a load-time validation error in
|
||||
`programme.load_programme_dir` refusing a code that starts with `c`/`o`/`s`,
|
||||
which at least converts a silent misread into a loud one. Renaming harbor's
|
||||
four codes fixes that one programme but leaves the trap armed for the next
|
||||
author.
|
||||
### 39.3 What shipped — loud validation + harbor rename, re-baselined
|
||||
|
||||
**The `class:` key was NOT built, deliberately.** Auditing every use of the
|
||||
prefix rule first showed it runs far deeper than `c`/`o`/`s`: `l`/`k`/`b`/`t`
|
||||
carry real adjacency semantics too (`graph.py` builds bedroom↔toilet and
|
||||
kitchen↔living relations from first characters, `fitness.py` gates access and
|
||||
public-entrance checks on them). Re-plumbing that into a declared type system
|
||||
would invalidate the entire corpus and every baseline in this document, for a
|
||||
problem whose actual damage is the **silence**, not the prefix convention.
|
||||
|
||||
Two facts made the smaller fix clearly sufficient:
|
||||
|
||||
1. **No corpus programme has ever declared a bare `c`/`o`/`s` code** (checked
|
||||
across all ten `examples/*/patterns.config`). So `check_space_counts`'
|
||||
skip never protected a legitimate case — generic types are not in `spaces`
|
||||
at all — and could only ever discard a declared room.
|
||||
2. **Nothing references the four harbor codes** in any `adjacency:` or
|
||||
`co_locate:` list, so renaming them is local.
|
||||
|
||||
Shipped instead:
|
||||
|
||||
- **`programme.validate_codes`** (new, `RESERVED_PREFIXES = ("c", "o", "s")`)
|
||||
raises with the full explanation and a pointer here. Called from **both**
|
||||
parse paths — `programme._parse_spaces` *and*
|
||||
`fitness.Fitness._load_programme`, which parse `conf["spaces"]`
|
||||
independently, so validating only one would leave the other door open.
|
||||
`l`/`k`/`b`/`t` are explicitly **not** reserved: they flavour heuristics but
|
||||
never discard a requirement.
|
||||
- **harbor-house / harbor-house-l0 renamed**: `cr1`→`fr1`, `of`→`ao`,
|
||||
`st1`→`gs1`, `st2`→`gs2`. Each new prefix is unused in harbor *and*
|
||||
semantically neutral (`f`/`a`/`g` carry no adjacency meaning, unlike
|
||||
`l`/`k`/`b`/`t`), and the two storage codes still share one prefix, so the
|
||||
prefix-sharing structure `fitness.evaluate_building`'s per-code plot-ratio
|
||||
term depends on is preserved. `name:` fields are unchanged.
|
||||
- **`experiments/migrate_ju3_rename.py`** rewrites `type:` lines in `.dom`
|
||||
files produced before the rename (`--check` to dry-run). Any pre-rename
|
||||
artefact — notably the `evolved-3M*.dom` harbor runs — **must** be migrated
|
||||
or its affected leaves will read as unmatched generics.
|
||||
|
||||
**Re-baseline** (`homemaker-evolve init.dom --budget 20000 --workers 4
|
||||
--seed 1`, same seed/budget/settings as §38's run, so the two are directly
|
||||
comparable):
|
||||
|
||||
| | effective programme | total fails | `fr1` area (declared 80) | `ao`/`gs1`/`gs2` |
|
||||
|---|---|---|---|---|
|
||||
| before | 32 instances | 57 | **32.9 and 17.1** (two leaves, `count: 1`, no too-many fail) | **absent, zero fails** |
|
||||
| after | **37 instances** | **55** (13 hard / 42 soft) | **87.2** | **all present: 14.2, 11.5, 24.6, 18.7** |
|
||||
|
||||
Every one of the five previously-lost room instances is now placed and sized
|
||||
inside its declared sigma band (`fr1` 80±10, `ao` 12.5±2.5, `gs1` 22±4,
|
||||
`gs2` 18±3.5), and **no failure in the result mentions any of the four codes**.
|
||||
The fail count did not rise despite the programme being 5 instances harder —
|
||||
though at one seed each, 57 vs 55 is within noise and the honest claim is
|
||||
"did not regress", not "improved". The robust result is the room placement.
|
||||
|
||||
**Historical numbers.** Every harbor-house fail count recorded before this
|
||||
section was measured against the 32-instance effective programme. They remain
|
||||
valid relative to each other but are not comparable to post-`ju3` numbers;
|
||||
treat §39.3's 55 as the new harbor reference point.
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ r:
|
|||
- 0.5292445435027436
|
||||
- 0.5292445435027436
|
||||
l:
|
||||
type: st1
|
||||
type: gs1
|
||||
rotation: 0
|
||||
r:
|
||||
type: la1
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
# Purpose: isolate per-floor CONSTRUCTION difficulty from multi-storey COUPLING.
|
||||
|
||||
spaces:
|
||||
cr1:
|
||||
fr1:
|
||||
name: Common Room with Fireplace
|
||||
size:
|
||||
- 80.0
|
||||
|
|
@ -102,7 +102,7 @@ spaces:
|
|||
adjacency:
|
||||
- c
|
||||
|
||||
st1:
|
||||
gs1:
|
||||
name: Ground Floor Storage
|
||||
size:
|
||||
- 22.0
|
||||
|
|
@ -124,7 +124,7 @@ spaces:
|
|||
adjacency:
|
||||
- c
|
||||
|
||||
of:
|
||||
ao:
|
||||
name: Staff Office
|
||||
size:
|
||||
- 12.5
|
||||
|
|
|
|||
|
|
@ -41,10 +41,10 @@ l:
|
|||
- 0.5042565018475718
|
||||
- 0.5042565018475718
|
||||
l:
|
||||
type: st2
|
||||
type: gs2
|
||||
rotation: 0
|
||||
r:
|
||||
type: st1
|
||||
type: gs1
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 1
|
||||
|
|
@ -84,7 +84,7 @@ l:
|
|||
type: ws1
|
||||
rotation: 0
|
||||
r:
|
||||
type: cr1
|
||||
type: fr1
|
||||
rotation: 0
|
||||
r:
|
||||
type: O
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
# Building programme: community living spaces
|
||||
spaces:
|
||||
# GROUND FLOOR - Main Community Areas
|
||||
cr1:
|
||||
fr1:
|
||||
name: Common Room with Fireplace
|
||||
size:
|
||||
- 80.0
|
||||
|
|
@ -148,7 +148,7 @@ spaces:
|
|||
co_locate:
|
||||
- me1
|
||||
|
||||
st1:
|
||||
gs1:
|
||||
name: Ground Floor Storage
|
||||
size:
|
||||
- 22.0
|
||||
|
|
@ -175,7 +175,7 @@ spaces:
|
|||
- la1
|
||||
|
||||
# STAFF OFFICES - Anonymous (2 total)
|
||||
of:
|
||||
ao:
|
||||
name: Staff Office
|
||||
size:
|
||||
- 12.5
|
||||
|
|
@ -216,7 +216,7 @@ spaces:
|
|||
count: 10
|
||||
|
||||
# FIRST FLOOR - Storage
|
||||
st2:
|
||||
gs2:
|
||||
name: First Floor Storage
|
||||
size:
|
||||
- 18.0
|
||||
|
|
|
|||
79
experiments/migrate_ju3_rename.py
Normal file
79
experiments/migrate_ju3_rename.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
"""Rename harbor-house's colliding room codes in existing `.dom` files.
|
||||
|
||||
`homemaker-py-ju3` (DESIGN.md §39.2) renamed four harbor-house programme codes
|
||||
that collided with Urb's reserved generic type prefixes (``c`` = circulation,
|
||||
``o``/``s`` = outside). Any `.dom` produced **before** that change still carries
|
||||
the old leaf types, and will score differently against the renamed programme —
|
||||
the old types are no longer declared, so every affected leaf reads as an
|
||||
unmatched generic instead of the room it was.
|
||||
|
||||
This matters for artefacts not checked in at the time, notably the
|
||||
``evolved-3M*.dom`` harbor-house runs.
|
||||
|
||||
Mapping (chosen so each new prefix is unused in harbor-house *and* carries no
|
||||
adjacency semantics of its own — ``l``/``k``/``b``/``t`` do, ``f``/``a``/``g``
|
||||
do not — while preserving the original prefix-sharing structure, i.e. the two
|
||||
storage codes still share one prefix):
|
||||
|
||||
cr1 -> fr1 Common Room with Fireplace
|
||||
of -> ao Staff Office
|
||||
st1 -> gs1 Ground Floor Storage
|
||||
st2 -> gs2 First Floor Storage
|
||||
|
||||
Usage::
|
||||
|
||||
python experiments/migrate_ju3_rename.py path/to/evolved-3M.dom [...]
|
||||
python experiments/migrate_ju3_rename.py --check path/to/*.dom
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
RENAMES = {"cr1": "fr1", "of": "ao", "st1": "gs1", "st2": "gs2"}
|
||||
|
||||
|
||||
def migrate(path: Path, check_only: bool) -> int:
|
||||
"""Rewrite ``type:`` lines in place; return the number of leaves changed."""
|
||||
text = path.read_text()
|
||||
total = 0
|
||||
for old, new in RENAMES.items():
|
||||
# Anchored to a whole `type: <code>` line so a code appearing inside a
|
||||
# name, comment or unrelated scalar is never touched.
|
||||
text, n = re.subn(rf"^(\s*type: ){re.escape(old)}\s*$", rf"\g<1>{new}",
|
||||
text, flags=re.M)
|
||||
total += n
|
||||
if total and not check_only:
|
||||
path.write_text(text)
|
||||
return total
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("paths", nargs="+", type=Path)
|
||||
ap.add_argument("--check", action="store_true",
|
||||
help="report what would change without writing")
|
||||
args = ap.parse_args()
|
||||
|
||||
stale = 0
|
||||
for path in args.paths:
|
||||
if not path.is_file():
|
||||
print(f" {path}: NOT FOUND", file=sys.stderr)
|
||||
continue
|
||||
n = migrate(path, args.check)
|
||||
stale += bool(n)
|
||||
verb = "would rename" if args.check else "renamed"
|
||||
print(f" {path}: {verb} {n} leaf type(s)"
|
||||
if n else f" {path}: already current")
|
||||
if args.check and stale:
|
||||
print(f"\n{stale} file(s) still carry pre-ju3 codes.")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1887,13 +1887,18 @@ class Fitness:
|
|||
|
||||
def _load_programme(self, conf: dict) -> None:
|
||||
"""Populate ``_programme_cache`` from spaces section of conf dict."""
|
||||
from .programme import SpaceReq
|
||||
from .programme import SpaceReq, validate_codes
|
||||
_DW = (4.0, 1.0)
|
||||
_DP = (1.5, 0.5)
|
||||
spaces = conf.get("spaces") or {}
|
||||
if not spaces:
|
||||
self._programme_cache = None
|
||||
return
|
||||
# homemaker-py-ju3 (DESIGN.md §39.2): reject codes colliding with the
|
||||
# generic c/o/s type prefixes here too — Fitness parses conf["spaces"]
|
||||
# independently of programme._parse_spaces, so validating in only one
|
||||
# of the two would leave the other door open.
|
||||
validate_codes(spaces)
|
||||
reqs: dict = {}
|
||||
for code, c in spaces.items():
|
||||
sz = c.get("size") or [0.0, 1.0]
|
||||
|
|
|
|||
|
|
@ -59,8 +59,50 @@ def _pair(d: dict, key: str, default: tuple[float, float]) -> tuple[float, float
|
|||
return float(v[0]), float(v[1])
|
||||
|
||||
|
||||
# homemaker-py-ju3 (DESIGN.md §39.2): Urb's type system is prefix-based — a
|
||||
# leaf type starting with "c" is circulation, "o"/"s" is outside — and
|
||||
# programme room codes share that namespace. A code starting with one of these
|
||||
# letters is silently reinterpreted as a generic type, with three unannounced
|
||||
# consequences: ``graph.check_space_counts`` skips it entirely (so the room is
|
||||
# never required), ``Fitness.get_space_params`` returns the generic
|
||||
# ``*_circulation``/``*_outside`` parameters instead of the declared ones, and
|
||||
# ``dom.is_circulation``/``is_outside`` flip (changing value rate, crinkliness
|
||||
# treatment, and whether the leaf supplies daylight to neighbours).
|
||||
#
|
||||
# harbor-house shipped four such codes for years — ``cr1`` "Common Room with
|
||||
# Fireplace" had its declared 80 m² read as circulation's 0-30 m², and 14% of
|
||||
# the programme was silently optional. Refusing the code at load turns a silent
|
||||
# misread into a loud one. The other prefixes with semantics (l/k/b/t) are NOT
|
||||
# reserved: they only flavour adjacency heuristics and never discard a
|
||||
# requirement, so programme codes may use them freely.
|
||||
RESERVED_PREFIXES = ("c", "o", "s")
|
||||
|
||||
|
||||
def validate_codes(codes) -> None:
|
||||
"""Raise ``ValueError`` if any programme code collides with a generic type.
|
||||
|
||||
Called from both parse paths (``_parse_spaces`` and
|
||||
``fitness.Fitness._load_programme``) so a colliding code cannot enter the
|
||||
system through either door.
|
||||
"""
|
||||
bad = sorted(c for c in codes if c and c[0].lower() in RESERVED_PREFIXES)
|
||||
if not bad:
|
||||
return
|
||||
raise ValueError(
|
||||
"programme code(s) collide with Urb's reserved generic type prefixes "
|
||||
f"{RESERVED_PREFIXES} (c=circulation, o/s=outside): {bad}. "
|
||||
"Such a code is silently treated as a generic type: it is dropped from "
|
||||
"the required-space check, its declared size/width/proportion are "
|
||||
"replaced by the generic circulation/outside parameters, and it is "
|
||||
"valued at the circulation/outside rate. Rename the code to start with "
|
||||
"another letter (the name: field is free text and need not change). "
|
||||
"See DESIGN.md §39.2 / homemaker-py-ju3."
|
||||
)
|
||||
|
||||
|
||||
def _parse_spaces(conf: dict) -> dict[str, SpaceReq]:
|
||||
spaces = conf.get("spaces") or {}
|
||||
validate_codes(spaces)
|
||||
out: dict[str, SpaceReq] = {}
|
||||
for code, c in spaces.items():
|
||||
size = _pair(c, "size", (0.0, 1.0))
|
||||
|
|
|
|||
|
|
@ -73,16 +73,16 @@ def test_level_constraint_never_assigns_wrong_level():
|
|||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_cos_prefixed_cells_are_not_relabelled():
|
||||
# cr1 collides with the c* (circulation) convention the scorer counts against,
|
||||
# so it is skeleton — never relabelled and never a demand slot.
|
||||
conf = _conf({
|
||||
"cr1": {"size": [20.0, 4.0]},
|
||||
"b1": {"size": [16.0, 4.0]},
|
||||
})
|
||||
# A GENERIC circulation leaf is skeleton — never relabelled, never a demand
|
||||
# slot. This used to be written with a programme code ("cr1") that collided
|
||||
# with the c* prefix; programme codes can no longer do that at all, since
|
||||
# programme.validate_codes rejects them at load (homemaker-py-ju3, DESIGN.md
|
||||
# §39.2), so the exclusion is now tested with the generic type it is for.
|
||||
conf = _conf({"b1": {"size": [16.0, 4.0]}})
|
||||
fit = Fitness(conf=conf)
|
||||
root = _two_leaf_root("cr1", "b1")
|
||||
root = _two_leaf_root("C", "b1")
|
||||
fit.collapse_global(root)
|
||||
assert sorted(lf.type for lf in root.leaves()) == ["b1", "cr1"]
|
||||
assert sorted(lf.type for lf in root.leaves()) == ["C", "b1"]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ GOOD_SVG = textwrap.dedent(
|
|||
<g inkscape:groupmode="layer" inkscape:label="storey-0">
|
||||
<path d="M 4,0 L 4,8"/>
|
||||
<path d="M 4,5 L 10,5"/>
|
||||
<text x="2" y="4">cr1</text>
|
||||
<text x="2" y="4">fr1</text>
|
||||
<text x="7" y="2.5">k1</text>
|
||||
<text x="7" y="6.5">b1</text>
|
||||
</g>
|
||||
|
|
@ -55,7 +55,7 @@ SLOPPY_SVG = textwrap.dedent(
|
|||
<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"/>
|
||||
<text x="2" y="4">cr1</text>
|
||||
<text x="2" y="4">fr1</text>
|
||||
<text x="7" y="2.5">k1</text>
|
||||
<text x="7" y="6.5">b1</text>
|
||||
</g>
|
||||
|
|
@ -92,14 +92,14 @@ def test_composes_synthetic_partition_and_scores(tmp_path):
|
|||
root = compose(boundary, storeys)
|
||||
|
||||
leaves = root.leaves()
|
||||
assert sorted(leaf.type for leaf in leaves) == ["b1", "cr1", "k1"]
|
||||
assert sorted(leaf.type for leaf in leaves) == ["b1", "fr1", "k1"]
|
||||
|
||||
# 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())
|
||||
assert reloaded_types == ["b1", "cr1", "k1"]
|
||||
assert reloaded_types == ["b1", "fr1", "k1"]
|
||||
|
||||
# geometry is sane: leaf areas sum to the (wall-inset) plot area
|
||||
total = sum(geometry.area(leaf) for leaf in reloaded.leaves())
|
||||
|
|
@ -123,7 +123,7 @@ def test_snaps_sloppy_hand_traced_lines(tmp_path):
|
|||
storeys = parse_svg(str(svg_path))
|
||||
root = compose(boundary, storeys, tol=0.15)
|
||||
|
||||
assert sorted(leaf.type for leaf in root.leaves()) == ["b1", "cr1", "k1"]
|
||||
assert sorted(leaf.type for leaf in root.leaves()) == ["b1", "fr1", "k1"]
|
||||
|
||||
# a tighter tolerance than the sketch's slop should fail to find the cuts
|
||||
boundary2 = dom.load(str(boundary_path))
|
||||
|
|
|
|||
50
tests/test_programme.py
Normal file
50
tests/test_programme.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
"""Tests for programme.py parsing and validation."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from homemaker_layout import fitness, programme
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# homemaker-py-ju3 / DESIGN.md §39.2 — reserved generic type prefixes
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_validate_codes_accepts_non_reserved_prefixes():
|
||||
programme.validate_codes(["b1", "k1", "t3", "la1", "me1", "n", "fr1", "ao"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code", ["cr1", "of", "st1", "C", "O", "s2"])
|
||||
def test_validate_codes_rejects_reserved_prefixes(code):
|
||||
"""A colliding code must fail LOUDLY: silently reinterpreting it as a
|
||||
generic type is the whole bug (§39.2)."""
|
||||
with pytest.raises(ValueError, match="reserved generic type prefixes"):
|
||||
programme.validate_codes([code])
|
||||
|
||||
|
||||
def test_reserved_prefix_rejected_by_both_parse_paths():
|
||||
"""programme._parse_spaces and fitness.Fitness._load_programme parse
|
||||
conf["spaces"] independently — validating only one would leave the other
|
||||
door open."""
|
||||
conf = {"spaces": {"cr1": {"size": [80.0, 10.0]}}}
|
||||
with pytest.raises(ValueError, match="reserved generic type prefixes"):
|
||||
programme._parse_spaces(conf)
|
||||
with pytest.raises(ValueError, match="reserved generic type prefixes"):
|
||||
fitness.Fitness(conf=conf)
|
||||
|
||||
|
||||
def test_semantic_but_unreserved_prefixes_are_allowed():
|
||||
"""l/k/b/t carry adjacency semantics but never discard a requirement, so
|
||||
programme codes may use them freely — only c/o/s are reserved."""
|
||||
reqs = programme._parse_spaces({"spaces": {
|
||||
"l1": {"size": [20.0, 4.0]}, "k1": {"size": [12.0, 3.0]},
|
||||
"b1": {"size": [16.0, 4.0]}, "t1": {"size": [3.0, 1.0]},
|
||||
}})
|
||||
assert sorted(reqs) == ["b1", "k1", "l1", "t1"]
|
||||
|
||||
|
||||
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))
|
||||
Loading…
Add table
Reference in a new issue