feat(IMP-15): 실행-3 — classifier consumes image+table events

Issue #47 (IMP-15 실행-3 axis 3): extend `classify_visual_runtime_check`
to consume the `image_events[]` and `table_events[]` arrays produced by
`run_overflow_check` (실행-1/2) and widen `visual_check_passed`.

Changes (src/phase_z2_classifier.py):
- Remove `overflow.passed=True` early-return so image/table event scans
  always run, even when zone-level overflow was clean.
- Deferred import of `IMAGE_ASPECT_DELTA_TOL` and `TABLE_SCROLL_TOL_PX`
  from `phase_z2_pipeline` (circular-safe SSoT; no duplicate literals).
- New `image_events` scan emits `image_aspect_mismatch` when
  `delta is not None AND |delta| > IMAGE_ASPECT_DELTA_TOL`
  (delta=None ⇒ skip, image not loaded).
- New `table_events` scan emits `tabular_overflow` when
  `wrapper_clipped_index is None AND (excess_x or excess_y > TABLE_SCROLL_TOL_PX)`
  (wrapper-clipped tables deduped against the existing zone cascade).
- `visual_check_passed = overflow.passed AND not classifications` —
  any image/table classification now flips the gate.

Guardrails preserved:
- §3.2 8-rule zone cascade (clipped_inner / zone-self) untouched —
  the new emitters are ADDITIONAL.
- `placement_diagnostics`, `categories_seen`, `unclassified_signals`
  return-shape preserved.
- No `pipeline.py` production changes; no router action or
  `debug.json` passthrough changes.

Tests (tests/phase_z2/test_phase_z2_visual_classifier.py — new):
- `test_image_aspect_mismatch_emits_classification` (|delta|>TOL fires)
- `test_image_aspect_delta_below_tol_no_classification` (≤TOL skipped)
- `test_standalone_table_overflow_emits_classification`
  (wrapper_clipped_index=None, excess>TOL fires)
- `test_table_dedup_when_wrapper_clipped`
  (wrapper_clipped_index set ⇒ no `tabular_overflow` emit)

All 4 pure-dict (no Selenium / chromedriver / pipeline execution).
Tolerances imported from `phase_z2_pipeline` (SSoT enforced via test
import — no classifier-local literals).

Verification (Stage 4):
- New classifier tests: 4/4 PASS.
- Regression `tests/phase_z2/` excluding new file: 93/93 PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 21:45:06 +09:00
parent 2827622858
commit 535c4848fd
2 changed files with 177 additions and 11 deletions

View File

@@ -0,0 +1,116 @@
"""IMP-15 실행-3 (Gitea issue #47) — classifier consumer pure-dict tests.
`classify_visual_runtime_check` was widened to consume the new
``image_events[]`` / ``table_events[]`` arrays produced by ``run_overflow_check``
(IMP-15 실행-1/2). The consumer must:
* emit ``image_aspect_mismatch`` when ``|delta| > IMAGE_ASPECT_DELTA_TOL`` and
skip when ``delta is None`` or ``|delta| <= IMAGE_ASPECT_DELTA_TOL``;
* emit ``tabular_overflow`` when a table self-overflows beyond
``TABLE_SCROLL_TOL_PX`` and ``wrapper_clipped_index is None`` — and dedupe
when the table sits under a wrapper already on the clipped-wrapper map
(``wrapper_clipped_index`` non-null);
* flip ``visual_check_passed`` to False whenever any classification fires, even
if zone-level overflow was clean (``overflow["passed"]=True``).
All four cases are pure-dict — no Selenium / chromedriver dependency.
"""
from __future__ import annotations
from src.phase_z2_classifier import classify_visual_runtime_check
from src.phase_z2_pipeline import IMAGE_ASPECT_DELTA_TOL, TABLE_SCROLL_TOL_PX
def _base_overflow(**overrides) -> dict:
"""Minimal clean overflow result; tests overlay image/table events."""
base = {
"passed": True,
"slide": {"overflowed": False},
"slide_body": {"overflowed": False},
"zones": [],
"image_events": [],
"table_events": [],
}
base.update(overrides)
return base
# ─── image_events scan ───────────────────────────────────────────────
def test_image_aspect_mismatch_emits_classification():
"""|delta| > IMAGE_ASPECT_DELTA_TOL ⇒ emit + flip visual_check_passed."""
delta = IMAGE_ASPECT_DELTA_TOL + 0.05
overflow = _base_overflow(image_events=[{
"zone_position": "top",
"zone_template_id": "f1b",
"src": "img/sample.png",
"natural_ratio": 2.0,
"rendered_ratio": 2.0 * (1.0 + delta),
"delta": delta,
}])
result = classify_visual_runtime_check(overflow, debug_zones=[])
assert result["visual_check_passed"] is False
assert result["categories_seen"] == ["image_aspect_mismatch"]
assert len(result["classifications"]) == 1
cls = result["classifications"][0]
assert cls["category"] == "image_aspect_mismatch"
assert cls["source"] == "image_event"
assert cls["zone_position"] == "top"
assert cls["delta"] == delta
def test_image_aspect_delta_below_tol_no_classification():
"""|delta| <= IMAGE_ASPECT_DELTA_TOL ⇒ skip (no false positive)."""
delta = IMAGE_ASPECT_DELTA_TOL / 2.0
overflow = _base_overflow(image_events=[{
"zone_position": "top",
"zone_template_id": "f1b",
"src": "img/sample.png",
"natural_ratio": 2.0,
"rendered_ratio": 2.0 * (1.0 + delta),
"delta": delta,
}])
result = classify_visual_runtime_check(overflow, debug_zones=[])
assert result["visual_check_passed"] is True
assert result["categories_seen"] == []
assert result["classifications"] == []
# ─── table_events scan ───────────────────────────────────────────────
def test_standalone_table_overflow_emits_classification():
"""wrapper_clipped_index=None AND excess > TOL ⇒ emit tabular_overflow."""
excess = TABLE_SCROLL_TOL_PX + 10
overflow = _base_overflow(table_events=[{
"zone_position": "bottom_l",
"zone_template_id": "f13b",
"wrapper_clipped_index": None,
"excess_x": 0,
"excess_y": excess,
}])
result = classify_visual_runtime_check(overflow, debug_zones=[])
assert result["visual_check_passed"] is False
assert result["categories_seen"] == ["tabular_overflow"]
assert len(result["classifications"]) == 1
cls = result["classifications"][0]
assert cls["category"] == "tabular_overflow"
assert cls["source"] == "table_event"
assert cls["zone_position"] == "bottom_l"
assert cls["excess_y"] == excess
def test_table_dedup_when_wrapper_clipped():
"""wrapper_clipped_index non-null ⇒ skip (dedupe with clipped_inner cascade)."""
overflow = _base_overflow(table_events=[{
"zone_position": "bottom_l",
"zone_template_id": "f13b",
"wrapper_clipped_index": 0,
"excess_x": 0,
"excess_y": TABLE_SCROLL_TOL_PX + 50,
}])
result = classify_visual_runtime_check(overflow, debug_zones=[])
assert result["visual_check_passed"] is True
assert result["categories_seen"] == []
assert result["classifications"] == []