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:
2026-05-21 19:39:16 +09:00
parent 15ef7c65e9
commit a06dd3d4b0
3 changed files with 1164 additions and 7 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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.

View File

@@ -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)}."
)