diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 24df000..4df8970 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -100,7 +100,7 @@ {"_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} +{"_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":"closed","priority":3,"issue_type":"bug","assignee":"Claude","owner":"bruno@postle.net","created_at":"2026-08-05T11:25:30Z","created_by":"Bruno Postle","updated_at":"2026-08-29T12:11:37Z","started_at":"2026-08-29T12:03:00Z","closed_at":"2026-08-29T12:11:37Z","close_reason":"Fixed at the root rather than by extending the monkeypatch (DESIGN.md §38.16).\n\ndriver.search_staged now HAS a collapse_insearch parameter (default True,\nbyte-identical to the value it previously inherited from search()), threaded\ninto all three internal search() calls. run_staged_search.py chooses the arm\nexplicitly via COLLAPSE (default 1), passes it to the search, and passes the\nSAME value to _native_score, which now overrides the key instead of hoping the\nconfig carries it. The rescore mirrors the search by construction, not by\ncoincidence of which monkeypatch happened to be installed.\n\nVerified, programme-house budget 150:\n baseline (no env) MISMATCH 1.56663e-08 vs 1.51708e-08 -\u003e OK\n COLLAPSE=0 (knob did not exist) -\u003e OK 1.66216e-08\n LEAFSHARE=1 OK -\u003e OK\n MULTIUSE=1 OK -\u003e OK\n\nCOLLAPSE=0 scoring differently from COLLAPSE=1 confirms the knob does real work.\nThe search result on the default arm is unchanged, so no prior staged number\nmoves.\n\nAudited the other three search_staged callers while here:\n run_and_capture_91f.py already pins collapse_insearch: True -- correct\n run_island_ab.py never re-scores -- cannot mismatch\n probe_harbor_floor.py DID NOT pin it, same bug -- now fixed. That is the\n harness which produced every §13.x floor number.\n\nOn the \"mitigating factor\" that only the continuous score moved while the fail\ncount matched: true, and precisely what made it dangerous. A harness that\nreports MISMATCH on its own control, in a way the metric-of-record cannot see,\ntrains everyone to ignore the warning.\n","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"homemaker-py-5bv","title":"CP-SAT post-collapse repair (Fitness.collapse_global's Jacobi+2-opt QAP relaxation)","description":"homemaker-py-2g7.5 item (c), deferred (DESIGN.md §37.7). Fitness.collapse_global (fitness.py:655-847) approximates a finish-time cell\u003c-\u003eroom relabelling QAP with a Jacobi-style fixpoint iteration (_best_assignment, linear_sum_assignment warm-started each round from the previous round's neighbour labels) plus _two_opt_adjacency_polish to escape 2-cycle plateaus. DESIGN.md §25 explicitly considered and rejected OR-Tools for this exact problem 'because the project has no ortools' -- 2g7.5 has now added that dependency (for a simpler, different problem: assignment on a FIXED topology, not this finish-time relabel). This bead: replace or augment collapse_global's Jacobi+2-opt loop with an exact CP-SAT solve of the same cell\u003c-\u003eroom assignment (reusing _collapse_value's per-(leaf,code) value function so both stay consistent), verified safe against the 94g keep-better guard. Riskier than 2g7.5's seeder/reassign work since collapse_global is delicate, heavily tested, and runs inside every in-search eval when collapse_insearch=True (driver.py default) -- correctness and wall-clock regressions would be felt everywhere, not just in an opt-in flag.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-08-04T07:43:33Z","created_by":"Bruno Postle","updated_at":"2026-08-04T07:43:33Z","dependencies":[{"issue_id":"homemaker-py-5bv","depends_on_id":"homemaker-py-2g7.5","type":"parent-child","created_at":"2026-08-04T08:44:03Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-5bv","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-04T08:44:25Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"homemaker-py-v4s","title":"driver.search A/B for shapecurve warm-start/prune on real multi-storey programmes","description":"Follow-up from homemaker-py-koo (DESIGN.md §37.6): koo generalised shapecurve.py's DP to handle below-inherited multi-storey trees and validated it (DP-vs-NM agreement/false-negative bar, 200 topologies on the real examples/harbor-house, 99.5% agreement, 0 false negatives, 117.7x speedup, DESIGN.md §37.6). Not measured: the search-level payoff of shapecurve_warmstart/shapecurve_prune on a real multi-storey programme at the 6xh/wkh A/B protocol (budget=2000, seeds 0-4, driver.search mean hard/soft/fitness fails, off vs on). Best sized as a single A/B once homemaker-py-tym (leaf_sharing/co_type modelling in shapecurve.leaf_constraints) also lands, since leaf_sharing defaults True in driver.search and both programme-house and harbor-house require it by default -- measuring the combined win (multi-storey + leaf_sharing) in one pass avoids two partial A/Bs that each only apply with a flag most real runs don't use.","notes":"Depends on homemaker-py-tym landing first for the combined measurement to be meaningful.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-08-03T22:23:50Z","created_by":"Bruno Postle","updated_at":"2026-08-03T22:24:36Z","dependencies":[{"issue_id":"homemaker-py-v4s","depends_on_id":"homemaker-py-tym","type":"blocks","created_at":"2026-08-03T23:24:37Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"homemaker-py-ekc","title":"True skew-quad polygon algebra for the shape-curve DP leaf region (remove ~7-12% rectangle approximation error)","description":"homemaker-py-6xh item (DESIGN.md §37.2, 'Remaining approximation error, root-caused'). src/homemaker_layout/shapecurve.py approximates every quad (leaf or internal) as a rectangle with edge-length-derived (w,h) = ((edge0+edge2)/2, (edge1+edge3)/2) -- exact only for a true rectangle/parallelogram. DESIGN.md §37.2's 200-topology harbor-house-l0 validation root-caused both measured false positives to this approximation specifically (not to global rotation or to the rotation-parity composition rule, both already fixed/verified exact): the DP's own realised point had a leaf whose edge-length-approximated area was comfortably inside its feasible bound but whose true geometry.area (a real, slightly non-parallelogram quad) fell just below the true lower bound -- an ~8-12% gap, the same magnitude as harbor-house-l0's own plot-level residual skew. Needs: either (a) replace the rectangle approximation with true skew-quad polygon algebra (a harder closed-form derivation, or a numerically-solved per-leaf feasible region), or (b) at minimum re-characterise the error's magnitude on a LESS rectangular plot than harbor-house-l0's near-rectangular trapezoid (§37.2 flagged this as untested and likely worse elsewhere) so shapecurve_warmstart's real-world false-positive rate is known before wider rollout.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-08-03T17:30:23Z","created_by":"Bruno Postle","updated_at":"2026-08-03T17:30:23Z","dependencies":[{"issue_id":"homemaker-py-ekc","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-03T18:31:51Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/DESIGN.md b/DESIGN.md index bbc7ad5..46ae21a 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -5666,6 +5666,57 @@ under `homemaker-py-vjd`. This is the same `id()`-keying hazard as the documented `geometry._cache` issue and a plausible contributor to `homemaker-py-b8g`. +### 38.16 The staged harness re-scored under a different objective than it searched (`homemaker-py-4ok`) + +`run_staged_search.py` reported `MISMATCH` on its **baseline** arm — the +`LEAFSHARE=0/MULTIUSE=0` control that every A/B in this document compares +against. Two facts combined: + +- `driver.search_staged` had **no `collapse_insearch` parameter at all**, so + every inner `search()` call inherited `search()`'s `collapse_insearch=True` + default, unconditionally; +- no example `patterns.config` sets the key, so the final `_native_score` + re-score got `False` from a bare `load_config`. + +Search optimised one objective; the rescore graded a different one. The +`homemaker-py-7ua` fix pinned the key inside a `fitness.load_config` monkeypatch +— but that patch was installed only `if leaf_share or multi_use`, so it fixed +every arm *except* the control. + +**Fix, in the right place: give `search_staged` the parameter it was missing** +(default `True`, byte-identical to the inherited default) and thread it into all +three internal `search()` calls. The harness then chooses the arm explicitly +(`COLLAPSE`, default 1), passes it to the search, and passes **the same value** +to `_native_score`, which now overrides the key rather than hoping the config +carries it. The rescore mirrors the search by construction instead of by +coincidence of which monkeypatch happened to be installed. + +Verified on programme-house, budget 150, all four arms: + +| arm | before | after | +|---|---|---| +| baseline (no env) | **MISMATCH** 1.56663e-08 vs 1.51708e-08 | **OK** | +| `COLLAPSE=0` | n/a (no knob existed) | **OK**, 1.66216e-08 | +| `LEAFSHARE=1` | OK | **OK** | +| `MULTIUSE=1` | OK | **OK** | + +`COLLAPSE=0` scoring differently from `COLLAPSE=1` (1.66216e-08 vs 1.56663e-08) +confirms the knob does real work rather than being a no-op, and the search +result itself is unchanged on the default arm, so no prior staged number moves. + +**One sibling had the same bug.** Auditing the other three `search_staged` +callers: `run_and_capture_91f.py` already pins `collapse_insearch: True` in its +overrides and is correct; `run_island_ab.py` never re-scores, so it cannot +mismatch; **`probe_harbor_floor.py` did not pin it** and so re-scored under a +different objective than it searched — and that is the harness which produced +"every §13.x floor number". Now pinned. + +**Why a P3 was worth doing.** The mitigating factor recorded on the issue was +that the fail *count* matched and only the continuous score moved, which the +`run_*_ab.sh` greps do not read. That is true and it is also exactly what makes +this dangerous: a harness that reports MISMATCH on its own control, in a way the +metric-of-record cannot see, trains everyone to ignore the warning. + ## 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/experiments/probe_harbor_floor.py b/experiments/probe_harbor_floor.py index 9378b9a..90304fb 100644 --- a/experiments/probe_harbor_floor.py +++ b/experiments/probe_harbor_floor.py @@ -109,6 +109,12 @@ def main() -> int: conf = dict(conf) conf["leaf_sharing"] = True conf["max_share"] = 3 + # homemaker-py-4ok: driver.search_staged runs its inner evaluator with + # collapse_insearch=True (its default), and no example patterns.config + # sets the key, so without pinning it here the re-score below silently + # used a DIFFERENT objective than the search it is reporting on -- the + # same MISMATCH class fixed in run_staged_search.py. + conf["collapse_insearch"] = True return conf, cost fitness.load_config = _load_with_sharing diff --git a/experiments/run_staged_search.py b/experiments/run_staged_search.py index b620faf..0eeb0a5 100644 --- a/experiments/run_staged_search.py +++ b/experiments/run_staged_search.py @@ -29,10 +29,23 @@ REPO = Path(__file__).resolve().parents[1] HARBOR = REPO / "examples" / "harbor-house" -def _native_score(root: dom.Node, programme_dir: Path) -> tuple[float, int]: +def _native_score(root: dom.Node, programme_dir: Path, + collapse_insearch: bool) -> tuple[float, int]: + """Re-score under the SAME objective the search optimised (homemaker-py-4ok). + + `collapse_insearch` must be passed explicitly, not left to the config file. + No example `patterns.config` sets it, so a bare `load_config` here yields + False while `driver.search_staged` was running its inner evaluator with + True -- the search and the rescore then disagree, which is exactly the + MISMATCH this script reports. The old code only avoided that on + leaf_sharing/multi_use arms, because the conf-patch that pinned it was + installed only for those; the default baseline arm, which every A/B uses as + its control, mismatched silently. + """ import copy - conf, cost = fitness.load_config(programme_dir) + conf, cost = fitness.load_config( + programme_dir, overrides={"collapse_insearch": collapse_insearch}) fit = fitness.Fitness(conf, cost) score, fails = fit.score_with_fails(copy.deepcopy(root)) return score, len(fails) @@ -71,6 +84,10 @@ def main() -> int: out_div = int(os.environ.get("ODIV", "6")) # ld2 outside-leaf-per-room divisor multi_use = os.environ.get("MULTIUSE", "0") == "1" # 1s3 §26 path b multi-use A/B workers = int(os.environ.get("WORKERS", "1")) # parallel child evaluation + # 4ok: search_staged now has a real collapse_insearch parameter, so the arm + # is chosen here rather than inherited from search()'s default and then + # contradicted by the rescore. Default 1 == the previous behaviour. + collapse = os.environ.get("COLLAPSE", "1") == "1" if leaf_share or multi_use: # erc.3 §13.3 / 1s3 §26: the FINAL re-score below (`_native_score`) loads @@ -92,12 +109,10 @@ def main() -> int: conf["share_edge_cap"] = share_edge if multi_use: conf["multi_use"] = True - # 7ua: driver.search_staged has no param to disable collapse_insearch, - # so its inner evaluator always runs with search()'s collapse_insearch= - # True default. Pin it here too or this rescore silently diverges from - # search-time conf whenever leaf_sharing is on, producing a false - # MISMATCH against the search-reported fail count. - conf["collapse_insearch"] = True + # 7ua/4ok: pin the dir-level default to the arm actually being run. + # search_staged now takes collapse_insearch directly, so this mirrors + # that choice rather than hardcoding True. + conf["collapse_insearch"] = collapse return conf, cost fitness.load_config = _load_with_flags @@ -120,6 +135,7 @@ def main() -> int: print(f"interior_o: {interior_o} (odiv={out_div})") print(f"multi_use : {multi_use}") print(f"workers : {workers}") + print(f"collapse : {collapse}") print(flush=True) seed_root = dom.load(str(seed_file)) @@ -137,6 +153,7 @@ def main() -> int: p_crossover=0.2, seed=rng_seed, n_workers=workers, + collapse_insearch=collapse, log=lambda m: print(m, flush=True), use_grade=use_grade, tournament_k=tournament_k, @@ -177,7 +194,7 @@ def main() -> int: out.parent.mkdir(parents=True, exist_ok=True) dom.dump(r.best.root, str(out)) - rs, rf = _native_score(r.best.root, programme_dir) + rs, rf = _native_score(r.best.root, programme_dir, collapse) ok = math.isclose(rs, r.best.fitness, rel_tol=1e-9) print(f"\n{out.name} re-scored (native): {rs:.6g} ({rf} fails) " f"→ {'OK' if ok else 'MISMATCH'}") diff --git a/src/homemaker_layout/driver.py b/src/homemaker_layout/driver.py index c8f2c68..527de42 100644 --- a/src/homemaker_layout/driver.py +++ b/src/homemaker_layout/driver.py @@ -1110,6 +1110,7 @@ def search_staged( construction_beam_width: int = 1, assign_solver: str = "greedy", enable_reassign: bool = False, + collapse_insearch: bool = True, ) -> SearchResult: """Staged per-floor topology search (DESIGN.md §11.3, ``homemaker-py-c4c.3``). @@ -1157,6 +1158,7 @@ def search_staged( seed_proportion_aware=seed_proportion_aware, enable_reassociate=enable_reassociate, enable_shape_repair=enable_shape_repair, + collapse_insearch=collapse_insearch, enable_bridge_circulation=enable_bridge_circulation, enable_ruin_recreate=enable_ruin_recreate, feasibility_filter=feasibility_filter, @@ -1191,6 +1193,7 @@ def search_staged( child_budget=child_budget, seed_budget=seed_budget, p_crossover=p_crossover, seed=seed, types=None, inner_kw=inner_kw, log=log, n_workers=n_workers, + collapse_insearch=collapse_insearch, rank_bonus_fn=lambda root: graph.substrate_readiness(root, reqs, n_storeys), rank_bonus_weight=rank_bonus_weight, tournament_k=tournament_k, @@ -1245,6 +1248,7 @@ def search_staged( child_budget=child_budget, seed_budget=seed_budget, p_crossover=p_crossover, seed=seed, types=types, inner_kw=inner_kw, log=log, n_workers=n_workers, + collapse_insearch=collapse_insearch, bootstrap=True, seed_factory=_seed_factory, base_p=base_p, # §11.4: the graded objective targets the dense two-floor quality-fail # regime, which is Stage 2. Stage 1 keeps its readiness-biased key so the