- u1~u9: AI fallback infrastructure (router/prompts/schema/validator) + Step 12 hook - u10: e2e reject chain (writes final.html with AI-repaired slot, full coverage) - u11: frontend wiring deferred to follow-up commit (split from IMP-41 hunks) - u12: coverage_invariant guard - u13: cache save gate (visual_check PASS + user_approved/auto_cache) — Codex #22 verified Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
181 lines
6.0 KiB
Python
181 lines
6.0 KiB
Python
"""IMP-47B u3 — override-selected reject frames are admitted as provisional.
|
|
|
|
Scope (this slice):
|
|
Helper `_apply_frame_override_to_unit` (src/phase_z2_pipeline.py) covers
|
|
the three probe layers used by the `--override-frame` path:
|
|
|
|
1. ``v4_candidates`` exact match (non-reject; existing behaviour).
|
|
2. Full 32 V4 judgments probe (reject inclusive) — when the user
|
|
picks a reject frame, the unit is promoted to
|
|
``provisional=True`` with ``label="reject"`` so Step 12
|
|
(IMP-47B u4) admits the AI repair path.
|
|
3. Raw fall-through (template_id only) — no provisional promotion,
|
|
no label mutation.
|
|
|
|
Frame visual / contract stay untouched per the AI isolation contract
|
|
(frame auto-swap forbidden — AI re-places content into the existing
|
|
frame only). Sibling test confirms a non-reject override still goes
|
|
through the v4_candidates path without provisional promotion.
|
|
|
|
Synthetic naming convention mirrors tests/test_phase_z2_imp30_first_render.py
|
|
(MOCK_ prefix mandatory, no real catalog template_id / frame_id leakage).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Optional
|
|
|
|
from src.phase_z2_pipeline import _apply_frame_override_to_unit
|
|
|
|
|
|
@dataclass
|
|
class _StubCandidate:
|
|
template_id: str
|
|
frame_id: str
|
|
frame_number: int
|
|
confidence: float
|
|
label: str
|
|
|
|
|
|
@dataclass
|
|
class _StubUnit:
|
|
source_section_ids: list[str]
|
|
frame_template_id: Optional[str] = None
|
|
frame_id: Optional[str] = None
|
|
frame_number: int = 0
|
|
confidence: float = 0.0
|
|
label: Optional[str] = None
|
|
provisional: bool = False
|
|
v4_candidates: list = field(default_factory=list)
|
|
|
|
|
|
def _v4_with_reject(section_id: str, target_tid: str) -> dict:
|
|
"""Synthetic V4 dict with target_tid mapped to a reject judgment.
|
|
|
|
Mirrors the production V4 schema surface (``mdx_sections`` →
|
|
``judgments_full32`` → list of judgment dicts with template_id /
|
|
frame_id / frame_number / confidence / label). Two judgments so we
|
|
can also assert that the helper picks the reject entry rather than
|
|
the first non-reject one when the template_ids differ.
|
|
"""
|
|
return {
|
|
"mdx_sections": {
|
|
section_id: {
|
|
"judgments_full32": [
|
|
{
|
|
"template_id": "MOCK_T_other",
|
|
"frame_id": "F_other",
|
|
"frame_number": 1,
|
|
"confidence": 0.85,
|
|
"label": "use_as_is",
|
|
},
|
|
{
|
|
"template_id": target_tid,
|
|
"frame_id": "F_reject",
|
|
"frame_number": 32,
|
|
"confidence": 0.40,
|
|
"label": "reject",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
# ─── Case 1 : reject override → provisional promotion ────────────
|
|
|
|
|
|
def test_override_to_reject_judgment_marks_unit_provisional():
|
|
"""User picks a reject frame → unit.label=reject, provisional=True.
|
|
|
|
Frame metadata is sourced from the reject judgment (frame_id /
|
|
frame_number / confidence) so Step 9 metadata stays consistent.
|
|
"""
|
|
unit = _StubUnit(
|
|
source_section_ids=["MOCK_S1"],
|
|
frame_template_id="MOCK_T_auto",
|
|
frame_id="F_auto",
|
|
frame_number=5,
|
|
confidence=0.90,
|
|
label="use_as_is",
|
|
provisional=False,
|
|
)
|
|
v4 = _v4_with_reject("MOCK_S1", "MOCK_T_reject")
|
|
|
|
meta = _apply_frame_override_to_unit(unit, "MOCK_T_reject", v4)
|
|
|
|
assert meta == "v4_reject_judgment_provisional"
|
|
assert unit.frame_template_id == "MOCK_T_reject"
|
|
assert unit.frame_id == "F_reject"
|
|
assert unit.frame_number == 32
|
|
assert unit.confidence == 0.40
|
|
assert unit.label == "reject"
|
|
assert unit.provisional is True
|
|
|
|
|
|
# ─── Case 2 : non-reject override → existing v4_candidates path ───
|
|
|
|
|
|
def test_override_to_v4_candidate_keeps_non_provisional():
|
|
"""User picks a non-reject candidate → existing v4_candidates path.
|
|
|
|
Helper takes the early v4_candidates branch without consulting the
|
|
full 32 judgments. provisional remains False (normal-path AI=0
|
|
contract — IMP-30 / IMP-47B router gate intact for this unit).
|
|
"""
|
|
unit = _StubUnit(
|
|
source_section_ids=["MOCK_S2"],
|
|
frame_template_id="MOCK_T_auto",
|
|
frame_id="F_auto",
|
|
frame_number=3,
|
|
confidence=0.95,
|
|
label="use_as_is",
|
|
provisional=False,
|
|
v4_candidates=[
|
|
_StubCandidate(
|
|
template_id="MOCK_T_pick",
|
|
frame_id="F_pick",
|
|
frame_number=2,
|
|
confidence=0.85,
|
|
label="light_edit",
|
|
),
|
|
],
|
|
)
|
|
v4 = {"mdx_sections": {}} # full-judgment probe must NOT be reached
|
|
|
|
meta = _apply_frame_override_to_unit(unit, "MOCK_T_pick", v4)
|
|
|
|
assert meta == "v4_candidates"
|
|
assert unit.frame_template_id == "MOCK_T_pick"
|
|
assert unit.frame_id == "F_pick"
|
|
assert unit.label == "light_edit"
|
|
assert unit.provisional is False
|
|
|
|
|
|
# ─── Case 3 : unknown template → raw fall-through (no provisional) ─
|
|
|
|
|
|
def test_override_unknown_template_falls_through_without_provisional():
|
|
"""Template ID absent from v4_candidates AND from judgments_full32 →
|
|
raw_template_id_only path. No provisional flag, no label change.
|
|
"""
|
|
unit = _StubUnit(
|
|
source_section_ids=["MOCK_S3"],
|
|
frame_template_id="MOCK_T_auto",
|
|
frame_id="F_auto",
|
|
frame_number=4,
|
|
confidence=0.92,
|
|
label="use_as_is",
|
|
provisional=False,
|
|
)
|
|
v4 = {"mdx_sections": {}}
|
|
|
|
meta = _apply_frame_override_to_unit(unit, "MOCK_T_unknown", v4)
|
|
|
|
assert meta == "raw_template_id_only"
|
|
assert unit.frame_template_id == "MOCK_T_unknown"
|
|
# frame_id / label unchanged — caller's print path warns on this case.
|
|
assert unit.frame_id == "F_auto"
|
|
assert unit.label == "use_as_is"
|
|
assert unit.provisional is False
|