IMP-05 deterministic V4 candidate bridge — pre-render rank-2/3 fallback + trace schema + dedup invariant test
round 55~73 review-loop lock per Codex #11 final + Claude #13 6-axis L1~L9. Scope (deterministic only) : - pre-render rank-2/3 fallback via lookup_v4_match_with_fallback (selector only, no calculate_fit migration, no AI, no full planner rerun, no layout topology change, no abort behavior change) - Step 9 informative candidate_evidence schema (additive) — v4_label / phase_z_status / catalog_registered / filtered_for_direct_execution / route_hint / decision / reason - Step 20 qualifier fields (additive) — fallback_used / fallback_selection_count / selection_paths[] — top-level enum unchanged - restructure / reject candidates preserved as non-direct evidence with route hints (design_reference_only / ai_adaptation_required) — deferred actual handlers IMP-29/IMP-31 - catalog 1:1 invariant test (separate file tests/test_catalog_invariant.py) — fails fast if template_id/frame_id 1:1 mapping ever breaks - 6 behavior tests fully synthetic with MOCK_ prefix (no real catalog IDs, no v4_full32_result.yaml dependency) — monkeypatch get_contract + compute_capacity_fit (selector has no DI, function signature unchanged) Deferred to follow-up issues : - IMP-30 first-render invariant + abort bypass (zero-unit + section status filter) - IMP-29 frontend zone-level override (deterministic only) - IMP-31 AI-assisted frame-aware adaptation Guardrails locked : no calculate_fit / no AI / no frontend / no full rerun / no layout topology / no abort behavior change / no 1-2 sample hardcoding. Tests : 8/8 pass (6 selector behavior + 2 catalog invariant). Smoke regression : 11/11 partials pass (IMP-04 F17 calibration intact). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
81
tests/test_catalog_invariant.py
Normal file
81
tests/test_catalog_invariant.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""Phase Z catalog invariant test — real `frame_contracts.yaml` 1:1 mapping verify.
|
||||
|
||||
IMP-05 L4 lock per Claude #13 §3 :
|
||||
- real catalog read (purpose 자체 = real catalog 검증)
|
||||
- template_id ↔ frame_id 1:1 mapping (Codex #6 terminology — 2 reference keys for same entry)
|
||||
- fail fast with explicit message if catalog policy changes
|
||||
|
||||
Codex #5 verified : 11 templates / 11 frames, all unique = 1:1 mapping confirm (2026-05-13).
|
||||
Codex #7 generalization guardrail : real catalog OK (purpose 자체) — NOT sample-hardcoding.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
CATALOG_PATH = PROJECT_ROOT / "templates" / "phase_z2" / "catalog" / "frame_contracts.yaml"
|
||||
|
||||
|
||||
def _load_catalog() -> dict:
|
||||
with CATALOG_PATH.open(encoding="utf-8") as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
def test_catalog_template_id_to_frame_id_one_to_one():
|
||||
"""Verify each catalog entry has unique template_id + unique frame_id (1:1 reference keys).
|
||||
|
||||
Fails fast if the catalog policy ever drifts from this assumption — IMP-05 dedup
|
||||
relies on `template_id` as the runtime key and assumes one frame per template.
|
||||
"""
|
||||
catalog = _load_catalog()
|
||||
|
||||
template_ids = []
|
||||
frame_ids = []
|
||||
for entry_key, entry in catalog.items():
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
tid = entry.get("template_id")
|
||||
fid = entry.get("frame_id")
|
||||
assert tid is not None, f"entry {entry_key} missing template_id"
|
||||
assert fid is not None, f"entry {entry_key} missing frame_id"
|
||||
template_ids.append(tid)
|
||||
frame_ids.append(str(fid))
|
||||
|
||||
duplicate_templates = [t for t in template_ids if template_ids.count(t) > 1]
|
||||
duplicate_frames = [f for f in frame_ids if frame_ids.count(f) > 1]
|
||||
|
||||
assert not duplicate_templates, (
|
||||
"Phase Z catalog currently expects one template_id per frame_id; "
|
||||
"update dedup policy if this changes. "
|
||||
f"Duplicate template_ids found: {set(duplicate_templates)}"
|
||||
)
|
||||
assert not duplicate_frames, (
|
||||
"Phase Z catalog currently expects one template_id per frame_id; "
|
||||
"update dedup policy if this changes. "
|
||||
f"Duplicate frame_ids found: {set(duplicate_frames)}"
|
||||
)
|
||||
assert len(template_ids) == len(frame_ids), (
|
||||
"Phase Z catalog template_id count must equal frame_id count "
|
||||
f"(templates={len(template_ids)}, frames={len(frame_ids)})."
|
||||
)
|
||||
|
||||
|
||||
def test_catalog_entry_count_matches_frame_count():
|
||||
"""Sanity guard — each entry contributes one template_id + one frame_id."""
|
||||
catalog = _load_catalog()
|
||||
entry_count = sum(1 for v in catalog.values() if isinstance(v, dict))
|
||||
template_count = sum(
|
||||
1 for v in catalog.values()
|
||||
if isinstance(v, dict) and v.get("template_id") is not None
|
||||
)
|
||||
frame_count = sum(
|
||||
1 for v in catalog.values()
|
||||
if isinstance(v, dict) and v.get("frame_id") is not None
|
||||
)
|
||||
assert entry_count == template_count == frame_count, (
|
||||
f"catalog shape inconsistent: entries={entry_count} "
|
||||
f"templates={template_count} frames={frame_count}"
|
||||
)
|
||||
Reference in New Issue
Block a user