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:
@@ -344,7 +344,7 @@ def classify_visual_runtime_check(overflow: dict, debug_zones: list[dict]) -> di
|
||||
|
||||
Returns:
|
||||
dict :
|
||||
visual_check_passed : Selenium 통과 여부
|
||||
visual_check_passed : Selenium 통과 여부 (overflow.passed AND no classifications)
|
||||
classifications : 각 overflow event 의 분류 결과 list
|
||||
summary : 텍스트 요약 (n events, categories seen)
|
||||
categories_seen : 등장한 카테고리 unique list
|
||||
@@ -353,6 +353,12 @@ def classify_visual_runtime_check(overflow: dict, debug_zones: list[dict]) -> di
|
||||
divergence + region / slot_assignment / rejection
|
||||
count) — passed 여부 무관 항상 surface
|
||||
"""
|
||||
# Deferred import — phase_z2_pipeline imports this module at module top, so
|
||||
# a top-level `from phase_z2_pipeline import ...` would be circular. Pulled
|
||||
# in at call time so both modules are fully loaded. Tolerances are owned by
|
||||
# phase_z2_pipeline (single source of truth — see IMP-15 실행-1/2).
|
||||
from phase_z2_pipeline import IMAGE_ASPECT_DELTA_TOL, TABLE_SCROLL_TOL_PX
|
||||
|
||||
# placement_diagnostics — debug_zones[i].placement_trace 를 per-zone diagnostic 으로 surface.
|
||||
# passed 여부 무관 항상 빌드 (B4 vs mapper divergence 가 passed 에서도 진단 가치).
|
||||
placement_diagnostics = [
|
||||
@@ -364,15 +370,9 @@ def classify_visual_runtime_check(overflow: dict, debug_zones: list[dict]) -> di
|
||||
for dz in (debug_zones or [])
|
||||
]
|
||||
|
||||
if overflow.get("passed", False):
|
||||
return {
|
||||
"visual_check_passed": True,
|
||||
"classifications": [],
|
||||
"summary": "visual check passed — no overflow to classify",
|
||||
"categories_seen": [],
|
||||
"unclassified_signals": [],
|
||||
"placement_diagnostics": placement_diagnostics,
|
||||
}
|
||||
# IMP-15 실행-3 (issue #47): no early-return on overflow.passed=True.
|
||||
# image_events / table_events scans below run unconditionally; the final
|
||||
# visual_check_passed is widened to: overflow.passed AND no classifications.
|
||||
|
||||
# zone position → debug_zones 매핑 (capacity_fit_status 추출용)
|
||||
capacity_status_by_position: dict[str, Optional[str]] = {}
|
||||
@@ -423,6 +423,53 @@ def classify_visual_runtime_check(overflow: dict, debug_zones: list[dict]) -> di
|
||||
cls["scroll_height"] = c.get("scrollHeight")
|
||||
classifications.append(cls)
|
||||
|
||||
# IMP-15 실행-3 (issue #47): image_events scan — image_aspect_mismatch emitter.
|
||||
# delta is None ⇒ skip (image not loaded; no false positive).
|
||||
# |delta| > IMAGE_ASPECT_DELTA_TOL ⇒ emit classification.
|
||||
for ev in (overflow.get("image_events") or []):
|
||||
delta = ev.get("delta")
|
||||
if delta is None:
|
||||
continue
|
||||
if abs(delta) > IMAGE_ASPECT_DELTA_TOL:
|
||||
classifications.append({
|
||||
"category": "image_aspect_mismatch",
|
||||
"source": "image_event",
|
||||
"zone_position": ev.get("zone_position"),
|
||||
"zone_template_id": ev.get("zone_template_id"),
|
||||
"src": ev.get("src"),
|
||||
"natural_ratio": ev.get("natural_ratio"),
|
||||
"rendered_ratio": ev.get("rendered_ratio"),
|
||||
"delta": delta,
|
||||
"rule_applied": (
|
||||
f"|delta|={abs(delta):.4f} > IMAGE_ASPECT_DELTA_TOL="
|
||||
f"{IMAGE_ASPECT_DELTA_TOL} (IMP-15 실행-3)"
|
||||
),
|
||||
})
|
||||
|
||||
# IMP-15 실행-3 (issue #47): table_events scan — tabular_overflow emitter.
|
||||
# wrapper_clipped_index is not None ⇒ skip (clipped_inner already covers this
|
||||
# case via zone cascade; honor dedup contract from pipeline producer).
|
||||
# excess_x or excess_y > TABLE_SCROLL_TOL_PX ⇒ emit tabular_overflow.
|
||||
for ev in (overflow.get("table_events") or []):
|
||||
if ev.get("wrapper_clipped_index") is not None:
|
||||
continue
|
||||
excess_x = ev.get("excess_x") or 0
|
||||
excess_y = ev.get("excess_y") or 0
|
||||
if excess_x > TABLE_SCROLL_TOL_PX or excess_y > TABLE_SCROLL_TOL_PX:
|
||||
classifications.append({
|
||||
"category": "tabular_overflow",
|
||||
"source": "table_event",
|
||||
"zone_position": ev.get("zone_position"),
|
||||
"zone_template_id": ev.get("zone_template_id"),
|
||||
"excess_x": excess_x,
|
||||
"excess_y": excess_y,
|
||||
"rule_applied": (
|
||||
f"table self-overflow — excess_x={excess_x} or excess_y="
|
||||
f"{excess_y} > TABLE_SCROLL_TOL_PX={TABLE_SCROLL_TOL_PX} "
|
||||
f"(wrapper not clipped; IMP-15 실행-3)"
|
||||
),
|
||||
})
|
||||
|
||||
# slide-level / slide-body overflow (zones 외부) 도 분류 시도 (보통 zone-level 에서 잡히지만 보조)
|
||||
unclassified: list[dict] = []
|
||||
slide_m = overflow.get("slide") or {}
|
||||
@@ -443,8 +490,11 @@ def classify_visual_runtime_check(overflow: dict, debug_zones: list[dict]) -> di
|
||||
})
|
||||
|
||||
categories = sorted({c["category"] for c in classifications})
|
||||
# IMP-15 실행-3 (issue #47): widened semantic — overflow.passed alone is not
|
||||
# enough; any image/table classification also flips visual_check_passed.
|
||||
visual_check_passed = bool(overflow.get("passed", False)) and not classifications
|
||||
return {
|
||||
"visual_check_passed": False,
|
||||
"visual_check_passed": visual_check_passed,
|
||||
"classifications": classifications,
|
||||
"summary": (
|
||||
f"{len(classifications)} overflow event(s) classified, "
|
||||
|
||||
Reference in New Issue
Block a user