feat(IMP-15): 실행-4 — debug.json event surfacing + spec taxonomy row

Issue: #48 (IMP-15 실행-4, axis 4: debug.json + spec doc trace).
Parent: #15. Depends on 실행-1/2/3 (events + classifier outputs).

Surfaces the image/table event streams that 실행-1/2/3 already produced
and consumed, mirroring the existing `zone_geometries_px` top-level
precedent (no new pattern introduced). Adds the matching taxonomy row
to the Phase Z fit-classifier/router spec.

src/phase_z2_pipeline.py (+3):
- write_debug_json now lifts `image_events` and `table_events` to
  top-level of `debug.json` via `(visual_runtime_check or {}).get(<k>, [])`,
  exactly mirroring the immediately preceding `zone_geometries_px`
  surfacing line. Defaults to `[]` when `visual_runtime_check` is None
  — additive, no consumer-visible breakage.

docs/architecture/PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC.md (+1):
- §3.1 taxonomy adds `image_aspect_mismatch` row. Row text explicitly
  marks the signal as post-render `fail_reasons` from Step 14
  visual_runtime_check (rendered vs declared aspect ratio mismatch),
  NOT a router-routed fit_classifier output, and notes the separate
  `image_events` stream surface. Prevents future readers from wiring
  this taxonomy into §3.2 priority list or §4 router action map.

tests/phase_z2/test_debug_json_event_surfacing.py (new, 2 tests):
- `test_write_debug_json_surfaces_image_and_table_events` invokes
  write_debug_json with synthetic visual_runtime_check containing
  both event lists; reads back the on-disk debug.json and asserts
  both keys are present at top level with the exact payloads.
- `test_write_debug_json_defaults_when_visual_runtime_check_none`
  asserts both new keys default to `[]` when visual_runtime_check
  is None — guards the defensive `(… or {})` pattern.

tests/phase_z2/test_spec_taxonomy_image_aspect_mismatch.py (new, 2 tests):
- `test_spec_has_image_aspect_mismatch_row` opens the spec file and
  asserts exactly one `^\| image_aspect_mismatch \|` row exists
  inside the §3.1 table block (no markdown-parser dependency).
- `test_spec_row_marks_post_render_fail_reasons_semantic` asserts the
  row text carries both "Post-render" and "fail_reasons" tokens —
  enforces the Stage 1 guardrail wording.

Verification (Stage 4 PASS, Claude + Codex independent):
- pytest -q tests/phase_z2/test_debug_json_event_surfacing.py \
              tests/phase_z2/test_spec_taxonomy_image_aspect_mismatch.py
  → 4 passed in 0.07s.
- git diff scope: 4 files, +148 insertions / 0 deletions.

Scope-locked: no edits to classifier (실행-3), event generation
(실행-1/2), Step 21 viewer, §3.2 priority list, §4 router action
mapping, or `table_self_overflow` taxonomy row. Pre-existing
dirty/untracked working-tree files left untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 22:25:41 +09:00
parent 535c4848fd
commit 614c53358e
4 changed files with 148 additions and 0 deletions

View File

@@ -93,6 +93,7 @@ action :
| `moderate_overflow` | content_type ∈ {`text_flow`, `frame_label`} AND `line_equivalent` ∈ (1.5, 4] |
| `minor_overflow` | content_type ∈ {`text_flow`, `frame_label`} AND `line_equivalent` ≤ 1.5 |
| `hard_visual_fail` | 위 어디에도 매핑 안 됨 OR retry budget 소진 |
| `image_aspect_mismatch` | Post-render `fail_reasons` signal — Step 14 visual_runtime_check 가 이미지 frame slot 의 rendered aspect ratio 와 declared aspect ratio 불일치를 감지 (router-routed fit_classifier 출력 아님; 별도 image_events stream 으로 표면화) |
### 3.2 분류 우선순위 (위에서 아래로)

View File

@@ -2737,6 +2737,9 @@ def write_debug_json(run_dir: Path, layout_preset: str,
"visual_runtime_check": visual_runtime_check,
# A-6 (IMP-01 #1) — additive top-level zone bbox trace (slide-relative px)
"zone_geometries_px": (visual_runtime_check or {}).get("zone_geometries_px", []),
# IMP-15 실행-4 (issue #48) — additive top-level Step 14 event streams
"image_events": (visual_runtime_check or {}).get("image_events", []),
"table_events": (visual_runtime_check or {}).get("table_events", []),
}
debug_path = run_dir / "debug.json"
debug_path.write_text(json.dumps(debug, ensure_ascii=False, indent=2), encoding="utf-8")

View File

@@ -0,0 +1,81 @@
"""IMP-15 실행-4 (Gitea issue #48) — debug.json top-level event surfacing.
Verifies ``write_debug_json`` lifts ``image_events`` + ``table_events`` out of
``visual_runtime_check`` and exposes them as top-level keys, mirroring the
existing ``zone_geometries_px`` precedent (src/phase_z2_pipeline.py:2739).
Two scenarios:
* Populated — ``visual_runtime_check`` carries non-empty event lists; the
written debug dict surfaces both at the top level with identical payloads.
* None — ``visual_runtime_check is None``; both top-level keys default to ``[]``
(no KeyError, no propagated None).
"""
from __future__ import annotations
import json
from pathlib import Path
from src.phase_z2_pipeline import write_debug_json
def _read_debug(run_dir: Path) -> dict:
return json.loads((run_dir / "debug.json").read_text(encoding="utf-8"))
def test_write_debug_json_surfaces_image_and_table_events(tmp_path: Path) -> None:
image_events = [
{
"src": "img/a.png",
"zone_position": "primary",
"zone_template_id": "tid-1",
"natural_w": 200,
"natural_h": 100,
"rendered_w": 200,
"rendered_h": 200,
"delta": 1.0,
}
]
table_events = [
{
"zone_position": "secondary",
"zone_template_id": "tid-2",
"clientWidth": 300,
"scrollWidth": 360,
"excess_x": 60,
"wrapper_clipped_index": 0,
}
]
visual_runtime_check = {
"image_events": image_events,
"table_events": table_events,
"zone_geometries_px": [],
}
write_debug_json(
run_dir=tmp_path,
layout_preset="single",
debug_zones=[],
layout_css={},
visual_runtime_check=visual_runtime_check,
)
debug = _read_debug(tmp_path)
assert "image_events" in debug, "image_events must be a top-level key"
assert "table_events" in debug, "table_events must be a top-level key"
assert debug["image_events"] == image_events
assert debug["table_events"] == table_events
def test_write_debug_json_defaults_when_visual_runtime_check_none(tmp_path: Path) -> None:
write_debug_json(
run_dir=tmp_path,
layout_preset="single",
debug_zones=[],
layout_css={},
visual_runtime_check=None,
)
debug = _read_debug(tmp_path)
assert debug["image_events"] == []
assert debug["table_events"] == []

View File

@@ -0,0 +1,63 @@
"""Spec lint: PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC.md §3.1 taxonomy must declare
the `image_aspect_mismatch` row (IMP-15 실행-4, issue #48 u2).
The row encodes a post-render `fail_reasons` signal surfaced by Step 14
visual_runtime_check, not a router-routed fit_classifier output. It is
intentionally placed inside §3.1 to keep the taxonomy vocabulary aligned
with the event streams now exposed at debug.json top level (u1).
"""
from __future__ import annotations
import re
from pathlib import Path
SPEC_PATH = (
Path(__file__).resolve().parents[2]
/ "docs"
/ "architecture"
/ "PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC.md"
)
def _read_spec_text() -> str:
return SPEC_PATH.read_text(encoding="utf-8")
def _extract_section_3_1(text: str) -> str:
start_match = re.search(r"^###\s+3\.1\b", text, flags=re.MULTILINE)
assert start_match, "§3.1 heading missing from spec"
after_3_1 = text[start_match.end():]
end_match = re.search(r"^###\s+3\.2\b", after_3_1, flags=re.MULTILINE)
assert end_match, "§3.2 heading missing from spec"
return after_3_1[: end_match.start()]
def test_spec_section_3_1_contains_image_aspect_mismatch_row():
section = _extract_section_3_1(_read_spec_text())
row_pattern = re.compile(r"^\|\s*`image_aspect_mismatch`\s*\|", re.MULTILINE)
matches = row_pattern.findall(section)
assert len(matches) == 1, (
"Expected exactly 1 `image_aspect_mismatch` row inside §3.1 taxonomy, "
f"found {len(matches)}"
)
def test_image_aspect_mismatch_row_reflects_post_render_semantic():
section = _extract_section_3_1(_read_spec_text())
row_line = next(
(
line
for line in section.splitlines()
if line.lstrip().startswith("| `image_aspect_mismatch`")
),
None,
)
assert row_line is not None, "image_aspect_mismatch row not found"
assert "Post-render" in row_line or "post-render" in row_line, (
"Row must mark the signal as post-render (Stage 1 guardrail)"
)
assert "fail_reasons" in row_line, (
"Row must reference `fail_reasons` so the vocabulary mirrors the "
"visual_runtime_check output"
)