diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index f35a11a..6a4d8ca 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -41,7 +41,7 @@ {"_type":"issue","id":"homemaker-py-3qj","title":"Programmes do not declare bedroom/treatment-room to toilet adjacency, so the Brand adaptability credit is never earned","description":"Established while settling homemaker-py-sel. A toilet adjacent to a sleeping or treatment room is a positive even with no door between them -- Stewart Brand, How Buildings Learn: the building may need it knocked through later, and the adjacency is what makes that possible.\\n\\nThe engine already supports this. fitness._evaluate_full runs graph.check_adjacency against graph_base_pre, the UNFILTERED adjacency graph, so a declared adjacency requirement is satisfied by a neighbouring room regardless of the edges graph.has_circulation later strips for routing. (The stripping is correct on its own terms -- you cannot walk through a bedroom to reach the loo.)\\n\\nBut it is only ever scored where a programme DECLARES it, and only one corpus programme does:\\n programme-house (+ derivatives) t1 -\u003e adjacency: [b1] DECLARED\\n harbor-house t (Bathroom, count 6) vs r / n (sleeping) not declared\\n maple-court t, tt vs r / n not declared\\n health-centre t9 / t10 vs tr1 (Treatment Room) not declared\\n\\nSo three of the four real programmes leave the adaptability credit on the table: the search has no reason to place a WC next to a dormitory or a treatment room, because nothing scores it.\\n\\nThis is programme authoring, not an engine change, and it is independent of the usage-key work -- it can land before or after.","acceptance_criteria":"harbor-house, maple-court and health-centre declare the sleeping/treatment-to-toilet adjacencies their briefs imply; re-baselined so the change in fail counts is attributed rather than absorbed.","status":"open","priority":2,"issue_type":"task","owner":"noreply@anthropic.com","created_at":"2026-08-26T12:33:39Z","created_by":"Claude","updated_at":"2026-08-26T12:33:39Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"homemaker-py-fdp","title":"constructive_topology is not bit-reproducible on the assign_solver=cpsat path","description":"Established while fixing §39.5. Narrowed but not closed:\\n\\n- cpsat.solve_room_labels IS now reproducible on every captured instance, after two fixes: sorting the model-build order (neighbors[slot] is a set of dom.Node, which hashes by id() -- a memory address -- so raw iteration made the model order vary and CP-SAT returned a different equally-optimal assignment each run), and adding max_deterministic_time alongside the wall-clock cap.\\n- operators.constructive_topology(assign_solver='greedy') IS reproducible.\\n- operators.constructive_topology(assign_solver='cpsat') is NOT: identical seed in the same process gives different leaf-type signatures across runs.\\n\\nSo something upstream of the solver in _assign_adjacency_aware still varies on the cpsat branch. Disabling _cpsat_relabel_settled does not fix it, so it is the first call site. Suspect another id()-hashed set of Nodes feeding slot/neighbour ordering.\\n\\nPlausible contributor to homemaker-py-b8g (parallel/BLAS non-determinism in n_workers\u003e1 runs) -- same id-keying hazard class as the documented geometry._cache issue. Meanwhile tests/test_operators.py::test_assign_cpsat_matches_or_beats_greedy_secondary_adjacency averages three repeats instead of asserting on one run.","acceptance_criteria":"constructive_topology(assign_solver='cpsat') produces identical output for identical seeds in-process and across processes; the A/B test can go back to a single-run assertion.","status":"open","priority":2,"issue_type":"bug","owner":"noreply@anthropic.com","created_at":"2026-08-26T10:06:55Z","created_by":"Claude","updated_at":"2026-08-26T10:06:55Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"homemaker-py-0wr","title":"Re-check any harbor-house A/B decided by a small margin before the §39.4 namespace fix","description":"Until §39.4, harbor-house scored against a 32-instance EFFECTIVE programme -- cr1/of/st1/st2 (14% of room instances) were dropped by the count check and mis-parameterised by the generic c/o/s prefix rule. Any harbor A/B decided by a narrow margin was therefore decided against a programme missing a seventh of its rooms.\\n\\nOne confirmed casualty already: 2g7.5's CP-SAT seeder win (§37.7). Measured over 6 seeds on the corrected programme, greedy 102 / cpsat 114 -- cpsat now LOSES; on the old 32-instance programme it was 98/99, a tie, so the recorded win was marginal from the start. Control: on namespace-clean maple-court cpsat still wins 144/156, so the solver did not regress. Test marked xfail with a companion maple-court assertion; both assign_solver flags stay default off.\\n\\nWorth re-checking with the same lens: §13.9/§13.11 floors, §17/§20 collapse A/Bs, §23 ruin-recreate, §29/§30 beam width, §37.1 tiering -- anything whose harbor arm was close.","acceptance_criteria":"Each narrow-margin harbor result either re-measured on the corrected programme or annotated in DESIGN.md as pre-§39.4.","status":"open","priority":2,"issue_type":"task","owner":"noreply@anthropic.com","created_at":"2026-08-26T09:44:55Z","created_by":"Claude","updated_at":"2026-08-26T09:44:55Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"homemaker-py-1i8","title":"Missing-space fail cascade weights rooms by patterns.config verbosity, not by design intent","description":"graph.check_space_counts emits, per missing room instance: 2 base fails ('missing required space: X' + '(critical)') plus one 'would need \u003ccheck\u003e' placeholder for each of size/width/proportion the programme HAPPENS to declare (has_size/has_width/has_proportion are literally 'size' in c etc. from the YAML). So a missing room costs 3 to 5 fails depending only on how many optional keys the author typed. Under value *= 0.5**len(failures) that is a 4x difference in fitness weight between two single rooms. Concretely in programme-house: missing b1 (declares size+width+proportion) = 5 fails = 1/32 penalty; missing t2 (declares size only) = 3 fails = 1/8 penalty. Same for harbor-house: n and cr1 cost 5 each, r and t cost 4 each. The tiered comparator inherits this -- n_hard is dominated by these cascades, so the primary search key is weighted by YAML verbosity.","design":"Either emit exactly one fail per missing instance (and let the cascade placeholders be informational, not counted), or normalise the cascade to a fixed count per instance independent of declared keys. Note this changes every historical fail-count baseline in DESIGN.md, so it needs its own A/B and a recorded re-baseline.","status":"open","priority":2,"issue_type":"bug","owner":"noreply@anthropic.com","created_at":"2026-08-25T23:15:00Z","created_by":"Claude","updated_at":"2026-08-25T23:15:00Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"homemaker-py-1i8","title":"Missing-space fail cascade weights rooms by patterns.config verbosity, not by design intent","description":"graph.check_space_counts emits, per missing room instance: 2 base fails ('missing required space: X' + '(critical)') plus one 'would need \u003ccheck\u003e' placeholder for each of size/width/proportion the programme HAPPENS to declare (has_size/has_width/has_proportion are literally 'size' in c etc. from the YAML). So a missing room costs 3 to 5 fails depending only on how many optional keys the author typed. Under value *= 0.5**len(failures) that is a 4x difference in fitness weight between two single rooms. Concretely in programme-house: missing b1 (declares size+width+proportion) = 5 fails = 1/32 penalty; missing t2 (declares size only) = 3 fails = 1/8 penalty. Same for harbor-house: n and cr1 cost 5 each, r and t cost 4 each. The tiered comparator inherits this -- n_hard is dominated by these cascades, so the primary search key is weighted by YAML verbosity.","design":"Either emit exactly one fail per missing instance (and let the cascade placeholders be informational, not counted), or normalise the cascade to a fixed count per instance independent of declared keys. Note this changes every historical fail-count baseline in DESIGN.md, so it needs its own A/B and a recorded re-baseline.","status":"closed","priority":2,"issue_type":"bug","owner":"noreply@anthropic.com","created_at":"2026-08-25T23:15:00Z","created_by":"Claude","updated_at":"2026-08-29T09:33:24Z","closed_at":"2026-08-29T09:33:24Z","close_reason":"Fixed by emitting all three quality placeholders unconditionally (DESIGN.md\n§38.12). A fixed 5 fails per missing instance, independent of how the programme\nwas spelled.\n\nThe decisive evidence is that the two paths disagreed about the same room. A\nPRESENT room is checked on all three qualities regardless of declaration --\nget_space_params fills width and proportion from defaults, deriving width from\nsize when absent, so programme-house's t2 declares `size:` alone and still gets\na real width target of 1.633 it can fail on. Missing, it emitted one placeholder\nwhere b1 emitted three. The cascade stands in for the checks that could not run,\nand it was standing in for the wrong number of them.\n\nCorpus: 36 of 67 codes were under-counted, 31 already at 5. Max weight ratio\nbetween two single rooms goes 4x -\u003e 1x (programme-house), 2x -\u003e 1x (harbor,\nmaple), 1x -\u003e 1x (health-centre, already uniform).\n\nThis makes fail counts LARGER, which is correct and not a regression:\n harbor evolved-3M-nols-3 82/37h -\u003e 84/39h\n harbor generated 155/128h -\u003e 174/147h\n harbor evolved-3M 131/87h -\u003e 144/100h\n maple generated 126/17h -\u003e unchanged (no missing instances)\n\nNOT taken: the \"one fail per missing instance, placeholders informational\"\noption. It fixes the verbosity dependence too, but silently rescales a missing\nroom from 1/32 to 1/2 -- the same weight as one crinkliness fail. Whether a\nmissing required room SHOULD cost 1/32 is a real question and a separate one;\nbundling it here would have changed the objective's priorities under cover of a\nbug fix. Magnitude left exactly where it was. Filed as a follow-up.\n","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"homemaker-py-gvb","title":"Crinkliness is mis-tiered as SOFT, but most crinkliness fails are topological (zero-exposure) and unreachable by the inner loop","description":"fitness._SOFT_FAIL_MARKERS lists ' crinkliness' as SOFT, defined in 37.1 as 'a continuous per-leaf shape metric the inner-loop ratio solve can improve without changing the tree'. That is false for the zero-exposure case: a leaf with no daylit wall cannot be given one by ANY ratio assignment -- it needs a topology change, which is the document's own definition of HARD. Measured share of crinkliness fails that are zero-exposure: harbor-house 60% (36 of 60), maple-court 65% (83 of 127), health-centre 100% (35 of 35); on a converged 20k harbor run, 14 of 17 (82%). Since crinkliness is the single largest fail category (48% of the residual per 13.11), the tiered comparator from 2g7.3 is mis-informed about the largest block of fails it sorts: it tells the search 'these ~40 soft fails are polishable' when two-thirds of them are structurally unreachable, and n_soft is therefore not the polish-budget signal it was designed to be.","design":"Split the crinkliness fail into two strings (or tier it dynamically on area_outside==0) so zero-exposure counts HARD and wrong-ratio counts SOFT. classify_fail_tier is string-based, so the cleanest fix is emitting a distinct fail string for the zero-exposure case -- which also makes the condition visible in .fails output, where today it is indistinguishable from an ordinary shape miss.","acceptance_criteria":"Distinct fail string for zero-exposure; classify_fail_tier maps it HARD; re-run the 37.1 tiered-vs-flat A/B, whose hard/soft split changes materially under the corrected tiering.","status":"open","priority":2,"issue_type":"bug","owner":"noreply@anthropic.com","created_at":"2026-08-25T23:15:00Z","created_by":"Claude","updated_at":"2026-08-25T23:15:00Z","dependencies":[{"issue_id":"homemaker-py-gvb","depends_on_id":"homemaker-py-ssz","type":"blocks","created_at":"2026-08-25T23:15:13Z","created_by":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"homemaker-py-57z","title":"Run homemaker-py-2g7.7 acceptance benchmark against evolved-3M-nols-3 plateau seed (needs live ANTHROPIC_API_KEY)","description":"homemaker-py-2g7.7's acceptance criteria requires an end-to-end run of the LLM repair loop against the evolved-3M-nols-3 15-hard-fail plateau seed (level 0 not connected + me1 on wrong level, which survived \u003e1M blind evals / ~2.4 days in the original evolve-3M-nols-3.log run): repair loop reduces hard-fail count within \u003c=20 LLM calls, edit-DSL rejects malformed proposals (should already be covered by unit tests), and an A/B at equal native-eval budget shows strictly better final fails on \u003e=2/3 seeds vs a no-repair control. This requires a live Claude API call (client.messages.create against claude-opus-5) which needs ANTHROPIC_API_KEY set or 'ant auth login' completed -- neither was available in the sandbox that scaffolded the feature (no ANTHROPIC_API_KEY env var, no ant CLI installed). Run this once credentials are available, then close out homemaker-py-2g7.7's acceptance criteria referencing the results.","status":"open","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-08-06T07:05:45Z","created_by":"Bruno Postle","updated_at":"2026-08-06T07:05:45Z","dependencies":[{"issue_id":"homemaker-py-57z","depends_on_id":"homemaker-py-2g7.7","type":"blocks","created_at":"2026-08-06T08:06:12Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"homemaker-py-8oq","title":"Review homemaker-py-2g7.7 LLM-repair-operator plan with a more expensive/capable model","description":"A detailed implementation plan for the LLM repair operator (homemaker-py-2g7.7) was drafted in a planning session on 2026-08-05: new src/homemaker_layout/llm_repair.py module (edit DSL over swap/divide/retype/rotate/undivide with explicit (level,path) targeting, apply_script/validate_script interpreter, prompt builder serializing dom+fails+programme summary, propose_repairs() with an injectable call_fn and a (genome.signature, sorted fails) cache), driver.py stagnation-trigger wiring modeled on the existing restart_patience/last_improve clock, evolve.py CLI flags (--llm-repair, --llm-repair-patience, --llm-repair-max-calls, --llm-repair-model default claude-opus-5), an anthropic SDK dependency addition, and a DESIGN.md writeup. Plan text is saved at /home/bruno/.claude/plans/glowing-snuggling-flute.md from that session. Before implementing, the user wants a second, more capable/expensive model to review this plan for soundness (DSL completeness vs the 19 existing mutate_* primitives, correctness of the path-addressing scheme, the stagnation-trigger placement in driver.py's search() loop, cache-key choice, and whether the phasing that defers the live acceptance-criteria benchmark to a separate follow-up issue is the right call) before a new implementation session begins.","status":"open","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-08-06T07:04:54Z","created_by":"Bruno Postle","updated_at":"2026-08-06T07:04:54Z","dependency_count":0,"dependent_count":1,"comment_count":0} @@ -95,6 +95,7 @@ {"_type":"issue","id":"homemaker-py-nyb","title":"High-locality topology operators (mutation + subtree crossover)","description":"DESIGN.md §5, §7 Phase 2, §8.4. Mutation moves: divide/undivide leaf, swap children, rotate cut, retype leaf, per-floor delta edits, storey add/delete (cf. Urb Mutate.pm — but geometry sliding belongs to the inner loop, not the operator set). Crossover: area-matched subtree exchange (a subtree = a contiguous region, so crossover is meaningful — Crossover.pm). Operators must be high-locality: small genome change =\u003e small phenotype change, so warm-started inner loops stay cheap.","acceptance_criteria":"Each operator produces valid genomes (oracle scores them without error); locality measured (mean fitness/geometry perturbation per operator)","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:37:27Z","created_by":"Bruno Postle","updated_at":"2026-06-12T13:07:37Z","started_at":"2026-06-12T12:54:23Z","closed_at":"2026-06-12T13:07:37Z","close_reason":"operators.py lands: 7 mutations + area-matched crossover, valid-by-construction via genome.encode repair. 115/115 oracle-valid children; locality measured: geom-pert 0.07-0.33 per op, fitness-pert 0.68-0.99 (0.5^n cliff flags raw moves — warm restart + penalty reshaping confirmed load-bearing). Also fixed dom._link stale below-links on structural mutation.","dependencies":[{"issue_id":"homemaker-py-nyb","depends_on_id":"homemaker-py-k2g","type":"blocks","created_at":"2026-06-12T00:39:36Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"homemaker-py-k2g","title":"Topology genome: base-floor tree + per-floor deltas + type assignment","description":"DESIGN.md §5.2, §7 Phase 2. Genome = base-floor slicing topology (primary) + per-leaf type assignment + per-floor divide/undivide deltas (Below-inheritance as regulariser; cut owned by lowest storey where its path is divided — §10). Must round-trip to/from dom.py Node trees so the oracle and inner loop consume it directly. Includes storey count and per-floor type overrides.","acceptance_criteria":"Genome \u003c-\u003e .dom round-trip on all 35 corpus files preserves fitness; multi-storey wall stacking preserved","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:37:26Z","created_by":"Bruno Postle","updated_at":"2026-06-12T12:52:34Z","started_at":"2026-06-12T10:55:21Z","closed_at":"2026-06-12T12:52:34Z","close_reason":"genome.py encode/decode lands. 35/35 oracle fitness parity after round-trip (flag-on); genome fixed-point + owned-projection tests. Dead-field discovery: corpus upper storeys carry drifted dead divisions (97) and rotations (187) — canonicalised by decode, validated fitness-neutral.","dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"homemaker-py-d0s","title":"Experiment: inner-loop optimiser bake-off at equal oracle budgets","description":"DESIGN.md §7 Phase 1, §8.3. DOF is only ~rooms-1 (6–7 on corpus). Compare Nelder-Mead vs CMA-ES vs batched multi-start pattern search at equal oracle-call budgets, measuring fitness gained per oracle call and wall-clock (batch-friendliness matters — §4.6). Measure, don't commit blind.","acceptance_criteria":"Table of fitness-per-budget across \u003e=3 candidates; one optimiser chosen and recorded in DESIGN.md","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:36:59Z","created_by":"Bruno Postle","updated_at":"2026-06-13T08:48:13Z","started_at":"2026-06-12T21:22:15Z","closed_at":"2026-06-13T08:48:13Z","close_reason":"Bake-off complete: CMA-ES confirmed as Phase 1/2 optimiser. NM wins quality per eval but sequential architecture incompatible with batching (§4.6). Compass stalls on narrow valleys. Results in DESIGN.md §8.3 and experiments/bakeoff_innerloop.*","dependencies":[{"issue_id":"homemaker-py-d0s","depends_on_id":"homemaker-py-1p0","type":"blocks","created_at":"2026-06-12T00:39:35Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"homemaker-py-3i3","title":"Is a missing required room correctly weighted at 1/32 of fitness?","description":"Split out of homemaker-py-1i8 (DESIGN.md §38.12), which deliberately fixed only the verbosity dependence and left the magnitude untouched.\n\nA missing required room now costs a fixed 5 fails, so under value *= 0.5 ** len(failures) it is a 1/32 penalty -- 32x the weight of a single crinkliness or proportion fail. Nobody has ever checked that ratio against intent. It may well be right (a missing room is a brief not met, arguably the most serious defect available), but it is currently an accident of how the cascade was built rather than a decision.\n\nThe question is what the objective should SAY, so it wants an owner ruling first and a measurement second: does the cascade's weight crowd out geometry-quality signal in the region where the search actually operates? Note most corpus layouts carry several missing instances at once, so the compounding is steep -- three missing rooms is 1/32768.","acceptance_criteria":"A recorded decision on the intended weight of a missing required room relative to a quality fail, with the cascade's fixed count set to match it; if the answer is 'as-is', that is recorded as a decision rather than left as an accident.","status":"open","priority":3,"issue_type":"task","owner":"noreply@anthropic.com","created_at":"2026-08-29T09:33:24Z","created_by":"Claude","updated_at":"2026-08-29T09:33:24Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"homemaker-py-1ue","title":"test_classify_fail_tier_covers_full_corpus passes only on a machine that has already run the scorer","description":"The test globs examples/**/*.fails and asserts checked \u003e 0, but .fails files are generated artefacts written by homemaker-fitness next to the .dom being scored. A clean checkout has none, so the test fails with 'assert 0 \u003e 0' -- it is asserting on the state of the developer's working tree, not on the code.\n\nSplit the two things it is doing: (1) that classify_fail_tier accepts every fail string the evaluator can emit, which should run against fail strings generated in-test from corpus .dom files (or a checked-in fixture list), and (2) the corpus-artefact sweep, which if it is worth keeping should skip rather than fail when no .fails exist.","acceptance_criteria":"The test passes in a fresh clone with no .fails present, and still fails if classify_fail_tier is given an unclassifiable fail string.","status":"closed","priority":3,"issue_type":"bug","owner":"noreply@anthropic.com","created_at":"2026-08-28T22:41:25Z","created_by":"Claude","updated_at":"2026-08-29T07:11:24Z","closed_at":"2026-08-29T07:11:24Z","close_reason":"Fixed. The test globbed examples/**/*.fails and asserted checked \u003e 0, but git\ntracks ZERO .fails files -- they are artefacts homemaker-fitness writes beside a\n.dom. Its own docstring claimed to cover \"every fail string ever emitted into a\nchecked-in native .fails file\", describing files that by design never exist in\nthe repo, so it could only ever pass on a machine that had already run the\nscorer, and failed `assert 0 \u003e 0` in a fresh clone.\n\nSplit into the three things it was conflating:\n\n test_classify_fail_tier_covers_every_fail_the_evaluator_emits\n GENERATES fails by scoring three corpus layouts chosen for breadth of\n failure kinds (harbor evolved-3M-nols-3 + generated, maple generated),\n and requires at least 8 distinct kinds so it cannot silently narrow.\n test_classify_fail_tier_rejects_an_unknown_fail_string\n the guard is only worth something if an unclassifiable string raises.\n test_classify_fail_tier_checks_any_native_fails_artefacts_present\n still sweeps .fails when a working tree happens to carry them, but never\n requires them.\n\nVerified by moving every .fails out of the tree and re-running: passes with none\npresent. Full suite now 376 passed, 0 failed.\n","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"homemaker-py-bdf","title":"7 tests fail in a fresh clone: evolved-3M*.dom fixtures are referenced but not checked in","description":"On a clean checkout (no local scratch), 'pytest' gives 7 failed / 331 passed / 72 skipped. All 7 trace to missing .dom fixtures that tests reference but the repo does not contain: examples/harbor-house/evolved-3M.dom and evolved-3M-nols-3.dom (the 3M-eval run outputs cited throughout DESIGN.md). tests/test_conn_grade.py raises FileNotFoundError on them (4 tests); tests/test_collapse_insearch.py fails for the same reason (2 tests); and tests/test_fitness.py::test_classify_fail_tier_covers_full_corpus asserts 'checked \u003e 0' but finds zero corpus files to walk, so it fails with 'assert 0 \u003e 0' -- i.e. it is a vacuous test that only appeared to pass on a machine that happened to have the artefacts. Confirmed pre-existing by stashing all local changes and re-running on clean HEAD.","design":"Either commit the two .dom artefacts (they are small YAML), regenerate them from a documented command and commit that, or skip the affected tests via pytest.importorskip-style guards when the fixture is absent. The corpus-walk test should skip rather than assert when it finds nothing, so a fresh clone does not report a false failure -- but only after the fixture question is settled, since making it skip silently would hide real coverage loss.","acceptance_criteria":"pytest is green on a fresh clone with no local artefacts, and test_classify_fail_tier_covers_full_corpus either checks a real corpus or skips explicitly.","status":"closed","priority":3,"issue_type":"bug","owner":"noreply@anthropic.com","created_at":"2026-08-25T23:18:02Z","created_by":"Claude","updated_at":"2026-08-28T22:41:24Z","closed_at":"2026-08-28T22:41:24Z","close_reason":"Fixed by the owner: 'Add missing test files' (0d4ae7a) checked in the evolved-3M*.dom fixtures. Fresh-clone failures went 7 -\u003e 2, and the 4 conn_grade tests plus one collapse_insearch test now pass.\n\nThe 2 that remain are NOT fixture problems and are tracked separately:\n - test_collapse_insearch_reproduces_94g_finish_time_result -- stale numeric baseline, see homemaker-py-ut5\n - test_classify_fail_tier_covers_full_corpus -- asserts checked \u003e 0 over examples/**/*.fails, which are generated artefacts absent from a clean tree, so the test passes only on a machine that has run the scorer. Filed separately.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"homemaker-py-4ok","title":"run_staged_search.py rescore still MISMATCHes on baseline (LEAFSHARE=0/MULTIUSE=0) arms","description":"Follow-up to homemaker-py-7ua. That fix added collapse_insearch=True to\nrun_staged_search.py's monkeypatched fitness.load_config, but the monkeypatch\nis only installed `if leaf_share or multi_use:`. driver.search_staged has NO\ncollapse_insearch parameter at all -- every internal search() call it makes\nalways inherits collapse_insearch=True unconditionally, regardless of the\nleaf_sharing/multi_use flags. So on a baseline run (LEAFSHARE=0, MULTIUSE=0,\nthe default), fitness.load_config is never patched, and _native_score's\nrescore still silently uses collapse_insearch=False (no example\npatterns.config sets it) -- the exact same MISMATCH the 7ua fix was meant to\neliminate, just on the arm the fix doesn't touch.\n\nConfirmed empirically: `URB_NO_OCCLUSION=1 python3 experiments/run_staged_search.py\nexamples/programme-house 150 0 ...` (no env vars) reports MISMATCH before AND\nafter the 7ua fix, since the patched _load_with_flags function is never\ninstalled for that arm.\n\nMitigating factor: in the small-budget cases tested so far, the fail COUNT\n(what the various run_*_ab.sh harnesses grep as their \"authoritative metric\")\nwas identical between collapse_insearch=True and False rescoring -- only the\ncontinuous score shifted in a low decimal place, which is invisible to those\ngreps. But the underlying mechanism (collapse_global relabels leaf types\nin-place before any check runs, so downstream adjacency/missing/level checks\nsee genuinely different trees) means fail count CAN diverge in principle --\n7ua's own cited historical example (91f: 38 vs 34 fails on a WORKERS=4\nharbor-house run) showed count divergence, albeit confounded with known\nWORKERS parallel non-determinism (homemaker-py-b8g).\n\nFix: make run_staged_search.py's rescore always pin collapse_insearch=True\n(not gated behind leaf_share/multi_use), since search_staged's inner\nevaluator always runs with it regardless of those flags. Simplest: hoist the\n`conf[\"collapse_insearch\"] = True` line out of the `if leaf_share or\nmulti_use:` gate into an unconditional monkeypatch install (or an\nunconditional post-load override in _native_score itself).\n\nAlso worth: audit whether any of run_leafshare_ab.sh / run_9gp_ab.sh /\nrun_c3g_ab.sh / run_depthbal_ab.sh / run_1s3_confirm_staged.sh's BASELINE\narms (which don't set LEAFSHARE/MULTIUSE) had their reported fail counts\nactually affected historically, not just theoretically -- e.g. by rescoring\na few archived .dom outputs (if kept) with the corrected conf and diffing\nfail counts against the published numbers.","status":"open","priority":3,"issue_type":"bug","owner":"bruno@postle.net","created_at":"2026-08-05T11:25:30Z","created_by":"Bruno Postle","updated_at":"2026-08-05T11:25:30Z","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/DESIGN.md b/DESIGN.md index 2e31a4e..c578f98 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -5433,6 +5433,68 @@ complaint is narrower and is about search mechanics rather than truth — a hard flat where it should be merely low. That is worth a separate issue; it is not the calibration fault this section spent its length chasing. +### 38.12 The missing-space cascade was weighted by YAML verbosity (`homemaker-py-1i8`) + +`graph.check_space_counts` emitted, per missing room instance, two base failures +plus **one placeholder for each optional key the author happened to type**: + +```python +if req.has_size: failures.append(f"missing {mid}: would need size check") +if req.has_width: failures.append(...) +if req.has_proportion: failures.append(...) +``` + +`has_size`/`has_width`/`has_proportion` are literally `"size" in c` from the +YAML. So a missing room cost 3, 4 or 5 fails depending on nothing but how +verbosely its space was written, and under `value *= 0.5 ** len(failures)` that +is a **4× difference in penalty between two single rooms**. The tiered +comparator inherits it directly, because `n_hard` is dominated by these +cascades — so the search's *primary key* was partly a measure of config style. + +**Why it is unambiguously wrong, not merely arbitrary.** A present room is +checked on all three qualities regardless of what was declared: +`get_space_params` fills width and proportion from defaults, deriving width from +size when it is absent. programme-house's `t2` declares `size:` alone and still +receives a real width target of **1.633** which it can fail on. So the two paths +disagreed about the same room: present, it faces three checks; missing, it +emitted one placeholder. The cascade is supposed to stand in for the checks that +could not be run, and it was standing in for the wrong number of them. + +**Fix: emit all three placeholders, always.** A fixed 5 fails per missing +instance, mirroring the present-room path. + +| programme | before | after | max weight ratio, before → after | +|---|---|---|---| +| programme-house | 3..5 | 5 | 4× → 1× | +| harbor-house | 4..5 | 5 | 2× → 1× | +| maple-court | 4..5 | 5 | 2× → 1× | +| health-centre | 5..5 | 5 | 1× → 1× | + +36 of the corpus's 67 codes were under-counted; 31 were already at 5. + +**This makes fail counts LARGER, and that is the point.** It is a correctness +fix, not an improvement, and anyone reading the new numbers as a regression has +misread them: + +| layout | before (total/hard) | after | +|---|---|---| +| harbor `evolved-3M-nols-3.dom` | 82 / 37 | **84 / 39** | +| harbor `generated.dom` | 155 / 128 | **174 / 147** | +| harbor `evolved-3M.dom` | 131 / 87 | **144 / 100** | +| maple `generated.dom` | 126 / 17 | 126 / 17 (no missing instances) | + +**Note the alternative that was NOT taken.** `1i8` also offered "emit exactly one +fail per missing instance and let the placeholders be informational". That would +have fixed the verbosity dependence too, but it silently rescales how much a +missing room matters — from 1/32 to 1/2, the same weight as a single crinkliness +fail. Whether a missing required room *should* cost 1/32 is a real question, and +a separate one; conflating it with this fix would have changed the objective's +priorities under cover of a bug fix. The magnitude is left exactly where it was. + +**Baselines.** Every historical corpus fail count is invalidated again, on top of +§39.4 and §38.10/§38.11. This is why the cold-start re-baseline is worth running +*after* the objective work rather than before it. + ## 39. Config audit: requirements that actively fight the engine (`homemaker-py-ju3`) — measured 2026-08-25 The corpus `patterns.config` targets and `costs.config` values were estimated diff --git a/src/homemaker_layout/graph.py b/src/homemaker_layout/graph.py index bd85cd9..e366c64 100644 --- a/src/homemaker_layout/graph.py +++ b/src/homemaker_layout/graph.py @@ -556,8 +556,10 @@ def check_space_counts( ``check_space_counts`` in ``ProgrammeDriven.pm:156-215``. Returns ``(failures, missing_ids)`` where: - - ``failures`` is the stacked failure list (2 base + per-quality per missing - space, up to ~7 per missing space; also "too many" for excess spaces). + - ``failures`` is the stacked failure list: per missing instance, 2 base + failures plus one placeholder for each of the three quality checks it + would have faced -- a FIXED 5, independent of how the programme was + spelled (homemaker-py-1i8); also "too many" for excess spaces. - ``missing_ids`` is the list of virtual space ids used to suppress false adjacency/level/vertical failures for absent spaces. @@ -602,13 +604,26 @@ def check_space_counts( failures.append(f"missing required space: {mid}") failures.append(f"missing required space: {mid} (critical)") missing.append(mid) - # Per-quality failures (1 each for explicitly configured params) - if req.has_size: - failures.append(f"missing {mid}: would need size check") - if req.has_width: - failures.append(f"missing {mid}: would need width check") - if req.has_proportion: - failures.append(f"missing {mid}: would need proportion check") + # One placeholder per quality check the missing room WOULD have + # faced -- always all three (homemaker-py-1i8, DESIGN.md §38.12). + # + # These used to be gated on req.has_size/has_width/has_proportion, + # which record only whether the author TYPED the key in + # patterns.config, not whether the requirement exists. It always + # exists: `get_space_params` fills width and proportion from + # defaults (or derives width from size), so a PRESENT room is + # checked on all three however its config was spelled -- + # programme-house's `t2` declares `size:` alone and still gets a + # real width target of 1.633 that it can fail on. + # + # So the missing path must mirror the present path. Gating it + # made one missing room cost 3 fails and another 5, and under + # `value *= 0.5 ** len(failures)` that is a 4x difference in + # penalty between two single rooms decided by YAML verbosity -- + # inherited by the tiered comparator, whose primary key n_hard + # is dominated by these cascades. + for check in ("size", "width", "proportion"): + failures.append(f"missing {mid}: would need {check} check") elif actual > expected: failures.append( diff --git a/tests/test_graph.py b/tests/test_graph.py index 8bcdc8a..ed5c388 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -168,3 +168,57 @@ def test_two_phase_build_graphs_independent(): assert pre_nodes_l2 == 7 assert post_nodes_l2 == 3 + + +# --------------------------------------------------------------------------- # +# homemaker-py-1i8 / DESIGN.md §38.12 — the missing-space cascade must not be +# weighted by how verbosely the programme was written. +# --------------------------------------------------------------------------- # +def _missing_fails(declared: dict) -> list[str]: + """Fails for a bare plot that declares one required room, absent.""" + from homemaker_layout.graph import check_space_counts + from homemaker_layout.programme import _parse_spaces + + reqs = _parse_spaces({"spaces": {"x1": dict({"usage": "living"}, **declared)}}) + root = dom.Node(node=[[0, 0], [6, 0], [6, 6], [0, 6]], type="O") + fails, missing = check_space_counts(root, reqs) + assert missing, "the room should be reported missing" + return fails + + +def test_missing_space_cost_is_independent_of_declared_keys(): + """A missing room costs the same whether or not the author typed the + optional keys. + + It used to cost 2 base + one placeholder per key PRESENT IN THE YAML, so a + room declaring size/width/proportion cost 5 fails and one declaring size + alone cost 3. Under `value *= 0.5 ** len(failures)` that is a 4x difference + in penalty between two single rooms, decided by verbosity -- and the tiered + comparator inherits it, since n_hard is dominated by these cascades. + """ + verbose = _missing_fails({"size": [16.0, 4.0], "width": [4.0, 1.0], + "proportion": [1.5, 0.5]}) + terse = _missing_fails({"size": [16.0, 4.0]}) + assert len(verbose) == len(terse) == 5 + assert set(verbose) == set(terse) + + +def test_missing_space_placeholders_mirror_the_checks_a_present_room_faces(): + """All three, always -- because a present room is checked on all three. + + `get_space_params` fills width and proportion from defaults (deriving width + from size when absent), so the requirement exists however the config is + spelled. The placeholder count has to mirror that or the two paths + disagree. + """ + fails = _missing_fails({"size": [16.0, 4.0]}) + for check in ("size", "width", "proportion"): + assert f"missing x1: would need {check} check" in fails + assert sum(1 for f in fails if f.startswith("missing required space")) == 2 + + +def test_missing_space_cascade_scales_per_instance_not_per_code(): + fails = _missing_fails({"size": [16.0, 4.0], "count": 3}) + assert len(fails) == 15 + for i in (1, 2, 3): + assert f"missing required space: x1#{i}" in fails