wip: phase_z2 evidence 파이프라인 + matching 실험(phase2~26) + 프론트 trace 패널 진행분 스냅샷

- src: phase_z2 composition/mapper/pipeline/placement_planner/retry, ai_fallback(prompts/schema/validate), mdx_text_atoms 신규
- Front: PipelineTracePanel 신규, FramePanel/SlideCanvas/Home/designAgentApi 등 갱신 + 테스트 4종 추가
- templates/phase_z2: catalog(component_expansion_registry, node_slot_mapping 신규), frames, families, slide_base 갱신
- tests/matching: phase2~26 매칭 실험 스크립트·리포트·온톨로지 전체 (미커밋 진행분)
- tests: b4_v4 evidence, task5~28.5 시리즈, regression(imp95 baseline) 등 신규 테스트 대량 추가
- docs/reference: MDX 구조 인벤토리, MDX→Frame 구조 계약 문서
- scripts: mdx 계약/parity/coverage/viewport 체크, gitea comment, run sync 유틸
- .gitignore: tmp*.json, chromedriver, .orchestrator, *.pkl, Front_test* 등 임시/스냅샷 제외

미완성 작업의 보존용 스냅샷 커밋 (2026-07-02)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-02 17:03:42 +09:00
co-authored by Claude Opus 4.8
parent 97b7833a1b
commit b836e79ee1
527 changed files with 673036 additions and 717 deletions
@@ -0,0 +1,112 @@
"""IMP-95 u1 — PHASE_Z_B4_V4_EVIDENCE flag reader unit tests.
Stage 2 plan (u1): locks the env flag reader helper and trace key
constants (default OFF). u2 wires the selector under
``accepted_content_types`` constraint; u3 extends ``plan_placement`` to
emit additive trace fields; u4 exposes them in placement_trace at
Step 11; u5 adds the gatekeeper short-circuit; u6 layers the
``partial_exists`` precheck.
Truthy contract mirrors ``_b4_mapper_source_enabled`` at
``src/phase_z2_pipeline.py:675-688``: case-insensitive + leading/trailing
whitespace stripped; truthy set = {'1', 'true', 'yes'}. Everything else
(including '0', '', 'no', 'false', missing env var) is OFF.
Flag independence (u1 scope-lock + Stage 2 A8): the new flag must NOT be
flipped by ``PHASE_Z_B4_MAPPER_SOURCE`` (IMP-89 89-a) or
``PHASE_Z_B4_GATEKEEPER``. Default OFF preserves final.html SHA parity
(Stage 2 A1 + A10).
"""
from __future__ import annotations
import pytest
from src.phase_z2_placement_planner import (
FRAME_SELECTION_BASIS_DECLARATION_ORDER,
FRAME_SELECTION_BASIS_V4_RANKED,
PHASE_Z_B4_V4_EVIDENCE_ENV,
TRACE_KEY_B4_PARTIAL_MISSING_SKIP,
TRACE_KEY_B4_V0_FALLBACK_REASON,
TRACE_KEY_FRAME_SELECTION_BASIS,
TRACE_KEY_V4_B4_FRAME_MATCH,
TRACE_KEY_V4_EVIDENCE_CONSUMED,
TRACE_KEY_V4_RANK_USED,
_b4_v4_evidence_enabled,
)
FLAG = "PHASE_Z_B4_V4_EVIDENCE"
def test_env_constant_matches_flag_name() -> None:
"""PHASE_Z_B4_V4_EVIDENCE_ENV is the literal env var the reader inspects."""
assert PHASE_Z_B4_V4_EVIDENCE_ENV == FLAG
def test_default_off_when_env_unset(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv(FLAG, raising=False)
assert _b4_v4_evidence_enabled() is False
@pytest.mark.parametrize("value", ["1", "true", "yes", "TRUE", "Yes", " true ", " 1\t"])
def test_truthy_values_enable_flag(
monkeypatch: pytest.MonkeyPatch, value: str
) -> None:
monkeypatch.setenv(FLAG, value)
assert _b4_v4_evidence_enabled() is True
@pytest.mark.parametrize("value", ["", "0", "no", "false", "off", "2", "on", "y"])
def test_non_truthy_values_keep_flag_off(
monkeypatch: pytest.MonkeyPatch, value: str
) -> None:
monkeypatch.setenv(FLAG, value)
assert _b4_v4_evidence_enabled() is False
def test_flag_distinct_from_mapper_source(monkeypatch: pytest.MonkeyPatch) -> None:
"""PHASE_Z_B4_MAPPER_SOURCE ON must not flip the V4 evidence flag.
Locks Stage 2 A8 (flag independence): IMP-89 89-a mapper-source flag
governs slot_payload source-of-truth; IMP-95 V4 evidence flag governs
_select_frame ranking. They must be independently toggleable.
"""
monkeypatch.setenv("PHASE_Z_B4_MAPPER_SOURCE", "1")
monkeypatch.delenv(FLAG, raising=False)
assert _b4_v4_evidence_enabled() is False
def test_flag_distinct_from_gatekeeper(monkeypatch: pytest.MonkeyPatch) -> None:
"""PHASE_Z_B4_GATEKEEPER ON must not flip the V4 evidence flag.
Locks Stage 2 A8 (flag independence): gatekeeper retains its
mismatch render-skip semantics; V4 evidence flag is orthogonal.
"""
monkeypatch.setenv("PHASE_Z_B4_GATEKEEPER", "1")
monkeypatch.delenv(FLAG, raising=False)
assert _b4_v4_evidence_enabled() is False
def test_trace_key_constants_are_unique_strings() -> None:
"""u1 locks trace key NAMES so u3~u6 cannot drift them silently."""
keys = {
TRACE_KEY_FRAME_SELECTION_BASIS,
TRACE_KEY_V4_EVIDENCE_CONSUMED,
TRACE_KEY_B4_V0_FALLBACK_REASON,
TRACE_KEY_V4_RANK_USED,
TRACE_KEY_V4_B4_FRAME_MATCH,
TRACE_KEY_B4_PARTIAL_MISSING_SKIP,
}
assert len(keys) == 6
assert TRACE_KEY_FRAME_SELECTION_BASIS == "frame_selection_basis"
assert TRACE_KEY_V4_EVIDENCE_CONSUMED == "v4_evidence_consumed"
assert TRACE_KEY_B4_V0_FALLBACK_REASON == "b4_v0_fallback_reason"
assert TRACE_KEY_V4_RANK_USED == "v4_rank_used"
assert TRACE_KEY_V4_B4_FRAME_MATCH == "v4_b4_frame_match"
assert TRACE_KEY_B4_PARTIAL_MISSING_SKIP == "b4_partial_missing_skip"
def test_frame_selection_basis_enum_values() -> None:
"""u1 locks enum values so u2 selector tests can rely on them."""
assert FRAME_SELECTION_BASIS_DECLARATION_ORDER == "declaration_order"
assert FRAME_SELECTION_BASIS_V4_RANKED == "v4_ranked"
assert FRAME_SELECTION_BASIS_DECLARATION_ORDER != FRAME_SELECTION_BASIS_V4_RANKED
@@ -0,0 +1,436 @@
"""IMP-95 u5 — Gatekeeper short-circuit (``v4_short_circuit``) telemetry.
Stage 2 plan (u5 / axes A7 + A8): the Step 11 ``placement_trace`` assembly
must carry a derived ``v4_short_circuit`` boolean that is True iff V4
evidence was consumed (``PHASE_Z_B4_V4_EVIDENCE`` flag ON + a V4 rank
resolved during selection) AND the resulting B4 selection matches the
mapper's V4 rank-1 ``template_id``. The gatekeeper logic itself (the
``PHASE_Z_B4_GATEKEEPER`` branch that appends to ``adapter_needed_units``)
must remain unchanged — gatekeeper triggers purely on ``not matches_mapper``
regardless of V4 path. PHASE_Z_B4_V4_EVIDENCE and PHASE_Z_B4_GATEKEEPER
stay independent flags.
Coverage axes :
* Derived-field contract — ``v4_short_circuit = v4_evidence_consumed AND
matches_mapper`` (Stage 2 A7).
* Flag-OFF parity — ``v4_short_circuit=False`` on every code path when
``PHASE_Z_B4_V4_EVIDENCE`` is OFF, regardless of mapper match (Stage 2
A1 + A10 — final.html SHA parity precondition).
* Flag-ON happy path — V4 rank-1 produces a mapper-matching selection,
``v4_short_circuit=True``.
* Flag-ON deliberate-mismatch — V4 evidence resolved but mapper template
differs from the V4-selected contract; ``v4_short_circuit=False`` and
the gatekeeper still triggers on ``not matches_mapper`` (Stage 2 A8 —
flag independence; V4 evidence consumption alone does NOT suppress the
gatekeeper).
* Flag-ON empty evidence — V4 evidence empty / None falls back to
declaration order; ``v4_short_circuit=False`` because evidence was not
consumed even when the fallback selection happens to match the mapper.
* Adapter-record contract — when the gatekeeper triggers (mismatch),
``adapter_record`` carries ``v4_short_circuit`` (always False on this
branch) so downstream consumers can read the V4-path outcome on the
rejected branch as well.
* Flag-independence structural assertion — the gatekeeper trigger
condition in ``src/phase_z2_pipeline.py`` references neither
``PHASE_Z_B4_V4_EVIDENCE`` nor ``v4_short_circuit``; only the
placement_trace assembly + adapter_record do.
Out of scope for u5 (Stage 2 scope-lock) :
* Partial-exists precheck (u6).
* Full-pipeline SHA / adapter_needed / trace-field regressions
(u8 / u9 / u10).
* Status-board markers (u11).
"""
from __future__ import annotations
import re
from dataclasses import asdict, dataclass
from pathlib import Path
import pytest
from src.phase_z2_content_extractor import ContentObject
from src.phase_z2_placement_planner import (
FRAME_SELECTION_BASIS_DECLARATION_ORDER,
FRAME_SELECTION_BASIS_V4_RANKED,
PHASE_Z_B4_V4_EVIDENCE_ENV,
TRACE_KEY_B4_V0_FALLBACK_REASON,
TRACE_KEY_FRAME_SELECTION_BASIS,
TRACE_KEY_V4_B4_FRAME_MATCH,
TRACE_KEY_V4_EVIDENCE_CONSUMED,
TRACE_KEY_V4_RANK_USED,
plan_placement,
)
PIPELINE_PATH = (
Path(__file__).resolve().parent.parent.parent
/ "src"
/ "phase_z2_pipeline.py"
)
# ─── Duck-typed CompositionUnit V4 candidate (composition.py:678-684) ─
@dataclass
class _V4Cand:
template_id: str
frame_id: str = ""
frame_number: int = 0
confidence: float = 0.0
label: str = "use_as_is"
def _text_obj() -> ContentObject:
return ContentObject(
id="u5.text-1",
type="text_block",
role="summary",
raw_payload="* bullet",
size_estimate={"line_count": 3},
type_specific={
"format": "bullet_list",
"bullet_count": 1,
"max_indent_level": 0,
"has_emphasis": False,
},
)
def _contract(template_id: str, frame_id: str, accepted: list[str]) -> dict:
return {
"template_id": template_id,
"frame_id": frame_id,
"accepted_content_types": accepted,
"sub_zones": [],
}
def _replay_trace_assembly_with_short_circuit(
plan, mapper_frame_template_id: str
) -> dict:
"""Replay the exact ``placement_trace`` assembly + the IMP-95 u5
``v4_short_circuit`` derivation from ``src/phase_z2_pipeline.py``
(Step 11). Binding mirror of the code under test — any drift in the
short-circuit formula MUST be reflected here so the test fails loudly.
"""
matches_mapper = plan.selected_template_id == mapper_frame_template_id
match_note = None
if not matches_mapper:
if plan.selected_template_id is None:
match_note = "no_frame_covers_content_types"
else:
match_note = (
f"B4 selected '{plan.selected_template_id}' but "
f"mapper uses '{mapper_frame_template_id}' (composition V4 rank-1)"
)
selection_trace = getattr(plan, "selection_trace", None) or {}
placement_trace = {
**asdict(plan),
"mapper_frame_template_id": mapper_frame_template_id,
"frame_selection_matches_mapper": matches_mapper,
"frame_selection_match_note": match_note,
TRACE_KEY_FRAME_SELECTION_BASIS: selection_trace.get(
TRACE_KEY_FRAME_SELECTION_BASIS
),
TRACE_KEY_V4_EVIDENCE_CONSUMED: selection_trace.get(
TRACE_KEY_V4_EVIDENCE_CONSUMED, False
),
TRACE_KEY_V4_RANK_USED: selection_trace.get(TRACE_KEY_V4_RANK_USED),
TRACE_KEY_V4_B4_FRAME_MATCH: selection_trace.get(
TRACE_KEY_V4_B4_FRAME_MATCH, False
),
TRACE_KEY_B4_V0_FALLBACK_REASON: selection_trace.get(
TRACE_KEY_B4_V0_FALLBACK_REASON
),
}
# IMP-95 u5 — derived field replay (binding mirror).
placement_trace["v4_short_circuit"] = bool(
placement_trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] and matches_mapper
)
return placement_trace, matches_mapper
# ─── Structural — pipeline.py wiring assertions ───────────────────
def test_pipeline_emits_v4_short_circuit_field() -> None:
"""``src/phase_z2_pipeline.py`` Step 11 must add ``v4_short_circuit``
to the assembled ``placement_trace`` (Stage 2 A7)."""
source = PIPELINE_PATH.read_text(encoding="utf-8")
assert 'placement_trace["v4_short_circuit"]' in source, (
"u5 wiring missing — placement_trace must expose v4_short_circuit "
"(derived from TRACE_KEY_V4_EVIDENCE_CONSUMED + matches_mapper)."
)
def test_pipeline_v4_short_circuit_formula_is_consumed_and_matches() -> None:
"""The derivation must combine ``TRACE_KEY_V4_EVIDENCE_CONSUMED`` with
``matches_mapper`` (Stage 2 A7). A pure structural assertion guards
against drift to a looser formula (e.g. only checking the env flag,
which would mis-record fallback paths as ``v4_short_circuit=True``)."""
source = PIPELINE_PATH.read_text(encoding="utf-8")
# Match the multi-line ``bool(... and ...)`` block tolerant of whitespace.
pattern = re.compile(
r"v4_short_circuit\s*=\s*bool\(\s*"
r"placement_trace\[TRACE_KEY_V4_EVIDENCE_CONSUMED\]\s*"
r"and\s+matches_mapper\s*\)",
re.DOTALL,
)
assert pattern.search(source), (
"u5 derivation must be exactly "
"bool(placement_trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] and matches_mapper) — "
"any other formula (env flag alone, v4_b4_frame_match alone, etc.) "
"breaks the Stage 2 A7 contract."
)
def test_pipeline_gatekeeper_trigger_remains_flag_independent() -> None:
"""The ``PHASE_Z_B4_GATEKEEPER`` branch must continue to trigger purely
on ``not matches_mapper`` (Stage 2 A8 — mapper/gatekeeper flag
independence). u5 must NOT add ``v4_short_circuit`` or
``PHASE_Z_B4_V4_EVIDENCE`` to the trigger condition."""
source = PIPELINE_PATH.read_text(encoding="utf-8")
# Locate the gatekeeper trigger `if (...)` block and extract its body
# up to the closing `):` so we can assert what is / is not inside.
gate_open = source.index('os.environ.get("PHASE_Z_B4_GATEKEEPER"')
# Walk back to the opening `if (` of this conditional.
if_idx = source.rfind("if (", 0, gate_open)
assert if_idx != -1, "could not locate gatekeeper `if (` opener"
# Walk forward from `if (` to balance parentheses up to the matching `)`.
depth = 0
i = if_idx + len("if ")
assert source[i] == "(", "gatekeeper opener malformed"
start = i
while i < len(source):
c = source[i]
if c == "(":
depth += 1
elif c == ")":
depth -= 1
if depth == 0:
break
i += 1
trigger_body = source[start: i + 1]
assert "PHASE_Z_B4_GATEKEEPER" in trigger_body
assert "not matches_mapper" in trigger_body
assert "PHASE_Z_B4_V4_EVIDENCE" not in trigger_body, (
"u5 must not couple gatekeeper trigger to PHASE_Z_B4_V4_EVIDENCE — "
"flag independence (Stage 2 A8) requires the two flags to be "
"evaluable independently."
)
assert "v4_short_circuit" not in trigger_body, (
"u5 must not couple gatekeeper trigger to v4_short_circuit — "
"the short-circuit is telemetry only; gatekeeper trigger remains "
"purely structural (not matches_mapper)."
)
def test_pipeline_adapter_record_includes_v4_short_circuit() -> None:
"""When the gatekeeper triggers (mismatch path), the appended
``adapter_record`` must carry ``v4_short_circuit`` so downstream
consumers can read the V4-path outcome on the rejected branch."""
source = PIPELINE_PATH.read_text(encoding="utf-8")
# Find the adapter_record dict literal and assert it contains the field.
adapter_open = source.index("adapter_record = {")
# Balance braces to capture the dict body.
depth = 0
i = adapter_open + len("adapter_record = ")
assert source[i] == "{"
start = i
while i < len(source):
c = source[i]
if c == "{":
depth += 1
elif c == "}":
depth -= 1
if depth == 0:
break
i += 1
adapter_body = source[start: i + 1]
assert '"v4_short_circuit": v4_short_circuit' in adapter_body, (
"u5 wiring missing — adapter_record must include "
'"v4_short_circuit": v4_short_circuit so the rejected branch '
"carries the same V4-path telemetry as the accepted branch."
)
# ─── Behavioral — derivation replay via plan_placement ────────────
def test_flag_off_v4_short_circuit_is_false_on_match(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Flag OFF: even when ``v4_candidates`` is supplied AND declaration
order happens to match the mapper, ``v4_short_circuit=False`` because
V4 evidence was not consumed (Stage 2 A1 + A10 — flag-OFF parity)."""
monkeypatch.delenv(PHASE_Z_B4_V4_EVIDENCE_ENV, raising=False)
contracts = [_contract("F_DECL_FIRST", "1", ["text_block"])]
plan = plan_placement(
[_text_obj()],
contracts,
section_id="u5_off_match",
v4_candidates=[_V4Cand("F_DECL_FIRST")],
)
trace, matches_mapper = _replay_trace_assembly_with_short_circuit(
plan, mapper_frame_template_id="F_DECL_FIRST"
)
assert matches_mapper is True
assert trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is False
assert trace["v4_short_circuit"] is False
def test_flag_off_v4_short_circuit_is_false_on_mismatch(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Flag OFF + declaration-order mismatch: ``v4_short_circuit=False``."""
monkeypatch.delenv(PHASE_Z_B4_V4_EVIDENCE_ENV, raising=False)
contracts = [
_contract("F_DECL_FIRST", "1", ["text_block"]),
_contract("F_RANK1", "2", ["text_block"]),
]
plan = plan_placement(
[_text_obj()],
contracts,
section_id="u5_off_mismatch",
v4_candidates=[_V4Cand("F_RANK1")],
)
trace, matches_mapper = _replay_trace_assembly_with_short_circuit(
plan, mapper_frame_template_id="F_RANK1"
)
assert matches_mapper is False
assert trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is False
assert trace["v4_short_circuit"] is False
def test_flag_on_v4_short_circuit_true_when_v4_rank1_matches_mapper(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Flag ON + V4 rank-1 eligible and matches mapper: ``v4_short_circuit=True``
(Stage 2 A7 happy path — V4 directly drove the mapper-matching selection)."""
monkeypatch.setenv(PHASE_Z_B4_V4_EVIDENCE_ENV, "1")
contracts = [
_contract("F_DECL_FIRST", "1", ["text_block"]),
_contract("F_RANK1", "2", ["text_block"]),
]
plan = plan_placement(
[_text_obj()],
contracts,
section_id="u5_on_match",
v4_candidates=[_V4Cand("F_RANK1")],
)
trace, matches_mapper = _replay_trace_assembly_with_short_circuit(
plan, mapper_frame_template_id="F_RANK1"
)
assert matches_mapper is True
assert trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is True
assert (
trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_V4_RANKED
)
assert trace["v4_short_circuit"] is True
def test_flag_on_v4_short_circuit_false_when_v4_picks_non_mapper_contract(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Flag ON + V4 evidence consumed but the V4-selected contract differs
from the mapper's frame_template_id: ``v4_short_circuit=False``. This is
the "V4 disagreed with mapper" path — the gatekeeper would still trigger
on ``not matches_mapper`` regardless of V4 consumption (Stage 2 A8
flag-independence)."""
monkeypatch.setenv(PHASE_Z_B4_V4_EVIDENCE_ENV, "1")
contracts = [
_contract("F_MAPPER", "1", ["text_block"]),
_contract("F_V4_PICK", "2", ["text_block"]),
]
plan = plan_placement(
[_text_obj()],
contracts,
section_id="u5_on_deliberate_mismatch",
v4_candidates=[_V4Cand("F_V4_PICK")],
)
trace, matches_mapper = _replay_trace_assembly_with_short_circuit(
plan, mapper_frame_template_id="F_MAPPER"
)
assert matches_mapper is False
assert trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is True
assert (
trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_V4_RANKED
)
assert trace["v4_short_circuit"] is False
def test_flag_on_v4_short_circuit_false_when_evidence_empty_even_if_decl_order_matches(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Flag ON but ``v4_candidates`` empty/None: planner falls back to
declaration order. Even when that fallback happens to match the mapper,
``v4_short_circuit=False`` because V4 evidence was NOT consumed
(``v4_evidence_consumed=False`` under the ``v4_evidence_empty`` fallback
reason). Distinguishes "V4 affirmatively chose" from "declaration order
coincidence" — the whole point of the short-circuit telemetry."""
monkeypatch.setenv(PHASE_Z_B4_V4_EVIDENCE_ENV, "1")
contracts = [_contract("F_DECL_FIRST", "1", ["text_block"])]
plan = plan_placement(
[_text_obj()],
contracts,
section_id="u5_on_empty_evidence",
v4_candidates=None,
)
trace, matches_mapper = _replay_trace_assembly_with_short_circuit(
plan, mapper_frame_template_id="F_DECL_FIRST"
)
assert matches_mapper is True
assert trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is False
assert trace[TRACE_KEY_B4_V0_FALLBACK_REASON] == "v4_evidence_empty"
assert (
trace[TRACE_KEY_FRAME_SELECTION_BASIS]
== FRAME_SELECTION_BASIS_DECLARATION_ORDER
)
assert trace["v4_short_circuit"] is False
def test_flag_on_v4_short_circuit_false_when_no_rank_eligible(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Flag ON + V4 candidates supplied but none resolve to an eligible
contract: planner records ``no_v4_rank_eligible`` and falls back to
declaration order. ``v4_evidence_consumed=False`` → ``v4_short_circuit=False``
even when the declaration-order fallback matches the mapper."""
monkeypatch.setenv(PHASE_Z_B4_V4_EVIDENCE_ENV, "1")
contracts = [_contract("F_DECL_FIRST", "1", ["text_block"])]
plan = plan_placement(
[_text_obj()],
contracts,
section_id="u5_on_no_rank_eligible",
v4_candidates=[_V4Cand("F_UNKNOWN_TEMPLATE")],
)
trace, matches_mapper = _replay_trace_assembly_with_short_circuit(
plan, mapper_frame_template_id="F_DECL_FIRST"
)
assert matches_mapper is True
assert trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is False
assert trace[TRACE_KEY_B4_V0_FALLBACK_REASON] == "no_v4_rank_eligible"
assert trace["v4_short_circuit"] is False
def test_v4_short_circuit_is_boolean_type_always(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The derived field must be ``bool`` (not ``None``, not a truthy
non-bool like ``1``). Guards against ``and``-chain returning the LHS /
RHS object instead of a normalized boolean — important for downstream
consumers (u10 trace-field type regression)."""
monkeypatch.setenv(PHASE_Z_B4_V4_EVIDENCE_ENV, "1")
contracts = [_contract("F_RANK1", "1", ["text_block"])]
plan = plan_placement(
[_text_obj()],
contracts,
section_id="u5_type_check",
v4_candidates=[_V4Cand("F_RANK1")],
)
trace, _ = _replay_trace_assembly_with_short_circuit(
plan, mapper_frame_template_id="F_RANK1"
)
assert isinstance(trace["v4_short_circuit"], bool)
assert trace["v4_short_circuit"] is True
@@ -0,0 +1,362 @@
"""IMP-95 u6 — partial_exists precheck regression.
Covers the V4-aware selector's contract-only / no-partial precheck:
* ``_select_frame_v4_aware(..., partial_exists=...)`` skips V4 ranks whose
resolved ``template_id`` returns False from the callable, recording the
skip in ``b4_partial_missing_skip`` and continuing to the next rank.
* ``partial_exists=None`` (default) preserves pre-u6 selector behavior —
no precheck performed, ``b4_partial_missing_skip`` stays empty.
* Declaration-order legacy fallback (``_select_frame``) is NEVER consulted
for partial existence — SHA parity guarantee under flag OFF.
* ``_declaration_order_selection_trace()`` carries the key with an empty
list so trace-field regression (u10) can read presence on every path.
* ``plan_placement(..., partial_exists=...)`` forwards the callable when
``PHASE_Z_B4_V4_EVIDENCE`` is ON; flag OFF ignores the kwarg verbatim.
* Pipeline-side ``_b4_partial_exists`` helper mirrors the partial filename
convention (``templates/phase_z2/families/{template_id}.html``) so the
V4 precheck and ``_load_frame_partial_html`` cannot drift.
These are unit tests; multi-mdx fixture regression lives in u8/u9/u10.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
SRC_DIR = REPO_ROOT / "src"
if str(SRC_DIR) not in sys.path:
sys.path.insert(0, str(SRC_DIR))
from phase_z2_content_extractor import ContentObject # noqa: E402
from phase_z2_placement_planner import ( # noqa: E402
FRAME_SELECTION_BASIS_DECLARATION_ORDER,
FRAME_SELECTION_BASIS_V4_RANKED,
PHASE_Z_B4_V4_EVIDENCE_ENV,
TRACE_KEY_B4_PARTIAL_MISSING_SKIP,
TRACE_KEY_B4_V0_FALLBACK_REASON,
TRACE_KEY_FRAME_SELECTION_BASIS,
TRACE_KEY_V4_B4_FRAME_MATCH,
TRACE_KEY_V4_EVIDENCE_CONSUMED,
TRACE_KEY_V4_RANK_USED,
_declaration_order_selection_trace,
_select_frame_v4_aware,
plan_placement,
)
# ─── Test fixtures ──────────────────────────────────────────────
class _V4Cand:
"""Duck-typed V4 candidate mirror (composition.py:678-684 contract)."""
__slots__ = ("template_id", "frame_id")
def __init__(self, template_id=None, frame_id=None):
self.template_id = template_id
self.frame_id = frame_id
def _text_obj(obj_id: str = "t1") -> ContentObject:
return ContentObject(
id=obj_id,
type="text_block",
role="summary",
raw_payload="* body",
size_estimate={"line_count": 1},
type_specific={"format": "bullet_list", "bullet_count": 1},
)
def _contract(template_id: str, frame_id: str = None, accepts=("text_block",)) -> dict:
return {
"template_id": template_id,
"frame_id": frame_id or template_id,
"accepted_content_types": list(accepts),
"sub_zones": [
{
"id": "slot_1",
"accepts": list(accepts),
"partial_target_path": "[data-slot='slot_1']",
"cardinality": {"strict": 1},
}
],
}
@pytest.fixture(autouse=True)
def _scrub_env(monkeypatch):
"""Force PHASE_Z_B4_V4_EVIDENCE OFF unless a test sets it explicitly."""
monkeypatch.delenv(PHASE_Z_B4_V4_EVIDENCE_ENV, raising=False)
yield
# ─── _declaration_order_selection_trace contract ────────────────
def test_declaration_order_trace_contains_partial_missing_skip_key():
"""Default trace MUST carry ``b4_partial_missing_skip`` with empty list.
u10 trace-field regression reads the top-level hoist on every code path;
the declaration-order default has to expose the key so a flag-OFF run
does not produce ``KeyError`` downstream.
"""
trace = _declaration_order_selection_trace()
assert TRACE_KEY_B4_PARTIAL_MISSING_SKIP in trace
assert trace[TRACE_KEY_B4_PARTIAL_MISSING_SKIP] == []
assert isinstance(trace[TRACE_KEY_B4_PARTIAL_MISSING_SKIP], list)
# ─── _select_frame_v4_aware precheck behavior ───────────────────
def test_selector_partial_exists_none_preserves_pre_u6_behavior():
"""When ``partial_exists`` is omitted/None, no precheck runs and rank-1 wins."""
contracts = [_contract("RANK1"), _contract("RANK2")]
candidates = [_V4Cand(template_id="RANK1"), _V4Cand(template_id="RANK2")]
matched, trace = _select_frame_v4_aware(
[_text_obj()], contracts, v4_candidates=candidates,
)
assert matched is contracts[0]
assert trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_V4_RANKED
assert trace[TRACE_KEY_V4_RANK_USED] == 0
assert trace[TRACE_KEY_B4_PARTIAL_MISSING_SKIP] == []
def test_selector_skips_rank1_when_partial_missing_falls_through_to_rank2():
"""Rank-1 with no partial → skip + record; rank-2 with partial → win."""
contracts = [_contract("CONTRACT_ONLY"), _contract("HAS_PARTIAL")]
candidates = [
_V4Cand(template_id="CONTRACT_ONLY"),
_V4Cand(template_id="HAS_PARTIAL"),
]
def partial_exists(tid: str) -> bool:
return tid == "HAS_PARTIAL"
matched, trace = _select_frame_v4_aware(
[_text_obj()], contracts,
v4_candidates=candidates,
partial_exists=partial_exists,
)
assert matched is contracts[1]
assert trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_V4_RANKED
assert trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is True
assert trace[TRACE_KEY_V4_RANK_USED] == 1
assert trace[TRACE_KEY_V4_B4_FRAME_MATCH] is True
assert trace[TRACE_KEY_B4_V0_FALLBACK_REASON] is None
# The skipped rank-1 must be recorded in order.
assert trace[TRACE_KEY_B4_PARTIAL_MISSING_SKIP] == [
{"rank": 0, "template_id": "CONTRACT_ONLY"},
]
def test_selector_all_ranks_missing_partial_falls_back_to_declaration_order():
"""When every V4 rank lacks a partial, fallback to declaration order.
The fallback reason must be ``no_v4_rank_eligible`` (ranks were tried but
none satisfied the precheck) and every skipped rank must be traced.
"""
contracts = [_contract("DECL_WINNER"), _contract("RANK1"), _contract("RANK2")]
candidates = [_V4Cand(template_id="RANK1"), _V4Cand(template_id="RANK2")]
def partial_exists(tid: str) -> bool:
return tid not in {"RANK1", "RANK2"}
matched, trace = _select_frame_v4_aware(
[_text_obj()], contracts,
v4_candidates=candidates,
partial_exists=partial_exists,
)
assert matched is contracts[0] # declaration order fallback
assert trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_DECLARATION_ORDER
assert trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is False
assert trace[TRACE_KEY_V4_RANK_USED] is None
assert trace[TRACE_KEY_V4_B4_FRAME_MATCH] is False
assert trace[TRACE_KEY_B4_V0_FALLBACK_REASON] == "no_v4_rank_eligible"
assert trace[TRACE_KEY_B4_PARTIAL_MISSING_SKIP] == [
{"rank": 0, "template_id": "RANK1"},
{"rank": 1, "template_id": "RANK2"},
]
def test_selector_partial_exists_not_called_for_unmatched_ranks():
"""Ranks that fail template_id/frame_id lookup MUST NOT consult partial_exists.
The precheck applies only after a contract is matched; otherwise we would
record a ``b4_partial_missing_skip`` for an unmatched rank, which is a
different failure mode.
"""
contracts = [_contract("ONLY_CONTRACT")]
candidates = [
_V4Cand(template_id="NOT_IN_CATALOG"), # unmatched — no precheck
_V4Cand(template_id="ONLY_CONTRACT"),
]
consulted: list[str] = []
def partial_exists(tid: str) -> bool:
consulted.append(tid)
return True
matched, trace = _select_frame_v4_aware(
[_text_obj()], contracts,
v4_candidates=candidates,
partial_exists=partial_exists,
)
assert matched is contracts[0]
assert trace[TRACE_KEY_V4_RANK_USED] == 1
# Only the matched candidate consulted partial_exists; unmatched is skipped earlier.
assert consulted == ["ONLY_CONTRACT"]
assert trace[TRACE_KEY_B4_PARTIAL_MISSING_SKIP] == []
def test_selector_partial_exists_runs_before_accepted_content_types_check():
"""Precheck order — partial_exists BEFORE accepted_content_types ⊇ check.
Stage 2 A9 + u6 docstring contract: a contract-only template must be
recorded as ``b4_partial_missing_skip`` even when the ⊇ check would also
have failed. The ordering keeps the skip reason unambiguous.
"""
# Contract present but does NOT cover the content type — would normally
# be ineligible. partial_exists=False means we record partial_missing
# before reaching the ⊇ check.
contracts = [_contract("MISSING_PARTIAL", accepts=("image",))]
candidates = [_V4Cand(template_id="MISSING_PARTIAL")]
seen_acceptable_check = []
def partial_exists(tid: str) -> bool:
seen_acceptable_check.append(tid)
return False
matched, trace = _select_frame_v4_aware(
[_text_obj()], contracts,
v4_candidates=candidates,
partial_exists=partial_exists,
)
assert matched is None # no declaration-order winner either
assert seen_acceptable_check == ["MISSING_PARTIAL"]
assert trace[TRACE_KEY_B4_PARTIAL_MISSING_SKIP] == [
{"rank": 0, "template_id": "MISSING_PARTIAL"},
]
assert trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_DECLARATION_ORDER
assert trace[TRACE_KEY_B4_V0_FALLBACK_REASON] == "no_v4_rank_eligible"
def test_selector_empty_evidence_keeps_partial_missing_skip_empty():
"""No V4 evidence → precheck is never reached → empty skip list.
Fallback reason is the empty-evidence variant, distinct from
``no_v4_rank_eligible``.
"""
contracts = [_contract("DECL_WINNER")]
matched, trace = _select_frame_v4_aware(
[_text_obj()], contracts,
v4_candidates=None,
partial_exists=lambda tid: True,
)
assert matched is contracts[0]
assert trace[TRACE_KEY_B4_V0_FALLBACK_REASON] == "v4_evidence_empty"
assert trace[TRACE_KEY_B4_PARTIAL_MISSING_SKIP] == []
# ─── plan_placement wiring ──────────────────────────────────────
def test_plan_placement_signature_accepts_partial_exists_kwarg():
import inspect
sig = inspect.signature(plan_placement)
assert "partial_exists" in sig.parameters
param = sig.parameters["partial_exists"]
assert param.default is None
# kwarg-style, not positional-only.
assert param.kind in (
inspect.Parameter.POSITIONAL_OR_KEYWORD,
inspect.Parameter.KEYWORD_ONLY,
)
def test_plan_placement_flag_off_ignores_partial_exists(monkeypatch):
"""Flag OFF (default) → selector never consulted, partial_exists ignored.
Stage 2 A10 SHA parity precondition.
"""
monkeypatch.delenv(PHASE_Z_B4_V4_EVIDENCE_ENV, raising=False)
contracts = [_contract("DECL_FIRST"), _contract("DECL_SECOND")]
called: list[str] = []
def partial_exists(tid: str) -> bool:
called.append(tid)
return False # would-be skip everything if consulted
plan = plan_placement(
[_text_obj()], contracts, section_id="zone-1",
v4_candidates=[_V4Cand(template_id="DECL_SECOND")],
partial_exists=partial_exists,
)
# Declaration-order winner unchanged; partial_exists never called.
assert plan.selected_template_id == "DECL_FIRST"
assert called == []
# Trace carries the declaration-order default with empty skip list.
assert plan.selection_trace[TRACE_KEY_B4_PARTIAL_MISSING_SKIP] == []
def test_plan_placement_flag_on_forwards_partial_exists(monkeypatch):
"""Flag ON → plan_placement forwards partial_exists to the V4-aware selector."""
monkeypatch.setenv(PHASE_Z_B4_V4_EVIDENCE_ENV, "1")
contracts = [_contract("RANK1"), _contract("RANK2")]
candidates = [_V4Cand(template_id="RANK1"), _V4Cand(template_id="RANK2")]
def partial_exists(tid: str) -> bool:
return tid == "RANK2"
plan = plan_placement(
[_text_obj()], contracts, section_id="zone-1",
v4_candidates=candidates,
partial_exists=partial_exists,
)
assert plan.selected_template_id == "RANK2"
assert plan.selection_trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_V4_RANKED
assert plan.selection_trace[TRACE_KEY_V4_RANK_USED] == 1
assert plan.selection_trace[TRACE_KEY_B4_PARTIAL_MISSING_SKIP] == [
{"rank": 0, "template_id": "RANK1"},
]
# ─── pipeline helper contract ───────────────────────────────────
def test_pipeline_b4_partial_exists_uses_families_path(tmp_path, monkeypatch):
"""``_b4_partial_exists`` checks ``templates/phase_z2/families/{id}.html``.
Same convention as ``_load_frame_partial_html`` — drift would break the
V4 precheck and the AI fallback loader independently.
"""
import phase_z2_pipeline as pipeline_mod
fake_template_dir = tmp_path / "templates" / "phase_z2"
families_dir = fake_template_dir / "families"
families_dir.mkdir(parents=True)
(families_dir / "PRESENT.html").write_text("<div/>", encoding="utf-8")
monkeypatch.setattr(pipeline_mod, "TEMPLATE_DIR", fake_template_dir)
assert pipeline_mod._b4_partial_exists("PRESENT") is True
assert pipeline_mod._b4_partial_exists("ABSENT") is False
# Defensive: empty / falsy template_id never resolves to True.
assert pipeline_mod._b4_partial_exists("") is False
@@ -0,0 +1,330 @@
"""IMP-95 u4 — Step 11 ``unit.v4_candidates`` wiring + placement_trace
top-level exposure.
Stage 2 plan (u4 / axes A4 + A5 + A6): the Step 11 renderable-zones loop at
``src/phase_z2_pipeline.py`` must (1) forward ``unit.v4_candidates`` into
``plan_placement`` as the new ``v4_candidates=`` kwarg added by u3 and
(2) hoist the u1 ``TRACE_KEY_*`` keys from ``PlacementPlan.selection_trace``
to the top level of ``placement_trace`` so downstream consumers
(u5 gatekeeper short-circuit, u10 trace-field regression) can read them
without traversing the nested ``selection_trace`` dict.
Coverage axes :
* Wiring contract — the call site uses ``v4_candidates=`` from the unit.
* Hoist contract — ``placement_trace`` carries the five u1 trace keys at
the top level, in addition to the nested ``selection_trace`` produced
by ``asdict(placement_plan)``.
* Flag-OFF parity — top-level keys exist with declaration-order defaults
(Stage 2 A1 + A10).
* Flag-ON V4-ranked — top-level keys reflect the V4-aware selector path
(basis = v4_ranked, rank_used = 0, evidence_consumed = True,
frame_match = True, fallback_reason = None).
* Flag-ON empty evidence fallback — top-level fallback_reason =
'v4_evidence_empty'.
* ``unit.v4_candidates = []`` (default-factory empty list) is coerced to
``None`` so the planner treats it as no-evidence (Stage 2 A4 contract
mirrored by u3 tests; explicit-empty is semantically "no evidence").
* Defensive ``getattr`` on a non-CompositionUnit shape keeps Step 11 alive.
Out of scope for u4 (Stage 2 scope-lock) :
* Gatekeeper short-circuit on V4=mapper match (u5).
* Partial-exists precheck (u6).
* Full-pipeline SHA / adapter_needed regressions (u8 / u9 / u10).
"""
from __future__ import annotations
import re
from dataclasses import asdict, dataclass
from pathlib import Path
import pytest
from src.phase_z2_content_extractor import ContentObject
from src.phase_z2_placement_planner import (
FRAME_SELECTION_BASIS_DECLARATION_ORDER,
FRAME_SELECTION_BASIS_V4_RANKED,
PHASE_Z_B4_V4_EVIDENCE_ENV,
TRACE_KEY_B4_V0_FALLBACK_REASON,
TRACE_KEY_FRAME_SELECTION_BASIS,
TRACE_KEY_V4_B4_FRAME_MATCH,
TRACE_KEY_V4_EVIDENCE_CONSUMED,
TRACE_KEY_V4_RANK_USED,
plan_placement,
)
PIPELINE_PATH = (
Path(__file__).resolve().parent.parent.parent
/ "src"
/ "phase_z2_pipeline.py"
)
# ─── Duck-typed CompositionUnit for evidence wiring (u4 / A4) ─────
@dataclass
class _V4Cand:
"""Duck-typed V4Match mirror (composition.py:678-684)."""
template_id: str
frame_id: str = ""
frame_number: int = 0
confidence: float = 0.0
label: str = "use_as_is"
def _text_obj() -> ContentObject:
return ContentObject(
id="u4.text-1",
type="text_block",
role="summary",
raw_payload="* bullet",
size_estimate={"line_count": 3},
type_specific={
"format": "bullet_list",
"bullet_count": 1,
"max_indent_level": 0,
"has_emphasis": False,
},
)
def _contract(template_id: str, frame_id: str, accepted: list[str]) -> dict:
return {
"template_id": template_id,
"frame_id": frame_id,
"accepted_content_types": accepted,
"sub_zones": [],
}
def _replay_trace_assembly(plan, mapper_frame_template_id: str) -> dict:
"""Replay the exact ``placement_trace`` assembly from
``src/phase_z2_pipeline.py`` (Step 11). This is a binding mirror of the
code under test — the hoist contract (u4) lives at that call site.
Any drift in field names or default semantics MUST be reflected here so
the test fails loudly. The body intentionally mirrors the production
assembly verbatim (including the ``frame_selection_match_note`` string
template) so a future edit to that block requires this mirror to
update — which is the unit's binding contract.
"""
matches_mapper = plan.selected_template_id == mapper_frame_template_id
match_note = None
if not matches_mapper:
if plan.selected_template_id is None:
match_note = "no_frame_covers_content_types"
else:
match_note = (
f"B4 selected '{plan.selected_template_id}' but "
f"mapper uses '{mapper_frame_template_id}' (composition V4 rank-1)"
)
selection_trace = getattr(plan, "selection_trace", None) or {}
return {
**asdict(plan),
"mapper_frame_template_id": mapper_frame_template_id,
"frame_selection_matches_mapper": matches_mapper,
"frame_selection_match_note": match_note,
TRACE_KEY_FRAME_SELECTION_BASIS: selection_trace.get(
TRACE_KEY_FRAME_SELECTION_BASIS
),
TRACE_KEY_V4_EVIDENCE_CONSUMED: selection_trace.get(
TRACE_KEY_V4_EVIDENCE_CONSUMED, False
),
TRACE_KEY_V4_RANK_USED: selection_trace.get(TRACE_KEY_V4_RANK_USED),
TRACE_KEY_V4_B4_FRAME_MATCH: selection_trace.get(
TRACE_KEY_V4_B4_FRAME_MATCH, False
),
TRACE_KEY_B4_V0_FALLBACK_REASON: selection_trace.get(
TRACE_KEY_B4_V0_FALLBACK_REASON
),
}
# ─── Structural — pipeline.py wiring assertion ────────────────────
def test_pipeline_imports_u1_trace_key_constants() -> None:
"""``src/phase_z2_pipeline.py`` imports the u1 ``TRACE_KEY_*`` constants
from the placement planner so the Step 11 trace assembly cannot inline
or drift the key names (Stage 2 A5)."""
source = PIPELINE_PATH.read_text(encoding="utf-8")
for name in (
"TRACE_KEY_FRAME_SELECTION_BASIS",
"TRACE_KEY_V4_EVIDENCE_CONSUMED",
"TRACE_KEY_V4_RANK_USED",
"TRACE_KEY_V4_B4_FRAME_MATCH",
"TRACE_KEY_B4_V0_FALLBACK_REASON",
):
assert name in source, (
f"u4 wiring missing — pipeline.py must import {name} so the Step 11 "
f"placement_trace assembly uses the u1 constant (avoids string drift)."
)
def test_pipeline_step11_passes_v4_candidates_kwarg() -> None:
"""The Step 11 ``plan_placement(...)`` call site forwards
``v4_candidates=`` using ``unit.v4_candidates`` (Stage 2 A4)."""
source = PIPELINE_PATH.read_text(encoding="utf-8")
# Locate every ``plan_placement(`` opener and balance parentheses
# manually so nested calls (``load_frame_contracts()``) do not truncate
# the captured body.
found_kwarg = False
for opener in re.finditer(r"plan_placement\s*\(", source):
depth = 1
i = opener.end()
while i < len(source) and depth > 0:
c = source[i]
if c == "(":
depth += 1
elif c == ")":
depth -= 1
i += 1
body = source[opener.end(): i - 1]
if "v4_candidates=" in body:
found_kwarg = True
break
assert found_kwarg, (
"u4 wiring missing — Step 11 plan_placement(...) call must pass "
"v4_candidates= (the kwarg added by u3)."
)
assert (
"unit.v4_candidates" in source
or 'getattr(unit, "v4_candidates"' in source
), (
"u4 wiring missing — Step 11 must source v4_candidates from "
"CompositionUnit.v4_candidates (composition.py:678-684)."
)
# ─── Behavioral — trace hoist via assembly replay ─────────────────
def test_flag_off_trace_assembly_hoists_declaration_order_defaults(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Flag OFF: even when ``v4_candidates`` is supplied by the unit, the
hoisted top-level keys must carry declaration-order defaults (Stage 2
A1 + A10 — final.html SHA parity precondition for u8)."""
monkeypatch.delenv(PHASE_Z_B4_V4_EVIDENCE_ENV, raising=False)
contracts = [
_contract("F_DECL_FIRST", "1", ["text_block"]),
_contract("F_RANK1_WOULD_WIN_IF_ON", "2", ["text_block"]),
]
plan = plan_placement(
[_text_obj()],
contracts,
section_id="u4_off",
v4_candidates=[_V4Cand("F_RANK1_WOULD_WIN_IF_ON")],
)
trace = _replay_trace_assembly(plan, mapper_frame_template_id="F_DECL_FIRST")
# u4 hoist contract — five top-level keys present with declaration-order defaults.
assert (
trace[TRACE_KEY_FRAME_SELECTION_BASIS]
== FRAME_SELECTION_BASIS_DECLARATION_ORDER
)
assert trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is False
assert trace[TRACE_KEY_V4_RANK_USED] is None
assert trace[TRACE_KEY_V4_B4_FRAME_MATCH] is False
assert trace[TRACE_KEY_B4_V0_FALLBACK_REASON] is None
# nested selection_trace from asdict(plan) survives — additive guarantee.
assert "selection_trace" in trace
assert (
trace["selection_trace"][TRACE_KEY_FRAME_SELECTION_BASIS]
== FRAME_SELECTION_BASIS_DECLARATION_ORDER
)
# mapper-comparison fields untouched.
assert trace["mapper_frame_template_id"] == "F_DECL_FIRST"
assert trace["frame_selection_matches_mapper"] is True
assert trace["frame_selection_match_note"] is None
def test_flag_on_trace_assembly_hoists_v4_ranked_evidence(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Flag ON + rank-1 candidate eligible: hoisted top-level keys reflect
the V4-aware selector path (Stage 2 A6 — v4_rank_used / v4_b4_frame_match
visibility)."""
monkeypatch.setenv(PHASE_Z_B4_V4_EVIDENCE_ENV, "1")
contracts = [
_contract("F_DECL_FIRST", "1", ["text_block"]),
_contract("F_RANK1", "2", ["text_block"]),
]
plan = plan_placement(
[_text_obj()],
contracts,
section_id="u4_on_rank1",
v4_candidates=[_V4Cand("F_RANK1")],
)
trace = _replay_trace_assembly(plan, mapper_frame_template_id="F_RANK1")
assert trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_V4_RANKED
assert trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is True
assert trace[TRACE_KEY_V4_RANK_USED] == 0
assert trace[TRACE_KEY_V4_B4_FRAME_MATCH] is True
assert trace[TRACE_KEY_B4_V0_FALLBACK_REASON] is None
# mapper now matches the V4-selected frame.
assert trace["frame_selection_matches_mapper"] is True
def test_flag_on_empty_evidence_hoists_v4_evidence_empty_reason(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Flag ON but ``unit.v4_candidates`` empty → planner falls back to
declaration order with ``b4_v0_fallback_reason='v4_evidence_empty'``.
Top-level hoist surfaces the reason without traversing ``selection_trace``."""
monkeypatch.setenv(PHASE_Z_B4_V4_EVIDENCE_ENV, "1")
contracts = [_contract("F_DECL_FIRST", "1", ["text_block"])]
plan = plan_placement(
[_text_obj()],
contracts,
section_id="u4_on_empty",
v4_candidates=None,
)
trace = _replay_trace_assembly(plan, mapper_frame_template_id="F_DECL_FIRST")
assert (
trace[TRACE_KEY_FRAME_SELECTION_BASIS]
== FRAME_SELECTION_BASIS_DECLARATION_ORDER
)
assert trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is False
assert trace[TRACE_KEY_V4_RANK_USED] is None
assert trace[TRACE_KEY_V4_B4_FRAME_MATCH] is False
assert trace[TRACE_KEY_B4_V0_FALLBACK_REASON] == "v4_evidence_empty"
def test_step11_empty_v4_candidates_list_coerced_to_none() -> None:
"""``unit.v4_candidates`` defaults to ``[]`` (CompositionUnit
default-factory). The Step 11 wiring coerces empty-list → ``None`` so
the planner emits ``v4_evidence_empty`` instead of
``no_v4_rank_eligible`` — these reasons are semantically distinct
(Stage 2 A5 enum)."""
# This mirrors the exact coercion at the call site :
# unit_v4_candidates = getattr(unit, "v4_candidates", None) or None
source = PIPELINE_PATH.read_text(encoding="utf-8")
assert 'getattr(unit, "v4_candidates", None) or None' in source, (
"u4 wiring must coerce empty / missing unit.v4_candidates to None "
"so the planner reports the v4_evidence_empty fallback reason."
)
def test_step11_non_composition_unit_shape_does_not_crash() -> None:
"""Defensive ``getattr(unit, 'v4_candidates', None)`` keeps Step 11
alive when ``unit`` lacks the attribute (e.g. legacy fixture shapes /
future unit subclasses). The unit-under-test is the coercion line at
the call site — verified by structural assertion in
:func:`test_step11_empty_v4_candidates_list_coerced_to_none`. Here
we assert the defensive pattern survives ``getattr`` semantics on a
bare ``object()``."""
class _BareUnit:
pass
bare = _BareUnit()
coerced = getattr(bare, "v4_candidates", None) or None
assert coerced is None
@@ -0,0 +1,271 @@
"""IMP-95 u3 — ``plan_placement`` V4 evidence wiring unit tests.
Stage 2 plan (u3 / axes A4, A5): ``plan_placement`` gains an optional
``v4_candidates`` kwarg and surfaces an additive ``selection_trace`` dict
on ``PlacementPlan``. Default OFF preserves declaration-order behavior
(Stage 2 A1); ON delegates to ``_select_frame_v4_aware`` (u2).
Coverage :
* Flag OFF: legacy declaration-order frame chosen; ``v4_candidates``
kwarg ignored even when supplied (Stage 2 A1 + A10 SHA parity).
* Flag OFF: ``selection_trace`` populated with declaration_order defaults.
* Flag ON + V4 rank-1 eligible: V4-ranked contract wins.
* Flag ON + empty V4 evidence: declaration-order fallback,
``b4_v0_fallback_reason = 'v4_evidence_empty'``.
* Flag ON + None V4 evidence: same fallback semantics.
* Empty ``content_objects`` short-circuit still emits ``selection_trace``.
* ``v4_candidates`` is keyword-only-by-convention (kwarg presence check).
Out of scope for u3 (per Stage 2 scope-lock) :
* Step 11 wiring of ``unit.v4_candidates`` (u4).
* Gatekeeper short-circuit (u5).
* Partial-exists precheck (u6).
* Pipeline regression / SHA parity (u8~u10).
Trace keys reuse the u1 ``TRACE_KEY_*`` constants so u4 cannot drift names.
"""
from __future__ import annotations
import inspect
from dataclasses import dataclass
import pytest
from src.phase_z2_content_extractor import ContentObject
from src.phase_z2_placement_planner import (
FRAME_SELECTION_BASIS_DECLARATION_ORDER,
FRAME_SELECTION_BASIS_V4_RANKED,
PHASE_Z_B4_V4_EVIDENCE_ENV,
TRACE_KEY_B4_V0_FALLBACK_REASON,
TRACE_KEY_FRAME_SELECTION_BASIS,
TRACE_KEY_V4_B4_FRAME_MATCH,
TRACE_KEY_V4_EVIDENCE_CONSUMED,
TRACE_KEY_V4_RANK_USED,
PlacementPlan,
plan_placement,
)
@dataclass
class _Cand:
"""Duck-typed V4Match (mirrors ``src/phase_z2_composition.py:678-684``)."""
template_id: str
frame_id: str = ""
frame_number: int = 0
confidence: float = 0.0
label: str = "use_as_is"
def _text_obj(oid: str = "u3.text-1") -> ContentObject:
return ContentObject(
id=oid,
type="text_block",
role="summary",
raw_payload="* bullet",
size_estimate={"line_count": 3},
type_specific={
"format": "bullet_list",
"bullet_count": 1,
"max_indent_level": 0,
"has_emphasis": False,
},
)
def _contract(template_id: str, frame_id: str, accepted: list[str]) -> dict:
"""Minimal frame_contract — empty ``sub_zones`` means Stage B picks no slot
(rejection), but selection_trace is still produced (the unit under test)."""
return {
"template_id": template_id,
"frame_id": frame_id,
"accepted_content_types": accepted,
"sub_zones": [],
}
# ─── Signature contract (Stage 2 A4) ──────────────────────────────
def test_plan_placement_accepts_v4_candidates_kwarg() -> None:
"""``plan_placement`` exposes ``v4_candidates`` as an optional kwarg
with default ``None``. Callers that omit it must keep working (legacy
Step 11 wiring path until u4 lands)."""
sig = inspect.signature(plan_placement)
assert "v4_candidates" in sig.parameters
param = sig.parameters["v4_candidates"]
assert param.default is None
# ─── Empty content_objects short-circuit ──────────────────────────
def test_empty_content_objects_returns_default_trace(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Empty ``content_objects`` → no Stage A/B, but ``selection_trace`` is
populated so u4 reading from the field never sees a missing key."""
monkeypatch.delenv(PHASE_Z_B4_V4_EVIDENCE_ENV, raising=False)
plan = plan_placement([], [_contract("F", "1", ["text_block"])], section_id="empty")
assert isinstance(plan, PlacementPlan)
assert plan.selected_frame_id is None
assert plan.selected_template_id is None
assert plan.internal_regions == []
assert plan.selection_trace[TRACE_KEY_FRAME_SELECTION_BASIS] == (
FRAME_SELECTION_BASIS_DECLARATION_ORDER
)
assert plan.selection_trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is False
assert plan.selection_trace[TRACE_KEY_V4_RANK_USED] is None
assert plan.selection_trace[TRACE_KEY_V4_B4_FRAME_MATCH] is False
assert plan.selection_trace[TRACE_KEY_B4_V0_FALLBACK_REASON] is None
# ─── Flag OFF — declaration-order parity (Stage 2 A1 + A10) ──────
def test_flag_off_ignores_v4_candidates_and_keeps_declaration_order(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Even when ``v4_candidates`` is supplied, flag OFF must keep the
legacy declaration-order frame selection. This is the load-bearing
SHA-parity guarantee for u8."""
monkeypatch.delenv(PHASE_Z_B4_V4_EVIDENCE_ENV, raising=False)
contracts = [
_contract("F_DECL_FIRST", "1", ["text_block"]),
_contract("F_RANK1_WOULD_WIN_IF_ON", "2", ["text_block"]),
]
plan = plan_placement(
[_text_obj()],
contracts,
section_id="off_with_evidence",
v4_candidates=[_Cand("F_RANK1_WOULD_WIN_IF_ON")],
)
assert plan.selected_template_id == "F_DECL_FIRST"
assert plan.selection_trace[TRACE_KEY_FRAME_SELECTION_BASIS] == (
FRAME_SELECTION_BASIS_DECLARATION_ORDER
)
assert plan.selection_trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is False
assert plan.selection_trace[TRACE_KEY_V4_RANK_USED] is None
assert plan.selection_trace[TRACE_KEY_V4_B4_FRAME_MATCH] is False
# Flag OFF never enters the V4 path → no fallback reason is recorded.
assert plan.selection_trace[TRACE_KEY_B4_V0_FALLBACK_REASON] is None
def test_flag_off_no_v4_kwarg_keeps_legacy_signature(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Existing callers (e.g. self-test, IMP-89/IMP-94) call without
``v4_candidates`` — confirm the default ``None`` path is wired to
declaration_order."""
monkeypatch.delenv(PHASE_Z_B4_V4_EVIDENCE_ENV, raising=False)
contracts = [_contract("F_FIRST", "1", ["text_block"])]
plan = plan_placement([_text_obj()], contracts, section_id="off_default")
assert plan.selected_template_id == "F_FIRST"
assert plan.selection_trace[TRACE_KEY_FRAME_SELECTION_BASIS] == (
FRAME_SELECTION_BASIS_DECLARATION_ORDER
)
# ─── Flag ON — V4 rank wins ───────────────────────────────────────
def test_flag_on_with_v4_rank1_match_consumes_evidence(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Flag ON + rank-1 candidate eligible → V4-ranked contract chosen."""
monkeypatch.setenv(PHASE_Z_B4_V4_EVIDENCE_ENV, "1")
contracts = [
_contract("F_DECL_FIRST", "1", ["text_block"]),
_contract("F_RANK1", "2", ["text_block"]),
]
plan = plan_placement(
[_text_obj()],
contracts,
section_id="on_rank1",
v4_candidates=[_Cand("F_RANK1")],
)
assert plan.selected_template_id == "F_RANK1"
assert plan.selection_trace[TRACE_KEY_FRAME_SELECTION_BASIS] == (
FRAME_SELECTION_BASIS_V4_RANKED
)
assert plan.selection_trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is True
assert plan.selection_trace[TRACE_KEY_V4_RANK_USED] == 0
assert plan.selection_trace[TRACE_KEY_V4_B4_FRAME_MATCH] is True
assert plan.selection_trace[TRACE_KEY_B4_V0_FALLBACK_REASON] is None
# ─── Flag ON — empty / None evidence falls back ───────────────────
@pytest.mark.parametrize("evidence", [None, []])
def test_flag_on_without_evidence_falls_back_to_declaration_order(
monkeypatch: pytest.MonkeyPatch, evidence: object
) -> None:
"""Flag ON but no V4 evidence → declaration-order winner; fallback
reason = ``v4_evidence_empty``. The frame outcome stays identical to
the OFF path, which is the load-bearing invariant for u9/u10 (trace-
only by default)."""
monkeypatch.setenv(PHASE_Z_B4_V4_EVIDENCE_ENV, "1")
contracts = [_contract("F_DECL_FIRST", "1", ["text_block"])]
plan = plan_placement(
[_text_obj()],
contracts,
section_id="on_empty_evidence",
v4_candidates=evidence, # type: ignore[arg-type]
)
assert plan.selected_template_id == "F_DECL_FIRST"
assert plan.selection_trace[TRACE_KEY_FRAME_SELECTION_BASIS] == (
FRAME_SELECTION_BASIS_DECLARATION_ORDER
)
assert plan.selection_trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is False
assert plan.selection_trace[TRACE_KEY_V4_RANK_USED] is None
assert plan.selection_trace[TRACE_KEY_V4_B4_FRAME_MATCH] is False
assert plan.selection_trace[TRACE_KEY_B4_V0_FALLBACK_REASON] == "v4_evidence_empty"
# ─── Flag ON — every rank ineligible falls back ───────────────────
def test_flag_on_all_ranks_ineligible_records_fallback_reason(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Flag ON + every V4 rank ineligible by accepted_content_types →
declaration-order winner; fallback reason = ``no_v4_rank_eligible``."""
monkeypatch.setenv(PHASE_Z_B4_V4_EVIDENCE_ENV, "1")
contracts = [
_contract("F_DECL_FIRST", "1", ["text_block"]),
_contract("F_RANK1", "2", ["transform_table"]),
]
plan = plan_placement(
[_text_obj()],
contracts,
section_id="on_no_eligible",
v4_candidates=[_Cand("F_RANK1")],
)
assert plan.selected_template_id == "F_DECL_FIRST"
assert plan.selection_trace[TRACE_KEY_FRAME_SELECTION_BASIS] == (
FRAME_SELECTION_BASIS_DECLARATION_ORDER
)
assert plan.selection_trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is False
assert plan.selection_trace[TRACE_KEY_B4_V0_FALLBACK_REASON] == "no_v4_rank_eligible"
# ─── No-frame-covers path still emits trace (rejection branch) ────
def test_no_frame_covers_records_trace_and_rejection(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""When no frame covers the content_types, ``selection_trace`` is
still populated (declaration-order default) so u4 can read it
uniformly on every code path."""
monkeypatch.delenv(PHASE_Z_B4_V4_EVIDENCE_ENV, raising=False)
contracts = [_contract("F_ONLY", "1", ["transform_table"])]
plan = plan_placement([_text_obj()], contracts, section_id="reject")
assert plan.selected_frame_id is None
assert plan.selected_template_id is None
assert any(r["reason"] == "no_frame_covers_content_types" for r in plan.rejection)
assert plan.selection_trace[TRACE_KEY_FRAME_SELECTION_BASIS] == (
FRAME_SELECTION_BASIS_DECLARATION_ORDER
)
assert plan.selection_trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is False
@@ -0,0 +1,477 @@
"""IMP-95 u2 — V4 evidence-aware ``_select_frame_v4_aware`` unit tests.
Stage 2 plan (u2 / axes A2, A3, A13): the V4-aware selector ranks
eligible frame_contracts under the existing
``accepted_content_types ⊇ content_type_set`` constraint, then falls
back to declaration order via ``_select_frame``.
Coverage (Stage 2 A13) :
* V4 rank-1 eligible → wins over declaration-order first contract.
* V4 rank-1 ineligible → fall-through to next eligible rank.
* All V4 ranks ineligible / unmatched → declaration-order fallback,
``b4_v0_fallback_reason = 'no_v4_rank_eligible'``.
* Empty / None V4 evidence → declaration-order fallback,
``b4_v0_fallback_reason = 'v4_evidence_empty'``.
* V4 candidate matches by ``frame_id`` when ``template_id`` empty.
* No contract covers content_types → returns ``None``.
* Legacy ``_select_frame`` signature/behavior preserved (Stage 2 A1).
Trace keys reuse the u1 constants so u3 (plan_placement wiring) cannot
silently drift the names.
"""
from __future__ import annotations
from dataclasses import dataclass
from src.phase_z2_content_extractor import ContentObject
from src.phase_z2_placement_planner import (
FRAME_SELECTION_BASIS_DECLARATION_ORDER,
FRAME_SELECTION_BASIS_V4_RANKED,
TRACE_KEY_B4_V0_FALLBACK_REASON,
TRACE_KEY_FRAME_SELECTION_BASIS,
TRACE_KEY_V4_B4_FRAME_MATCH,
TRACE_KEY_V4_EVIDENCE_CONSUMED,
TRACE_KEY_V4_RANK_USED,
_select_frame,
_select_frame_v4_aware,
)
@dataclass
class _Cand:
"""Duck-typed V4Match for tests.
Mirrors the contract documented at
``src/phase_z2_composition.py:678-684`` :
``template_id / frame_id / frame_number / confidence / label``.
"""
template_id: str
frame_id: str = ""
frame_number: int = 0
confidence: float = 0.0
label: str = "use_as_is"
def _text_obj(oid: str = "o1") -> ContentObject:
return ContentObject(
id=oid,
type="text_block",
role="summary",
raw_payload="* bullet",
size_estimate={"line_count": 3},
type_specific={
"format": "bullet_list",
"bullet_count": 1,
"max_indent_level": 0,
"has_emphasis": False,
},
)
def _contract(template_id: str, frame_id: str, accepted: list[str]) -> dict:
return {
"template_id": template_id,
"frame_id": frame_id,
"accepted_content_types": accepted,
"sub_zones": [],
}
# ─── Happy path: V4 rank-1 wins ───────────────────────────────────
def test_v4_rank1_match_returns_rank1_contract() -> None:
"""Eligible V4 rank-1 wins over declaration-order first contract."""
contracts = [
_contract("F_DECL_FIRST", "1", ["text_block"]),
_contract("F_RANK1", "2", ["text_block"]),
]
chosen, trace = _select_frame_v4_aware(
[_text_obj()], contracts, [_Cand("F_RANK1")],
)
assert chosen is not None
assert chosen["template_id"] == "F_RANK1"
assert trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_V4_RANKED
assert trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is True
assert trace[TRACE_KEY_V4_RANK_USED] == 0
assert trace[TRACE_KEY_V4_B4_FRAME_MATCH] is True
assert trace[TRACE_KEY_B4_V0_FALLBACK_REASON] is None
# ─── Rank fall-through ────────────────────────────────────────────
def test_rank2_fallthrough_when_rank1_ineligible() -> None:
"""V4 rank-1 ineligible (does not accept text_block) → rank-2 wins."""
contracts = [
_contract("F_DECL_FIRST", "1", ["text_block"]),
_contract("F_RANK1", "2", ["transform_table"]),
_contract("F_RANK2", "3", ["text_block"]),
]
chosen, trace = _select_frame_v4_aware(
[_text_obj()], contracts,
[_Cand("F_RANK1"), _Cand("F_RANK2")],
)
assert chosen is not None
assert chosen["template_id"] == "F_RANK2"
assert trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_V4_RANKED
assert trace[TRACE_KEY_V4_RANK_USED] == 1
assert trace[TRACE_KEY_V4_B4_FRAME_MATCH] is True
assert trace[TRACE_KEY_B4_V0_FALLBACK_REASON] is None
def test_unmatched_v4_candidate_skipped_to_next_rank() -> None:
"""V4 rank-1 template_id absent from frame_contracts → skip to next rank."""
contracts = [
_contract("F_DECL_FIRST", "1", ["text_block"]),
_contract("F_RANK2", "3", ["text_block"]),
]
chosen, trace = _select_frame_v4_aware(
[_text_obj()], contracts,
[_Cand("F_NOT_IN_CONTRACTS"), _Cand("F_RANK2")],
)
assert chosen is not None
assert chosen["template_id"] == "F_RANK2"
assert trace[TRACE_KEY_V4_RANK_USED] == 1
assert trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_V4_RANKED
# ─── Declaration-order fallback ───────────────────────────────────
def test_all_ranks_ineligible_falls_back_to_declaration_order() -> None:
"""Every V4 rank ineligible → declaration-order first eligible contract wins."""
contracts = [
_contract("F_DECL_FIRST", "1", ["text_block"]),
_contract("F_RANK1", "2", ["transform_table"]),
]
chosen, trace = _select_frame_v4_aware(
[_text_obj()], contracts, [_Cand("F_RANK1")],
)
assert chosen is not None
assert chosen["template_id"] == "F_DECL_FIRST"
assert trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_DECLARATION_ORDER
assert trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is False
assert trace[TRACE_KEY_V4_RANK_USED] is None
assert trace[TRACE_KEY_V4_B4_FRAME_MATCH] is False
assert trace[TRACE_KEY_B4_V0_FALLBACK_REASON] == "no_v4_rank_eligible"
def test_empty_v4_evidence_falls_back_to_declaration_order() -> None:
contracts = [_contract("F_DECL_FIRST", "1", ["text_block"])]
chosen, trace = _select_frame_v4_aware([_text_obj()], contracts, [])
assert chosen is not None
assert chosen["template_id"] == "F_DECL_FIRST"
assert trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_DECLARATION_ORDER
assert trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is False
assert trace[TRACE_KEY_V4_RANK_USED] is None
assert trace[TRACE_KEY_B4_V0_FALLBACK_REASON] == "v4_evidence_empty"
def test_none_v4_evidence_falls_back_to_declaration_order() -> None:
contracts = [_contract("F_DECL_FIRST", "1", ["text_block"])]
chosen, trace = _select_frame_v4_aware([_text_obj()], contracts, None)
assert chosen is not None
assert chosen["template_id"] == "F_DECL_FIRST"
assert trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_DECLARATION_ORDER
assert trace[TRACE_KEY_B4_V0_FALLBACK_REASON] == "v4_evidence_empty"
# ─── Frame_id matching ────────────────────────────────────────────
def test_v4_match_by_frame_id_when_template_id_empty() -> None:
"""V4 candidate without template_id but with frame_id matches by str-coerced frame_id."""
contracts = [_contract("F_RANK1", "1171281190", ["text_block"])]
chosen, trace = _select_frame_v4_aware(
[_text_obj()], contracts,
[_Cand(template_id="", frame_id="1171281190")],
)
assert chosen is not None
assert chosen["template_id"] == "F_RANK1"
assert trace[TRACE_KEY_V4_RANK_USED] == 0
assert trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_V4_RANKED
# ─── template_id-first precedence (Codex r1 regression) ──────────
def test_template_id_match_wins_over_frame_id_match_on_earlier_contract() -> None:
"""V4 candidate with both ``template_id`` and ``frame_id`` set —
a later contract whose ``template_id`` matches must win over an
earlier contract whose ``frame_id`` matches.
Regression for Codex r1 verification (#95 u2):
a single-pass-per-contract loop would return the earlier
``frame_id`` match because that contract is encountered first.
The two-pass implementation (template_id across ALL contracts,
then frame_id) keeps the documented precedence.
"""
contracts = [
_contract("FRAME_ID_MATCH_EARLY", "FRAME_LOSE", ["text_block"]),
_contract("TEMPLATE_WIN", "OTHER_FID", ["text_block"]),
]
chosen, trace = _select_frame_v4_aware(
[_text_obj()], contracts,
[_Cand(template_id="TEMPLATE_WIN", frame_id="FRAME_LOSE")],
)
assert chosen is not None
assert chosen["template_id"] == "TEMPLATE_WIN"
assert trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_V4_RANKED
assert trace[TRACE_KEY_V4_RANK_USED] == 0
assert trace[TRACE_KEY_V4_B4_FRAME_MATCH] is True
def test_frame_id_match_used_only_when_template_id_unmatched() -> None:
"""When the candidate's ``template_id`` is not present in any
contract, the second pass falls through to ``frame_id`` match."""
contracts = [
_contract("F_DECL_FIRST", "1", ["text_block"]),
_contract("F_OTHER", "1171281190", ["text_block"]),
]
chosen, trace = _select_frame_v4_aware(
[_text_obj()], contracts,
[_Cand(template_id="TEMPLATE_NOT_DEFINED", frame_id="1171281190")],
)
assert chosen is not None
assert chosen["template_id"] == "F_OTHER"
assert trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_V4_RANKED
assert trace[TRACE_KEY_V4_RANK_USED] == 0
# ─── Hard "no eligible contract" path ─────────────────────────────
def test_no_contract_covers_content_types_returns_none() -> None:
"""Declaration-order fallback finds no eligible contract → returns None."""
contracts = [_contract("F_ONLY", "1", ["transform_table"])]
chosen, trace = _select_frame_v4_aware([_text_obj()], contracts, [])
assert chosen is None
assert trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_DECLARATION_ORDER
assert trace[TRACE_KEY_B4_V0_FALLBACK_REASON] == "v4_evidence_empty"
# ─── Legacy selector untouched ────────────────────────────────────
def test_legacy_select_frame_signature_preserved() -> None:
"""``_select_frame`` keeps the original (objects, contracts) → Optional[dict] contract."""
contracts = [
_contract("F_FIRST", "1", ["text_block"]),
_contract("F_SECOND", "2", ["text_block"]),
]
legacy_choice = _select_frame([_text_obj()], contracts)
assert legacy_choice is not None
assert legacy_choice["template_id"] == "F_FIRST"
# ─── IMP-95 u7 — Selector coverage expansion ─────────────────────
#
# u7 scope-lock : additive tests ONLY against the existing
# ``_select_frame_v4_aware`` selector (planner u2 implementation,
# augmented by u6's ``partial_exists`` kwarg). u7 does NOT touch
# plan_placement, Step 11 wiring, gatekeeper, partial precheck, or
# any regression fixture — those belong to u3/u4/u5/u6/u8/u9/u10/u11.
#
# Strengthens the four Stage 2 A13 axes (rank-1, rank-2 fallthrough,
# all-ineligible, empty evidence) plus structural invariants the
# pipeline-trace tests (u4) and gatekeeper tests (u5) rely on :
#
# 1. Multi-rank (rank-3) fallthrough — u2 only proved rank-2.
# 2. First-eligible-rank stops iteration — V4 must not "search" past
# the first hit (a future regression here would silently re-order
# rank ties).
# 3. Trace-shape invariant — every selector return path (V4 hit,
# V4 fallthrough, empty evidence, no-eligible-contract) MUST emit
# all six u1 TRACE_KEY_* keys. u4 hoists these onto placement_trace
# verbatim; a drift would silently strip top-level keys.
# 4. Multi-content-type ⊇ preserved — V4 must not relax the
# legacy semantics for text+transform unions.
# 5. Duck-type defense — candidates missing both ``template_id`` and
# ``frame_id`` are silently skipped (matches u2 implementation,
# composition.py:678-684 contract).
# 6. Empty frame_contracts edge — V4 evidence supplied but no
# contracts → ``no_v4_rank_eligible`` fallback, not crash.
# 7. Selector path independent of env flag — the helper itself does
# not consult ``PHASE_Z_B4_V4_EVIDENCE``; gating belongs to
# ``plan_placement`` (Stage 2 A8 flag-independence).
def _transform_obj(oid: str = "tr1") -> ContentObject:
"""transform_table ContentObject for multi-content-type ⊇ tests."""
return ContentObject(
id=oid,
type="transform_table",
role="summary",
raw_payload="| AS-IS | ➜ | TO-BE |",
size_estimate={"rows": 1},
type_specific={
"pair_count": 1,
"arrow_glyph": "",
"rows": [{"from": "a", "arrow": "", "to": "b"}],
},
)
_TRACE_KEYS_EXPECTED = frozenset({
TRACE_KEY_FRAME_SELECTION_BASIS,
TRACE_KEY_V4_EVIDENCE_CONSUMED,
TRACE_KEY_V4_RANK_USED,
TRACE_KEY_V4_B4_FRAME_MATCH,
TRACE_KEY_B4_V0_FALLBACK_REASON,
"b4_partial_missing_skip",
})
def test_rank3_wins_when_ranks_1_and_2_ineligible() -> None:
"""Multi-rank fallthrough — rank-1 + rank-2 both ineligible, rank-3 wins.
u2 only covered rank-2 fallthrough; the loop must continue past
arbitrarily many ineligible ranks until the first eligible match.
"""
contracts = [
_contract("F_DECL_FIRST", "1", ["text_block"]),
_contract("F_RANK1", "2", ["transform_table"]),
_contract("F_RANK2", "3", ["transform_table"]),
_contract("F_RANK3", "4", ["text_block"]),
]
chosen, trace = _select_frame_v4_aware(
[_text_obj()], contracts,
[_Cand("F_RANK1"), _Cand("F_RANK2"), _Cand("F_RANK3")],
)
assert chosen is not None
assert chosen["template_id"] == "F_RANK3"
assert trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_V4_RANKED
assert trace[TRACE_KEY_V4_RANK_USED] == 2
assert trace[TRACE_KEY_V4_B4_FRAME_MATCH] is True
assert trace[TRACE_KEY_B4_V0_FALLBACK_REASON] is None
def test_first_eligible_rank_stops_iteration() -> None:
"""V4 must stop at the FIRST eligible rank — subsequent ranks ignored.
If a future change accidentally iterated past the first match
(e.g. to "score" all ranks), rank-2 here would silently override
rank-1 and final.html SHA parity would drift.
"""
contracts = [
_contract("F_DECL_FIRST", "1", ["text_block"]),
_contract("F_RANK1", "2", ["text_block"]),
_contract("F_RANK2", "3", ["text_block"]),
]
chosen, trace = _select_frame_v4_aware(
[_text_obj()], contracts,
[_Cand("F_RANK1"), _Cand("F_RANK2")],
)
assert chosen is not None
assert chosen["template_id"] == "F_RANK1"
assert trace[TRACE_KEY_V4_RANK_USED] == 0
def test_trace_shape_invariant_all_six_keys_present_on_every_return_path() -> None:
"""Every selector return path emits all six u1 trace keys.
u4 hoists these keys onto top-level placement_trace; a drift here
would silently strip pipeline-trace fields. Cover the four
structurally distinct return paths:
(a) V4 rank-1 match,
(b) V4 fallthrough → declaration-order fallback,
(c) empty V4 evidence → declaration-order fallback,
(d) no contract covers content_types → (None, trace).
"""
text_objs = [_text_obj()]
eligible_contracts = [_contract("F_OK", "1", ["text_block"])]
ineligible_contracts = [_contract("F_NOPE", "1", ["transform_table"])]
paths: list[tuple[object, dict]] = [
_select_frame_v4_aware(text_objs, eligible_contracts, [_Cand("F_OK")]),
_select_frame_v4_aware(text_objs, eligible_contracts, [_Cand("F_MISS")]),
_select_frame_v4_aware(text_objs, eligible_contracts, []),
_select_frame_v4_aware(text_objs, ineligible_contracts, []),
]
for _, trace in paths:
assert isinstance(trace, dict)
assert set(trace.keys()) >= _TRACE_KEYS_EXPECTED, (
f"trace missing keys: {_TRACE_KEYS_EXPECTED - set(trace.keys())}"
)
assert isinstance(trace[TRACE_KEY_V4_EVIDENCE_CONSUMED], bool)
assert isinstance(trace[TRACE_KEY_V4_B4_FRAME_MATCH], bool)
assert trace[TRACE_KEY_V4_RANK_USED] is None or isinstance(
trace[TRACE_KEY_V4_RANK_USED], int
)
assert isinstance(trace["b4_partial_missing_skip"], list)
def test_multi_content_type_supseteq_preserved_under_v4() -> None:
"""text+transform union must still satisfy ⊇ on the V4-selected contract.
A contract accepting ``text_block`` only must NOT be returned for
a content_type_set = {text_block, transform_table}, regardless of
V4 rank. The selector falls through to the next rank.
"""
contracts = [
_contract("F_DECL_FIRST", "1", ["text_block", "transform_table"]),
_contract("F_RANK1_TEXT_ONLY", "2", ["text_block"]),
_contract("F_RANK2_BOTH", "3", ["text_block", "transform_table"]),
]
chosen, trace = _select_frame_v4_aware(
[_text_obj(), _transform_obj()], contracts,
[_Cand("F_RANK1_TEXT_ONLY"), _Cand("F_RANK2_BOTH")],
)
assert chosen is not None
assert chosen["template_id"] == "F_RANK2_BOTH"
assert trace[TRACE_KEY_V4_RANK_USED] == 1
def test_candidate_without_template_or_frame_id_skipped_to_next_rank() -> None:
"""Duck-type defense — a candidate with neither identifier is silently
skipped per the composition.py:678-684 contract."""
contracts = [
_contract("F_DECL_FIRST", "1", ["text_block"]),
_contract("F_RANK2", "2", ["text_block"]),
]
chosen, trace = _select_frame_v4_aware(
[_text_obj()], contracts,
[_Cand(template_id="", frame_id=""), _Cand("F_RANK2")],
)
assert chosen is not None
assert chosen["template_id"] == "F_RANK2"
assert trace[TRACE_KEY_V4_RANK_USED] == 1
def test_empty_frame_contracts_with_v4_evidence_falls_back_no_v4_rank_eligible() -> None:
"""V4 evidence supplied but no contracts → ``no_v4_rank_eligible`` fallback
(NOT ``v4_evidence_empty``) and selector returns None without crashing."""
chosen, trace = _select_frame_v4_aware(
[_text_obj()], [], [_Cand("F_RANK1")],
)
assert chosen is None
assert trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_DECLARATION_ORDER
assert trace[TRACE_KEY_B4_V0_FALLBACK_REASON] == "no_v4_rank_eligible"
assert trace[TRACE_KEY_V4_EVIDENCE_CONSUMED] is False
def test_selector_path_independent_of_env_flag(monkeypatch) -> None:
"""Selector helper must NOT consult ``PHASE_Z_B4_V4_EVIDENCE``.
Flag-gating belongs to ``plan_placement`` (Stage 2 A8 flag
independence). Calling the selector directly with V4 evidence must
use the V4 path regardless of the env flag — otherwise u4's
pipeline-trace tests would silently flip behavior when the test
environment leaks the flag.
"""
contracts = [
_contract("F_DECL_FIRST", "1", ["text_block"]),
_contract("F_RANK1", "2", ["text_block"]),
]
# Both flag OFF and ON must yield the same V4-driven outcome.
for flag_value in ("", "1"):
monkeypatch.setenv("PHASE_Z_B4_V4_EVIDENCE", flag_value)
chosen, trace = _select_frame_v4_aware(
[_text_obj()], contracts, [_Cand("F_RANK1")],
)
assert chosen is not None
assert chosen["template_id"] == "F_RANK1"
assert trace[TRACE_KEY_FRAME_SELECTION_BASIS] == FRAME_SELECTION_BASIS_V4_RANKED
@@ -0,0 +1,121 @@
from __future__ import annotations
from src.phase_z2_retry import plan_zone_ratio_retry
_ROUTER_ACTIVE = {"router_active": True}
def _classification(target_pos: str, excess_y: float) -> dict:
return {
"classifications": [
{
"proposed_action": "zone_ratio_retry",
"zone_position": target_pos,
"inputs": {"excess_y": excess_y},
}
]
}
def _zone(position: str, height_px: int, min_height_px: int) -> dict:
return {
"position": position,
"height_px": height_px,
"min_height_px": min_height_px,
"composition_rationale": {"capacity_fit": {"fit_status": "ok"}},
}
def _overflow(*positions: str) -> dict:
return {
"zones": [
{"position": p, "overflowed": False, "clipped_inner": False}
for p in positions
]
}
def test_t28_5b_manual_target_is_blocked_and_geometry_unchanged():
debug_zones = [
_zone("left", height_px=300, min_height_px=180),
_zone("right", height_px=420, min_height_px=200),
]
plan = plan_zone_ratio_retry(
debug_zones=debug_zones,
overflow=_overflow("right"),
fit_classification=_classification("left", excess_y=40),
router_decision=_ROUTER_ACTIVE,
override_zone_geometries={"left": {"x": 0, "y": 0, "w": 0.45, "h": 1}},
)
assert plan["feasible"] is False
assert plan["manual_target_blocked"] is True
assert plan["manual_zone_positions"] == ["left"]
assert plan["donor_candidates_considered"] == []
assert plan["zones_after"] == {"left": 300, "right": 420}
assert "manual target zone" in plan["failure_reason"]
def test_t28_5b_manual_donor_is_excluded_but_auto_donor_can_resolve():
debug_zones = [
_zone("top", height_px=300, min_height_px=200),
_zone("middle", height_px=260, min_height_px=200),
_zone("bottom", height_px=360, min_height_px=200),
]
plan = plan_zone_ratio_retry(
debug_zones=debug_zones,
overflow=_overflow("middle", "bottom"),
fit_classification=_classification("top", excess_y=40),
router_decision=_ROUTER_ACTIVE,
override_zone_geometries={"bottom": {"x": 0, "y": 0.5, "w": 1, "h": 0.5}},
)
assert plan["feasible"] is True
assert plan["target_added_px"] == 44
assert plan["donor_zone_position"] == "middle"
assert plan["donors_used"] == [
{"position": "middle", "reduced_px": 44, "slack_before": 60, "slack_after": 16}
]
assert plan["zones_after"]["top"] == 344
assert plan["zones_after"]["middle"] == 216
assert plan["zones_after"]["bottom"] == 360
def test_t28_5b_no_automatic_donor_escalates_without_using_manual_slack():
debug_zones = [
_zone("top", height_px=300, min_height_px=200),
_zone("middle", height_px=200, min_height_px=200),
_zone("bottom", height_px=420, min_height_px=200),
]
plan = plan_zone_ratio_retry(
debug_zones=debug_zones,
overflow=_overflow("middle", "bottom"),
fit_classification=_classification("top", excess_y=40),
router_decision=_ROUTER_ACTIVE,
override_zone_geometries={"bottom": {"x": 0, "y": 0.5, "w": 1, "h": 0.5}},
)
assert plan["feasible"] is False
assert plan["donor_candidates_considered"] == []
assert plan["zones_after"] == {"top": 300, "middle": 200, "bottom": 420}
assert "no donor candidates eligible" in plan["failure_reason"]
def test_t28_5b_same_input_same_decision():
kwargs = {
"debug_zones": [
_zone("top", height_px=300, min_height_px=200),
_zone("middle", height_px=260, min_height_px=200),
_zone("bottom", height_px=360, min_height_px=200),
],
"overflow": _overflow("middle", "bottom"),
"fit_classification": _classification("top", excess_y=40),
"router_decision": _ROUTER_ACTIVE,
"override_zone_geometries": {"bottom": {"x": 0, "y": 0.5, "w": 1, "h": 0.5}},
}
assert plan_zone_ratio_retry(**kwargs) == plan_zone_ratio_retry(**kwargs)
@@ -59,6 +59,13 @@ def test_standalone_mode_explicit():
assert "params.get('embedded')" not in html
def test_debug_marker_hidden_by_default():
html = _render("auto")
body = html.split("<body>", 1)[1]
assert "phase-z2-marker" not in body
assert "phase_z2 / mvp-1.5b" not in body
def test_deterministic():
assert _render("embedded") == _render("embedded")
assert _render("auto") == _render("auto")