Staged harness re-scored under a different objective than it searched

run_staged_search.py reported MISMATCH on its BASELINE arm -- the
LEAFSHARE=0/MULTIUSE=0 control every A/B compares against. Two facts
combined: driver.search_staged had no collapse_insearch parameter at all,
so every inner search() call inherited search()'s True default
unconditionally; and no example patterns.config sets the key, so the final
_native_score rescore got False from a bare load_config. Search optimised
one objective, the rescore graded another.

The 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.

Fixed in the right place: search_staged now HAS the parameter (default
True, byte-identical to the inherited default), threaded into all three
internal search() calls. The harness chooses the arm explicitly (COLLAPSE,
default 1), passes it to the search, and passes the SAME value to
_native_score, which overrides the key rather than hoping the config
carries it. The rescore mirrors the search by construction.

Verified on programme-house, budget 150:
  baseline    MISMATCH 1.56663e-08 vs 1.51708e-08  ->  OK
  COLLAPSE=0  (knob did not exist)                 ->  OK 1.66216e-08
  LEAFSHARE=1 / MULTIUSE=1                         ->  OK

COLLAPSE=0 scoring differently confirms the knob is not a no-op, and the
default arm's search result is unchanged, so no prior staged number moves.

Audited the other three search_staged callers: run_and_capture_91f.py
already pins collapse_insearch: True; run_island_ab.py never re-scores;
probe_harbor_floor.py did NOT pin it and had the same bug -- now fixed, and
that is the harness which produced every 13.x floor number.

The recorded mitigating factor -- only the continuous score moved, the fail
count matched, and the run_*_ab.sh greps read only the count -- is true and
is exactly what made it dangerous: a harness that reports MISMATCH on its
own control, invisibly to the metric of record, trains everyone to ignore
the warning.

Closes homemaker-py-4ok.

Lint at parity (46); tests 381 passed, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
This commit is contained in:
Claude 2026-08-29 12:11:38 +00:00
parent 148988df14
commit 225e673ae3
No known key found for this signature in database
5 changed files with 88 additions and 10 deletions

View file

@ -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-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-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-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-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-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} {"_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}

View file

@ -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 documented `geometry._cache` issue and a plausible contributor to
`homemaker-py-b8g`. `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 ## 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 The corpus `patterns.config` targets and `costs.config` values were estimated

View file

@ -109,6 +109,12 @@ def main() -> int:
conf = dict(conf) conf = dict(conf)
conf["leaf_sharing"] = True conf["leaf_sharing"] = True
conf["max_share"] = 3 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 return conf, cost
fitness.load_config = _load_with_sharing fitness.load_config = _load_with_sharing

View file

@ -29,10 +29,23 @@ REPO = Path(__file__).resolve().parents[1]
HARBOR = REPO / "examples" / "harbor-house" 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 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) fit = fitness.Fitness(conf, cost)
score, fails = fit.score_with_fails(copy.deepcopy(root)) score, fails = fit.score_with_fails(copy.deepcopy(root))
return score, len(fails) 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 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 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 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: if leaf_share or multi_use:
# erc.3 §13.3 / 1s3 §26: the FINAL re-score below (`_native_score`) loads # 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 conf["share_edge_cap"] = share_edge
if multi_use: if multi_use:
conf["multi_use"] = True conf["multi_use"] = True
# 7ua: driver.search_staged has no param to disable collapse_insearch, # 7ua/4ok: pin the dir-level default to the arm actually being run.
# so its inner evaluator always runs with search()'s collapse_insearch= # search_staged now takes collapse_insearch directly, so this mirrors
# True default. Pin it here too or this rescore silently diverges from # that choice rather than hardcoding True.
# search-time conf whenever leaf_sharing is on, producing a false conf["collapse_insearch"] = collapse
# MISMATCH against the search-reported fail count.
conf["collapse_insearch"] = True
return conf, cost return conf, cost
fitness.load_config = _load_with_flags fitness.load_config = _load_with_flags
@ -120,6 +135,7 @@ def main() -> int:
print(f"interior_o: {interior_o} (odiv={out_div})") print(f"interior_o: {interior_o} (odiv={out_div})")
print(f"multi_use : {multi_use}") print(f"multi_use : {multi_use}")
print(f"workers : {workers}") print(f"workers : {workers}")
print(f"collapse : {collapse}")
print(flush=True) print(flush=True)
seed_root = dom.load(str(seed_file)) seed_root = dom.load(str(seed_file))
@ -137,6 +153,7 @@ def main() -> int:
p_crossover=0.2, p_crossover=0.2,
seed=rng_seed, seed=rng_seed,
n_workers=workers, n_workers=workers,
collapse_insearch=collapse,
log=lambda m: print(m, flush=True), log=lambda m: print(m, flush=True),
use_grade=use_grade, use_grade=use_grade,
tournament_k=tournament_k, tournament_k=tournament_k,
@ -177,7 +194,7 @@ def main() -> int:
out.parent.mkdir(parents=True, exist_ok=True) out.parent.mkdir(parents=True, exist_ok=True)
dom.dump(r.best.root, str(out)) 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) ok = math.isclose(rs, r.best.fitness, rel_tol=1e-9)
print(f"\n{out.name} re-scored (native): {rs:.6g} ({rf} fails) " print(f"\n{out.name} re-scored (native): {rs:.6g} ({rf} fails) "
f"{'OK' if ok else 'MISMATCH'}") f"{'OK' if ok else 'MISMATCH'}")

View file

@ -1110,6 +1110,7 @@ def search_staged(
construction_beam_width: int = 1, construction_beam_width: int = 1,
assign_solver: str = "greedy", assign_solver: str = "greedy",
enable_reassign: bool = False, enable_reassign: bool = False,
collapse_insearch: bool = True,
) -> SearchResult: ) -> SearchResult:
"""Staged per-floor topology search (DESIGN.md §11.3, ``homemaker-py-c4c.3``). """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, seed_proportion_aware=seed_proportion_aware,
enable_reassociate=enable_reassociate, enable_reassociate=enable_reassociate,
enable_shape_repair=enable_shape_repair, enable_shape_repair=enable_shape_repair,
collapse_insearch=collapse_insearch,
enable_bridge_circulation=enable_bridge_circulation, enable_bridge_circulation=enable_bridge_circulation,
enable_ruin_recreate=enable_ruin_recreate, enable_ruin_recreate=enable_ruin_recreate,
feasibility_filter=feasibility_filter, feasibility_filter=feasibility_filter,
@ -1191,6 +1193,7 @@ def search_staged(
child_budget=child_budget, seed_budget=seed_budget, child_budget=child_budget, seed_budget=seed_budget,
p_crossover=p_crossover, seed=seed, types=None, p_crossover=p_crossover, seed=seed, types=None,
inner_kw=inner_kw, log=log, n_workers=n_workers, 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_fn=lambda root: graph.substrate_readiness(root, reqs, n_storeys),
rank_bonus_weight=rank_bonus_weight, rank_bonus_weight=rank_bonus_weight,
tournament_k=tournament_k, tournament_k=tournament_k,
@ -1245,6 +1248,7 @@ def search_staged(
child_budget=child_budget, seed_budget=seed_budget, child_budget=child_budget, seed_budget=seed_budget,
p_crossover=p_crossover, seed=seed, types=types, p_crossover=p_crossover, seed=seed, types=types,
inner_kw=inner_kw, log=log, n_workers=n_workers, inner_kw=inner_kw, log=log, n_workers=n_workers,
collapse_insearch=collapse_insearch,
bootstrap=True, seed_factory=_seed_factory, base_p=base_p, bootstrap=True, seed_factory=_seed_factory, base_p=base_p,
# §11.4: the graded objective targets the dense two-floor quality-fail # §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 # regime, which is Stage 2. Stage 1 keeps its readiness-biased key so the