Files
C.E.L_Slide_test2/tests/test_family_contract_baseline.py
kyeongmin a06dd3d4b0 feat(#42): IMP-04b catalog extension to 32 frames (u1~u24)
Extends frame_contracts.yaml from 11 to 32 contracts to match V4 evidence
(tests/matching/v4_full32_result.yaml unique template_ids), closing the
IMP-04b gap surfaced in IMP-04 (#4) Track A milestone.

Scope (Stage 2 24-unit plan):
- u3/u4: WIP partial absorb — app_sw_package_vs_solution (F23),
  pre_construction_model_info_stacked (F9). Both promoted from
  _WIP_FILES.md to frame_contracts.yaml. WIP allowlist now empty.
- u5~u11: Track A 7 frames (index.html present, contract missing).
- u12~u23: Track B 12 frames (visual_pending: true; family partial
  authoring deferred — contract-first per Stage 2 plan).
- u24: BT closure gate. Adds
  test_imp04b_closure_gate_v4_coverage_and_wip_empty (catalog ↔ V4
  set-equal + WIP==0) and test_vp_exempt_keys_are_contracted_and_disk_absent
  (vp ∩ disk == ∅). Relaxes test_contracts_set_equals_disk_families_minus_wip
  to (disk - wip) ∪ vp. 32 derived from V4 evidence YAML (no hardcoding).

Closure facts (locked):
  contracts = 32, v4_unique = 32, missing = [], extra = [],
  wip_count = 0, vp_count = 19, vp ∩ disk = [].

Guardrails honored:
- No calculate_fit migration.
- No AI/Kei API call in per-frame work.
- No 1-2 sample hardcoding (Codex #7 generalization guardrail).
- No production refactor for tests (IMP-32 owns helper extract).
- figma_to_html / V4 / Phase Z 3-layer separation preserved.
- 1 commit = 1 IMP-04b decision unit (bundled u1~u24 per Stage 2
  plan; CAT+WIP atomicity for u3/u4 preserved).

Tests: tests/test_family_contract_baseline.py 4/4 PASS.
Cross-ref: IMP-04 (#4), IMP-29 (#38), IMP-30 (#39), IMP-31 (#40),
IMP-32 (#41), IMP-33 (#61), IMP-47A (#75).

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

166 lines
6.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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.
IMP-04b (#42) extends the rule with a second exemption axis: catalog entries
flagged `visual_pending: true` are contracted but have no family partial on
disk yet (Track A/B VP frames). The invariant becomes:
contracts == (disk - wip) vp
where `vp ∩ disk == ∅` (VP entry with disk file = promotion overdue).
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 + VP axis)
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"
V4_EVIDENCE_PATH = PROJECT_ROOT / "tests" / "matching" / "v4_full32_result.yaml"
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 _load_v4_evidence_template_ids() -> set[str]:
"""Unique `template_id` values across all V4 full32 judgments.
IMP-04b (#42) closure gate derives its 32-frame target from V4 evidence
rather than a hardcoded count, so future Track C additions extend both
surfaces in lockstep.
"""
with V4_EVIDENCE_PATH.open(encoding="utf-8") as f:
v4 = yaml.safe_load(f)
return {
j["template_id"]
for sec in v4["mdx_sections"].values()
for j in sec.get("judgments_full32", [])
if "template_id" in j
}
def _load_vp_exempt_keys() -> set[str]:
"""Catalog entries flagged `visual_pending: true` (IMP-04b / #42).
VP = contracted but no family partial on disk yet (Track A/B VP frames).
Exempt from the disk-family existence check until the partial is authored.
"""
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) and v.get("visual_pending") is True
}
def test_contracts_set_equals_disk_families_minus_wip():
"""`frame_contracts.yaml` keys ↔ (disk family stems WIP) VP-exempt."""
contracts = _load_contract_keys()
disk = _load_disk_family_stems()
wip = _load_wip_allowlist()
vp = _load_vp_exempt_keys()
expected = disk - wip
missing = expected - contracts
extra = (contracts - vp) - 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"(and not flagged `visual_pending: true`): {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."
)
def test_vp_exempt_keys_are_contracted_and_disk_absent():
"""VP-exempt keys: must be in catalog (by construction) AND must not have
a family partial on disk (VP = pending partial authoring per #42)."""
contracts = _load_contract_keys()
disk = _load_disk_family_stems()
vp = _load_vp_exempt_keys()
leaked_outside_contracts = vp - contracts
has_disk_partial = vp & disk
assert not leaked_outside_contracts, (
f"VP-exempt keys not in catalog: {sorted(leaked_outside_contracts)}."
)
assert not has_disk_partial, (
f"VP-exempt entries have a family partial on disk: "
f"{sorted(has_disk_partial)}. Drop `visual_pending: true` from the "
"catalog entry (authoring complete) instead of carrying the flag."
)
def test_imp04b_closure_gate_v4_coverage_and_wip_empty():
"""IMP-04b (#42) u24 closure gate: catalog set-equals V4 evidence + WIP==0.
Locks the 32/32 frame coverage by comparing catalog top-level keys to
`tests/matching/v4_full32_result.yaml` unique `template_id` values, and
asserts the WIP allowlist is empty (both partials absorbed in u3/u4).
"""
contracts = _load_contract_keys()
wip = _load_wip_allowlist()
v4_template_ids = _load_v4_evidence_template_ids()
missing = v4_template_ids - contracts
extra = contracts - v4_template_ids
assert not missing, (
f"IMP-04b closure gate: V4 template_ids not in frame_contracts.yaml: "
f"{sorted(missing)}."
)
assert not extra, (
f"IMP-04b closure gate: frame_contracts.yaml has entries outside "
f"V4 evidence: {sorted(extra)}. Extend V4 evidence first or scope "
"the addition under a follow-up IMP."
)
assert not wip, (
f"IMP-04b closure gate: WIP allowlist must be empty: {sorted(wip)}."
)