homemaker-layout/experiments/migrate_ju3_rename.py
Claude 697914fac8
ju3: reject programme codes colliding with the c/o/s generic type prefixes
Closes homemaker-py-ju3. DESIGN.md §39.3.

The class: key from the bead's design was deliberately NOT built. Auditing
every use of the prefix rule first showed it runs deeper than c/o/s -- l/k/b/t
carry real adjacency semantics (graph.py builds bedroom<->toilet and
kitchen<->living relations from first characters) -- so re-plumbing the type
system would invalidate the whole corpus and every baseline, for a problem
whose damage is the silence, not the convention. Two findings made the smaller
fix sufficient: no corpus programme has ever declared a bare c/o/s code, so
check_space_counts' skip only ever discarded declared rooms; and nothing
references harbor's four codes in any adjacency or co_locate list.

- programme.validate_codes raises on a reserved-prefix code, with the full
  explanation. Called from BOTH parse paths (programme._parse_spaces and
  fitness.Fitness._load_programme parse conf["spaces"] independently, so
  validating one would leave the other door open). l/k/b/t stay unreserved.
- harbor-house and harbor-house-l0 renamed: cr1->fr1, of->ao, st1->gs1,
  st2->gs2. New prefixes are unused in harbor and semantically neutral, and
  the two storage codes still share a prefix, preserving the structure
  evaluate_building's per-code plot-ratio term depends on. name: unchanged.
- experiments/migrate_ju3_rename.py migrates .dom files written before the
  rename (--check dry-runs). Pre-rename artefacts, notably evolved-3M*.dom,
  must be migrated or their leaves read as unmatched generics.
- test_collapse_global's c/o/s exclusion test now uses a generic C leaf, which
  is what the exclusion is actually for; it previously relied on a programme
  code colliding, which is no longer possible.

Re-baseline (seed 1, 20k evals, same settings as §38's run): 57 fails against
the 32-instance effective programme -> 55 against the real 37-instance one,
with all five previously-lost room instances now placed inside their declared
sigma bands (fr1 87.2 vs declared 80, was 32.9/17.1; ao/gs1/gs2 were absent
entirely) and no failure naming any of the four codes. At one seed each,
57 vs 55 is within noise -- the robust result is the room placement, not the
count. Historical harbor numbers are not comparable to post-ju3 ones; filed
homemaker-py-t3s to restate 2v1's acceptance figure once evolved-3M is
migrated.

346 passed (+10 new), same 7 pre-existing fixture failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-08-26 09:06:08 +00:00

79 lines
2.8 KiB
Python

"""Rename harbor-house's colliding room codes in existing `.dom` files.
`homemaker-py-ju3` (DESIGN.md §39.2) renamed four harbor-house programme codes
that collided with Urb's reserved generic type prefixes (``c`` = circulation,
``o``/``s`` = outside). Any `.dom` produced **before** that change still carries
the old leaf types, and will score differently against the renamed programme —
the old types are no longer declared, so every affected leaf reads as an
unmatched generic instead of the room it was.
This matters for artefacts not checked in at the time, notably the
``evolved-3M*.dom`` harbor-house runs.
Mapping (chosen so each new prefix is unused in harbor-house *and* carries no
adjacency semantics of its own — ``l``/``k``/``b``/``t`` do, ``f``/``a``/``g``
do not — while preserving the original prefix-sharing structure, i.e. the two
storage codes still share one prefix):
cr1 -> fr1 Common Room with Fireplace
of -> ao Staff Office
st1 -> gs1 Ground Floor Storage
st2 -> gs2 First Floor Storage
Usage::
python experiments/migrate_ju3_rename.py path/to/evolved-3M.dom [...]
python experiments/migrate_ju3_rename.py --check path/to/*.dom
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
RENAMES = {"cr1": "fr1", "of": "ao", "st1": "gs1", "st2": "gs2"}
def migrate(path: Path, check_only: bool) -> int:
"""Rewrite ``type:`` lines in place; return the number of leaves changed."""
text = path.read_text()
total = 0
for old, new in RENAMES.items():
# Anchored to a whole `type: <code>` line so a code appearing inside a
# name, comment or unrelated scalar is never touched.
text, n = re.subn(rf"^(\s*type: ){re.escape(old)}\s*$", rf"\g<1>{new}",
text, flags=re.M)
total += n
if total and not check_only:
path.write_text(text)
return total
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("paths", nargs="+", type=Path)
ap.add_argument("--check", action="store_true",
help="report what would change without writing")
args = ap.parse_args()
stale = 0
for path in args.paths:
if not path.is_file():
print(f" {path}: NOT FOUND", file=sys.stderr)
continue
n = migrate(path, args.check)
stale += bool(n)
verb = "would rename" if args.check else "renamed"
print(f" {path}: {verb} {n} leaf type(s)"
if n else f" {path}: already current")
if args.check and stale:
print(f"\n{stale} file(s) still carry pre-ju3 codes.")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())