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
83 lines
2.9 KiB
Python
83 lines
2.9 KiB
Python
"""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())
|