[{"id": 19414, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/45#issuecomment-19414", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/45", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "body": "[Claude #1] Stage 1 Round #1 problem-review (issue #45 = IMP-15 실행-1 image axis)\n\ncommit_sha: 7a52cebfaa8b356c42a5de45c7241a3002615cab\n\n## 1. Issue restatement (scope axis 1 only)\n\nAdd Step 14 (`run_overflow_check`) runtime measurement for every `` in the rendered slide:\nintrinsic aspect ratio (naturalWidth/naturalHeight) vs rendered aspect ratio\n(rendered width/height); when `|delta| > IMAGE_ASPECT_DELTA_TOL (=0.05)`, emit an\n`image aspect mismatch in zone--: …` entry into the existing `fail_reasons` list.\nNew JS payload field `image_events[]` populated for every ``. Selenium-driven\ntest added with chromedriver skip-guard + `xfail(strict=True)` when\n`PHASE_Z_REQUIRE_SELENIUM=1`.\n\nOUT-OF-SCOPE (deferred sub-issues): `table_events` (실행-2), classifier consumer +\n`visual_check_passed` flip (실행-3), debug.json surfacing + spec doc row (실행-4).\n\n## 2. Current code gap (factual)\n\n| axis | file:line | current state | gap |\n|---|---|---|---|\n| Step 14 JS payload | `src/phase_z2_pipeline.py:2105-2212` (`run_overflow_check`) | returns `{slide, slide_body, zones, frame_slot_metrics, zone_geometries_px}`; iterates `.zone` for `clipped_inner` only | no `` inspection; no `image_events[]` field |\n| Step 14 fail_reason aggregation | `src/phase_z2_pipeline.py:2226-2252` | appends slide / slide_body / zone / clipped_inner fail strings | no image-related branch |\n| pipeline constant | `src/phase_z2_pipeline.py` (none) | absent | `IMAGE_ASPECT_DELTA_TOL = 0.05` must be added at module scope (importable for tests) |\n| step note | `src/phase_z2_pipeline.py:4355-4359` | `\"image / table 검사 부재 — Step 14 ⚠ partial.\"` | this issue closes the image half (table = 실행-2) |\n| skip-guard env var | nowhere in repo (greppped — only in `.orchestrator/tmp/codex_prompt.txt:161`) | not a convention yet | this issue introduces `PHASE_Z_REQUIRE_SELENIUM=1` |\n| chromedriver resolver | `src/phase_z2_pipeline.py:2082-2099` | candidates = `PROJECT_ROOT/chromedriver{,.exe}` then PATH | test skip-guard MUST mirror this exact resolution order (factual lock) |\n| zone marker | `templates/phase_z2/slide_base.html:267` (`data-zone-position=\"{{ zone.position }}\"`) | present on `.zone` divs only | attachment fallback (img with no `.zone` ancestor) → `zone_position = 'unknown'` per issue Fixture C |\n\nNo collision with classifier (`src/phase_z2_classifier.py`) — that module is not touched in this issue per the explicit 실행-3 deferral.\n\n## 3. Spec anchor verification (no-hardcoding lock)\n\nIssue body claims `IMAGE_ASPECT_DELTA_TOL = 0.05` is \"spec-anchored to\nPHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC §3.1\". I verified\n`docs/architecture/PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC.md:82-113` (§3.1 has 8\ncategories: `frame_capacity_mismatch`, `tabular_overflow`, `structural_major/minor`,\n`layout_zone_mismatch`, `moderate/minor_overflow`, `hard_visual_fail`). **No\n`image_aspect_mismatch` category exists in §3.1 yet.** §2 registry classifies ``\nas `visual_asset` (line 75) but the category row is missing.\n\nThis is consistent with the issue's own out-of-scope split — spec doc row is\nexplicitly deferred to 실행-4. So this issue lays the *detection wire + fail_reason\nemission* now; the spec category row + classifier consumption arrive in 실행-3/4.\n**The constant name and the wire format introduced here will be the binding contract\nthose later sub-issues consume**, so naming + structure must be stable.\n\n## 4. Scope-lock (files + symbols)\n\nAllowed edits only:\n- `src/phase_z2_pipeline.py`:\n - module-scope: new constant `IMAGE_ASPECT_DELTA_TOL = 0.05` (placed near other Step 14 constants — confirm exact insertion point in Stage 2 plan).\n - inside `run_overflow_check` JS payload: add a new `image_events = []` block that walks `slide.querySelectorAll('img')`. Per-image entry must record:\n `zone_position` (closest `.zone` `data-zone-position`, else `'unknown'`),\n `natural_width`, `natural_height`, `rendered_width`, `rendered_height`,\n `intrinsic_ratio`, `rendered_ratio`, `aspect_delta` (formula locked in Stage 2),\n `complete` (img.complete), `src_suffix` (last path segment, no full URL — privacy + log size).\n - JS return object: add `image_events` key. Existing keys unchanged.\n - Python aggregation block (after current zone loop, before `result[\"passed\"] = …`):\n iterate `result.get(\"image_events\", [])`; when entry is loaded AND `aspect_delta > IMAGE_ASPECT_DELTA_TOL` → `fail_reasons.append(\"image aspect mismatch in zone--: …\")`.\n- `tests/phase_z2/test_phase_z2_step14_image_check.py` (NEW): three Selenium fixtures (A/B/C per issue body) + chromedriver skip-guard mirroring `run_overflow_check` resolver + `xfail(strict=True)` when `PHASE_Z_REQUIRE_SELENIUM=1`.\n\nForbidden in this issue:\n- `src/phase_z2_classifier.py` — DO NOT touch (실행-3).\n- `compute_slide_status` / debug.json surfacing — DO NOT touch (실행-4).\n- `visual_check_passed` flip — `overflow.get(\"passed\", False)` semantics stay\n identical; adding to `fail_reasons` will naturally flip `passed` to False when a\n mismatch is detected, but no other surface should be reshaped here.\n- `table_events` — strictly 실행-2.\n- Spec doc row in PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC.md — 실행-4.\n- Any MDX 03/04/05 sample hardcoding (RULE 0 / RULE 7).\n\n## 5. Decisions that MUST land in Stage 2 plan (do not resolve in this stage)\n\n| # | Decision | Why deferred to Stage 2 |\n|---|---|---|\n| D1 | `aspect_delta` formula: **relative** (`abs(intrinsic-rendered)/intrinsic`) vs absolute (`abs(intrinsic-rendered)`) | Both satisfy issue body Fixture A (delta=0) and Fixture B (relative=0.5, absolute=1.0, both > 0.30). Tolerance `0.05` is small — relative is more semantically defensible (1 px on a 1000-px ratio is not the same as on a 2-px ratio). Recommend RELATIVE; Stage 2 plan must lock and the test must assert by the same formula. |\n| D2 | Loaded-image guard | If `!img.complete \\|\\| !img.naturalWidth`, set `aspect_delta = null` and SKIP the fail_reason. Otherwise headless-Chrome image-load race causes false positives. Document this in JS comment + test asserts that an unloaded image does not produce a fail_reason. |\n| D3 | Rendered dimensions source | `img.clientWidth/clientHeight` (CSS box, no border/padding for replaced elements) vs `getBoundingClientRect().{width,height}` (sub-pixel). Recommend `getBoundingClientRect()` rounded to int — matches the existing `zone_geometries_px` convention at `src/phase_z2_pipeline.py:2137-2145`. |\n| D4 | fail_reason exact wording | Issue body shows the prefix `\"image aspect mismatch in zone--: \"` — Stage 2 plan must lock the suffix (suggest: `\"intrinsic x ({intrinsic_ratio:.2f}) vs rendered x ({rendered_ratio:.2f}), delta={delta:.3f}\"`). |\n| D5 | Test fixture image source | Synthetic via `data:image/png;base64,…` (Fixture A/B/C self-contained, no asset files, no MDX dependency). 1×1 PNG seed scaled to declared intrinsic via `width`/`height` attributes is NOT acceptable (that would make naturalWidth=1). Must use two real intrinsic-size PNGs (200×100 and 200×200) embedded as base64. |\n| D6 | xfail-strict mechanism | `pytest.mark.xfail(condition=os.environ.get(\"PHASE_Z_REQUIRE_SELENIUM\") == \"1\", strict=True, reason=\"…\")` vs `pytest.skip` for default path. Recommend the **same fixture predicate** drives both: skip when chromedriver unresolvable AND env not set; xfail-strict when env set AND chromedriver unresolvable. |\n| D7 | Constant placement | Top of `src/phase_z2_pipeline.py` near other module constants vs immediately above `run_overflow_check`. Stage 2 must pick one and grep-verify no name clash. |\n\n## 6. Guardrails (binding for Stage 2 plan + Stage 3 edit)\n\n- G1: **Existing `fail_reasons` strings for slide / slide_body / zone / clipped_inner cases stay byte-identical.** New entries are append-only. Stage 3 verification must include a regression assertion that a slide with no `` produces the same `fail_reasons` list as before this change.\n- G2: **No image hardcoding to MDX sample slides.** Test fixtures are self-contained synthetic HTML strings written into `tmp_path`. RULE 0.\n- G3: **`image_events` ordering is DOM order** (`querySelectorAll` document order). Stage 3 test must assert this so 실행-3 classifier consumer can rely on it.\n- G4: **Attachment fallback wording** for fail_reason is `zone--unknown` (lowercase), matching existing `zone--{position}` convention at line 2242.\n- G5: **Constant rename later is a breaking change** for 실행-3 → spec doc row + classifier; pick the name carefully now. `IMAGE_ASPECT_DELTA_TOL` is the locked name (issue body specifies it).\n- G6: **Selenium test must NOT depend on running the full pipeline.** It calls `run_overflow_check(tmp_path/X.html)` directly — same boundary as Codex #4 enforced for IMP-15 parent.\n- G7: **`src_suffix` capture is for debug-only**, not asserted in fail_reason wording (logs may be reviewed by humans — full URLs leak filesystem paths).\n- G8: When the Selenium guard skips, the test is *informationally green* — Stage 4 verify will mark \"pytest collected N tests, M skipped (selenium unresolved)\" so the orchestrator sees coverage status.\n\n## 7. Anchors I will quote in Stage 2 / Stage 3\n\n- `src/phase_z2_pipeline.py:2070-2253` (`run_overflow_check` body)\n- `src/phase_z2_pipeline.py:4347-4360` (Step 14 step-artifact note that this PR partially closes)\n- `docs/architecture/PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC.md:65-78` (§2 — `` = `visual_asset`)\n- `templates/phase_z2/slide_base.html:267` (`data-zone-position` source)\n- `tests/phase_z2/test_slide_base_embedded_mode.py:1-50` (existing test pattern reference for new test file structure)\n\n## 8. Risks / open questions for reviewer\n\n- R1: Is `IMAGE_ASPECT_DELTA_TOL = 0.05` an absolute or relative delta? Issue body does not pin. I am recommending **relative**; if reviewer prefers absolute, Stage 2 plan must update D1 + D4 fail_reason wording + test thresholds.\n- R2: Should the `image_events[]` entry include an `aspect_delta_exceeded: bool` boolean for downstream consumers, or only the raw delta? I recommend including both (raw for telemetry, bool for fast filter). Confirm in Stage 2.\n- R3: Headless Chrome image loading. Will `driver.implicitly_wait(1)` give a 200×100 data-URL PNG enough time to set `naturalWidth`? If not, Fixture A/B may flake. Stage 2 plan should call out an explicit `WebDriverWait until img.complete && img.naturalWidth > 0` in the fixture HTML's own `