fix(IMP-05): complete V4 fallback evidence and dedup qualifiers
Refs #5 - Add runtime template_id dedup in lookup_v4_match_with_fallback with first-occurrence reservation; duplicate ranks become audit evidence, not new fallback candidates. - Add Step 9 candidate_evidence as the primary per-unit evidence field while keeping fallback_chain as a compat alias for legacy readers. - Add Step 20 fallback_selection_count and selection_paths derived from comp_debug.v4_fallback_summary with defensive defaults; top-level overall enum unchanged. - Tighten synthetic fallback tests for duplicate handling (rank-1 reject A + rank-2 use_as_is A + rank-3 distinct B → rank-3 wins) and add tests for candidate_evidence + alias equality and Step 20 qualifier presence with defensive defaults. - Verify with pytest (10 passed) and smoke_frame_render --self-check (11/11 partials, IMP-04 F17 calibration intact). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -491,6 +491,12 @@ def lookup_v4_match_with_fallback(
|
||||
return None, trace
|
||||
|
||||
first_skip_reason: Optional[str] = None
|
||||
# IMP-05 L4 dedup (Codex #14 ordering — Claude #16 placement precision) :
|
||||
# first occurrence claims template_id for the chain regardless of decision
|
||||
# (selected/non-direct/rejected/missing-contract/capacity-skipped). Defensive
|
||||
# against V4 anomaly where same template_id appears at multiple ranks with
|
||||
# different labels — first label/reason is preserved, later duplicates skip.
|
||||
seen_template_ids: set[str] = set()
|
||||
for i, judgment in enumerate(judgments, start=1):
|
||||
match = _v4_match_from_judgment(section_id, judgment, rank=i)
|
||||
status = to_phase_z_status(match)
|
||||
@@ -515,6 +521,15 @@ def lookup_v4_match_with_fallback(
|
||||
"reason": None,
|
||||
}
|
||||
|
||||
# IMP-05 L4 dedup — duplicate check BEFORE rank evaluation.
|
||||
# First occurrence reserves template_id even if non-direct/rejected/skipped.
|
||||
# Later rank with same template_id is skipped as duplicate, audit fields preserved.
|
||||
if match.template_id in seen_template_ids:
|
||||
candidate_trace["reason"] = "duplicate_template_id"
|
||||
trace["candidates"].append(candidate_trace)
|
||||
continue
|
||||
seen_template_ids.add(match.template_id)
|
||||
|
||||
if status not in MVP1_ALLOWED_STATUSES:
|
||||
candidate_trace["reason"] = f"phase_z_status_not_allowed:{status}"
|
||||
elif get_contract(match.template_id) is None:
|
||||
@@ -1350,6 +1365,13 @@ def compute_slide_status(sections: list[MdxSection],
|
||||
else:
|
||||
overall = "PARTIAL_COVERAGE_WITH_VISUAL_REGRESSION"
|
||||
|
||||
# IMP-05 L3 (Codex #10 D4 / #17 idea F / Claude #21 idea J) — Step 20 qualifier fields.
|
||||
# Additive only — top-level overall enum unchanged. Defensive defaults so non-fallback
|
||||
# paths (empty v4_fallback_summary) do not crash and report 0 / [] cleanly.
|
||||
_v4_fb_summary = comp_debug.get("v4_fallback_summary", {}) or {}
|
||||
_fallback_selection_count = _v4_fb_summary.get("fallback_selection_count", 0)
|
||||
_selection_paths = _v4_fb_summary.get("selection_paths", [])
|
||||
|
||||
return {
|
||||
"rendered": True,
|
||||
"visual_check_passed": visual_passed,
|
||||
@@ -1361,6 +1383,9 @@ def compute_slide_status(sections: list[MdxSection],
|
||||
"selection_path": "fallback_used" if fallback_selections else "rank_1",
|
||||
"fallback_used": bool(fallback_selections),
|
||||
"fallback_selections": fallback_selections,
|
||||
# IMP-05 L3 qualifier fields — grouped near existing fallback fields for readability.
|
||||
"fallback_selection_count": _fallback_selection_count,
|
||||
"selection_paths": _selection_paths,
|
||||
"visual_fail_reasons": list(overflow.get("fail_reasons") or []),
|
||||
"adapter_needed_count": len(adapter_needed_units),
|
||||
"adapter_needed_units": adapter_needed_units,
|
||||
@@ -2706,7 +2731,11 @@ def run_phase_z2_mvp1(
|
||||
"selection_path": unit.selection_path,
|
||||
"fallback_used": bool(unit.selection_path and "fallback" in unit.selection_path),
|
||||
"fallback_reason": unit.fallback_reason,
|
||||
"fallback_chain": selection_trace.get("candidates", []),
|
||||
# IMP-05 L2 (Codex #10 D4 / #16 idea A) — Step 9 per-unit candidate evidence.
|
||||
# candidate_evidence is the primary field for future frontend / AI consumers.
|
||||
# fallback_chain is kept as a compat alias for any pre-IMP-05 reader.
|
||||
"candidate_evidence": selection_trace.get("candidates", []),
|
||||
"fallback_chain": selection_trace.get("candidates", []), # compat alias; prefer candidate_evidence
|
||||
"v4_candidates": [
|
||||
{
|
||||
"template_id": c.template_id,
|
||||
|
||||
Reference in New Issue
Block a user