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>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -18,8 +18,9 @@ Active contracted family count = **11**. Tracked basenames ↔ `frame_contracts.
|
||||
|
||||
| File | Figma frame | Status |
|
||||
|---|---|---|
|
||||
| `app_sw_package_vs_solution.html` | frame 23 (`1171281203`) | WIP — not in `frame_contracts.yaml`, not in runtime matcher set. |
|
||||
| `pre_construction_model_info_stacked.html` | frame 9 (`1171281180`) | WIP — not in `frame_contracts.yaml`, not in runtime matcher set. |
|
||||
|
||||
> **#42 IMP-04b u3 (2026-05-21)** — frame 23 (`1171281203`) partial absorbed → `frame_contracts.yaml::app_sw_package_vs_solution`. Counts in `## Baseline lock (2026-05-19)` are historical (post-u3: 11 tracked + 1 WIP / 12 contract).
|
||||
> **#42 IMP-04b u4 (2026-05-21)** — frame 9 (`1171281180`) partial absorbed → `frame_contracts.yaml::pre_construction_model_info_stacked`. WIP family table now empty (post-u4: 13 tracked / 13 contract).
|
||||
|
||||
These files are partials authored during Phase Z-2 MVP-1.5b exploration. They are **not** part of the contracted Phase Z runtime catalog and must not be enumerated by frame selection, matcher, or any Stage 3 pipeline surface.
|
||||
|
||||
|
||||
@@ -6,11 +6,19 @@ 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)
|
||||
- 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.
|
||||
@@ -26,6 +34,7 @@ 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]:
|
||||
@@ -43,14 +52,46 @@ def _load_wip_allowlist() -> set[str]:
|
||||
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 minus WIP allowlist."""
|
||||
"""`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 - expected
|
||||
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)}. "
|
||||
@@ -58,8 +99,8 @@ def test_contracts_set_equals_disk_families_minus_wip():
|
||||
"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)}."
|
||||
f"frame_contracts.yaml has entries with no matching family file "
|
||||
f"(and not flagged `visual_pending: true`): {sorted(extra)}."
|
||||
)
|
||||
|
||||
|
||||
@@ -78,3 +119,47 @@ def test_wip_allowlist_is_disk_only_and_uncontracted():
|
||||
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)}."
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user