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:
2026-05-13 23:06:39 +09:00
parent 73a98b8ad1
commit 15c5b9ae00
5 changed files with 600 additions and 31 deletions

View File

@@ -343,6 +343,9 @@ class CompositionUnit:
phase_z_status: str
raw_content: str
title: str
v4_rank: Optional[int] = None
selection_path: str = "rank_1"
fallback_reason: Optional[str] = None
score: float = 0.0
rationale: dict = field(default_factory=dict)
@@ -473,6 +476,9 @@ def collect_candidates(sections, v4_lookup_fn, v4_label_to_status: dict,
confidence=match.confidence,
label=match.label,
phase_z_status=v4_label_to_status.get(match.label, "unknown"),
v4_rank=getattr(match, "v4_rank", None),
selection_path=getattr(match, "selection_path", "rank_1"),
fallback_reason=getattr(match, "fallback_reason", None),
raw_content=s.raw_content,
title=s.title,
v4_candidates=_v4_cands(s.section_id),
@@ -504,6 +510,9 @@ def collect_candidates(sections, v4_lookup_fn, v4_label_to_status: dict,
confidence=parent_match.confidence,
label=parent_match.label,
phase_z_status=v4_label_to_status.get(parent_match.label, "unknown"),
v4_rank=getattr(parent_match, "v4_rank", None),
selection_path=getattr(parent_match, "selection_path", "rank_1"),
fallback_reason=getattr(parent_match, "fallback_reason", None),
raw_content=merged_raw,
title=pid,
v4_candidates=_v4_cands(pid),
@@ -597,6 +606,9 @@ def collect_candidates(sections, v4_lookup_fn, v4_label_to_status: dict,
confidence=rep_match.confidence,
label=rep_match.label,
phase_z_status=rep_status,
v4_rank=getattr(rep_match, "v4_rank", None),
selection_path=getattr(rep_match, "selection_path", "rank_1"),
fallback_reason=getattr(rep_match, "fallback_reason", None),
raw_content=merged_raw,
title=pid,
auto_selectable=auto_selectable,
@@ -773,6 +785,9 @@ def plan_composition(sections, v4_lookup_fn, v4_label_to_status: dict,
"template_id": c.frame_template_id,
"label": c.label,
"phase_z_status": c.phase_z_status,
"v4_rank": c.v4_rank,
"selection_path": c.selection_path,
"fallback_reason": c.fallback_reason,
"score": c.score,
"selection_state": _candidate_state(c),
"auto_selectable": c.auto_selectable,