Files
C.E.L_Slide_test2/tests/test_family_contract_baseline.py
kyeongmin 8f06a4c99f docs(IMP-52): reconcile Phase Z family count drift -- F-2 option (c)
Audit follow-up F-2 (INTEGRATION-AUDIT-01 §10.2). Phase Z families surface
showed 11 tracked / 11 contracted / 13 on disk. The 2 untracked WIP files
(app_sw_package_vs_solution.html, pre_construction_model_info_stacked.html)
are now declared in _WIP_FILES.md as uncontracted and out-of-scope for the
runtime matcher; promote/remove is gated on #42. The 11/11 tracked +
contracted baseline is unchanged. A new pytest enforces tracked families ↔
frame_contracts.yaml set-equality modulo the WIP allowlist parsed from
_WIP_FILES.md, so future drift fails fast in CI before #42 expands to 32
frames.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 19:15:04 +09:00

81 lines
3.0 KiB
Python

"""Phase Z family-template ↔ frame_contracts.yaml baseline invariant.
#52 F-2 option (c) lock (2026-05-19) — locks active contracted family count
at 11/11 with a WIP allowlist for the 2 untracked WIP family templates
documented in `templates/phase_z2/families/_WIP_FILES.md`. Any drift after
this point (new family file on disk without a contract entry, or a contract
entry pointing to a missing file) fails fast in CI.
References:
- docs/architecture/INTEGRATION-AUDIT-01-REPORT.md §10.2 F-2
- docs/architecture/IMP-18-SVG-GAP-REPORT.md L28/L30/L51
- templates/phase_z2/families/_WIP_FILES.md (WIP allowlist source)
- Gitea #52 (this reconciliation), #42 (promote/remove gate)
Pattern mirrors `tests/test_catalog_invariant.py` — fail fast with explicit
diff message if family ↔ contract surfaces drift.
"""
from __future__ import annotations
import re
from pathlib import Path
import yaml
PROJECT_ROOT = Path(__file__).parent.parent
FAMILIES_DIR = PROJECT_ROOT / "templates" / "phase_z2" / "families"
CATALOG_PATH = PROJECT_ROOT / "templates" / "phase_z2" / "catalog" / "frame_contracts.yaml"
WIP_DOC_PATH = FAMILIES_DIR / "_WIP_FILES.md"
def _load_contract_keys() -> set[str]:
with CATALOG_PATH.open(encoding="utf-8") as f:
catalog = yaml.safe_load(f)
return {k for k, v in catalog.items() if isinstance(v, dict)}
def _load_disk_family_stems() -> set[str]:
return {p.stem for p in FAMILIES_DIR.glob("*.html")}
def _load_wip_allowlist() -> set[str]:
text = WIP_DOC_PATH.read_text(encoding="utf-8")
return {m.group(1) for m in re.finditer(r"`([A-Za-z0-9_\-]+)\.html`", text)}
def test_contracts_set_equals_disk_families_minus_wip():
"""`frame_contracts.yaml` keys ↔ disk family stems minus WIP allowlist."""
contracts = _load_contract_keys()
disk = _load_disk_family_stems()
wip = _load_wip_allowlist()
expected = disk - wip
missing = expected - contracts
extra = contracts - expected
assert not missing, (
f"Family files on disk without frame_contracts.yaml entry "
f"(and not in _WIP_FILES.md): {sorted(missing)}. "
"Add a contract entry, or list the file in "
"templates/phase_z2/families/_WIP_FILES.md as WIP."
)
assert not extra, (
f"frame_contracts.yaml has entries with no matching family file: "
f"{sorted(extra)}."
)
def test_wip_allowlist_is_disk_only_and_uncontracted():
"""WIP allowlist names must exist on disk AND have no contract entry."""
contracts = _load_contract_keys()
disk = _load_disk_family_stems()
wip = _load_wip_allowlist()
missing_on_disk = wip - disk
leaked_into_contracts = wip & contracts
assert not missing_on_disk, (
f"_WIP_FILES.md names files not on disk: {sorted(missing_on_disk)}."
)
assert not leaked_into_contracts, (
f"_WIP_FILES.md names files that already have a contract entry: "
f"{sorted(leaked_into_contracts)}. Promote via #42 instead — "
"WIP allowlist must be disk-only / uncontracted."
)