feat(#85): IMP catalog builder invariant + VP runtime gate (u1~u7)

- u1: BuilderMissingError(FitError) — narrow exception aligned with pipeline catch
- u2: load_frame_contracts catalog invariant + VP skip + CatalogInvariantError
- u3a: audit CLI I1~I3 (partial existence / declared builder / registry membership)
- u3b: audit CLI I4 (slot_payload refs vs declared/generated payload keys)
- u4: lookup_v4_candidates VP filter (lookup_v4_all_judgments raw telemetry untouched)
- u5: catalog invariant regression coverage + temp non-VP failure fixtures
- u6: mdx04 VP routing fixture tests (sw_dependency_four_problems excluded from live)
- u7: tests/conftest.py env isolation + mdx03/mdx04/mdx05 subprocess smoke

Targeted 74 PASS (12.31s). Full regression 1063 PASS (87.70s). Audit CLI clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-23 16:56:38 +09:00
parent d9d338416a
commit cacc5b30db
14 changed files with 2163 additions and 3 deletions

View File

@@ -1099,6 +1099,20 @@ def lookup_v4_all_judgments(
return out
def _is_visual_pending(template_id: str) -> bool:
"""IMP-#85 u4 — return True iff catalog marks contract as ``visual_pending``.
Data-driven from ``frame_contracts.yaml`` (no hard-coded frame allow-list).
Used by ``lookup_v4_candidates`` to exclude VP frames from the live
candidate set; ``lookup_v4_all_judgments`` raw telemetry stays untouched
(Step 7-A axis preserves full 32-frame evidence for the frontend).
"""
contract = get_contract(template_id)
if not isinstance(contract, dict):
return False
return contract.get("visual_pending") is True
def lookup_v4_candidates(
v4: dict,
section_id: str,
@@ -1112,6 +1126,7 @@ def lookup_v4_candidates(
v4_candidates = [
c for c in judgments_full32
if c["label"] != "reject"
and not visual_pending(c.template_id) # IMP-#85 u4
][:max_n]
Returns:
@@ -1123,6 +1138,11 @@ def lookup_v4_candidates(
lookup_v4_match() (rank-1) 는 그대로. Step 6 의 plan_composition()
호출처 무변. 본 함수는 Step 5 artifact + Step 9 application_plan input
위한 새 entry point.
IMP-#85 u4 — visual_pending frames are excluded from the live candidate
set (catalog scaffolding without registered builder would crash the
mapper). lookup_v4_all_judgments raw telemetry is intentionally NOT
gated here.
"""
resolved = _resolve_v4_section_key(v4, section_id, alias_keys=alias_keys)
sec = v4.get("mdx_sections", {}).get(resolved) if resolved else None
@@ -1133,6 +1153,9 @@ def lookup_v4_candidates(
for j in judgments:
if j.get("label") == "reject":
continue
tid = j.get("template_id")
if tid and _is_visual_pending(tid):
continue
candidates.append(_v4_match_from_judgment(section_id, j))
if len(candidates) >= max_n:
break