homemaker-layout/experiments/migrate_crinkliness_key.py

84 lines
2.9 KiB
Python
Raw Normal View History

ssz: crinkliness is declared per space; there is no daylight attribute Owner's ruling, and it corrects the design not just the classification: the daylight requirement is already defined in the crinkliness. The gaussian's compact side IS "too little exposed wall per unit floor"; its exposed side is envelope cost. 38.9's proposed daylight: axis was redundant, and keying it off usage: was worse than redundant. What was actually missing: crinkliness is the only leaf quality factor with no per-space target. size, width and proportion are all declared by the space; crinkliness was one global number for every room in every building. crinkliness: none -> no minimum-exposure requirement, may be buried crinkliness: [t, s] -> this space's own target key absent -> the global uncrinkliness target, as today `none` clips the factor on the compact side, it does not switch it off: over-exposure is still penalised, because a crinkly leaf costs envelope whatever it holds. A store may be buried; a store may not be a starfish. The mechanism is backward compatible -- an absent key resolves to the global target, so shipping it changes no score. Behaviour changes only where a config declares something, which keeps the objective change visible per programme in config rather than hidden in a default. Owner's classification: everything a person occupies wants a window, WCs and reception/waiting/foyer included; only stores, plant, records and laundry do not. migrate_crinkliness_key.py declared crinkliness: none on 18 corpus spaces. Crinkliness fails 271 -> 243, of which not-defects 136 (50%) -> 108 (44%); the 28 that went are exactly the utility fails. usage_daylight and needs_daylight are removed as mis-keyed, and DAYLIGHT_USAGES with them -- a vocabulary value should exist only where the engine treats it differently. The historical crinkliness_mode modes stay, default off, so 38.6/38.8 remain reproducible. uncrinkliness_circulation is now settable to none like any space, but its default is left unchanged pending a ruling: corridors were not among the groups ruled on and are 63% of the remaining phantom fails. Lint at parity (46); tests 364 passed, 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-28 22:38:54 +00:00
"""Declare `crinkliness:` where a space needs no window (`homemaker-py-ssz`).
One-shot migration for DESIGN.md §38.10. The compact side of the crinkliness
gaussian IS the daylight requirement, so a space that does not need daylight
says so in its own `crinkliness:` target -- there is no separate attribute.
Ruled by the project owner: everything a person occupies wants a window --
WCs and bathrooms included, reception/waiting/foyer included, offices and
consulting rooms included. Only stores, plant, records and laundry
(`usage: utility`) do not. Internal corridors do not either, but those are
generic `C` leaves with no `spaces:` entry, so they are handled by the
`uncrinkliness_circulation` key rather than here.
Edits the file as text, preserving comments and layout. Idempotent: a space
that already declares `crinkliness:` is left alone unless `--force`.
Usage::
python experiments/migrate_crinkliness_key.py --check # dry run
python experiments/migrate_crinkliness_key.py # apply
"""
from __future__ import annotations
import argparse
import re
from pathlib import Path
import yaml
CORPUS = Path(__file__).resolve().parent.parent / "examples"
NO_DAYLIGHT_USAGES = {"utility"}
def migrate(path: Path, check: bool, force: bool) -> tuple[int, int]:
text = path.read_text()
spaces = (yaml.safe_load(text) or {}).get("spaces") or {}
space_key = re.compile(r"^ ([A-Za-z_][\w-]*):\s*$")
written = skipped = 0
out: list[str] = []
for line in text.splitlines(keepends=True):
m = space_key.match(line)
code = m.group(1) if m and m.group(1) in spaces else None
if force and re.match(r"^ crinkliness:\s", line):
continue
out.append(line)
if code is None:
continue
c = spaces[code]
if c.get("usage") not in NO_DAYLIGHT_USAGES:
continue
if "crinkliness" in c and not force:
skipped += 1
else:
out.append(" crinkliness: none # no window needed\n")
written += 1
if not check and written:
path.write_text("".join(out))
return written, skipped
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--check", action="store_true", help="dry run")
ap.add_argument("--force", action="store_true",
help="rewrite a crinkliness: already present")
args = ap.parse_args()
for cfg in sorted(CORPUS.glob("*/patterns.config")):
written, skipped = migrate(cfg, args.check, args.force)
verb = "would declare" if args.check else "declared"
print(f" {cfg.parent.name:<20} {verb} crinkliness:none on {written:>2} "
f"space(s), already present {skipped}")
return 0
if __name__ == "__main__":
raise SystemExit(main())