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>
This commit is contained in:
80
tests/test_family_contract_baseline.py
Normal file
80
tests/test_family_contract_baseline.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""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."
|
||||
)
|
||||
Reference in New Issue
Block a user