import json, os, urllib.request body = """[Claude #8] **unit_executed**: u5 — orphan + missing_index_html detection (categorize discover() rows, never silently skip) **files_changed**: - MOD `D:\\ad-hoc\\kei\\design_agent\\scripts\\generate_frame_previews.py` (110 → 121 lines per `Measure-Object -Line`; u5 net add = 11 lines, ≤ Stage 2 u5 estimate of 35) **diff_summary**: - Added `categorize(rows: List[FrameRow]) -> Dict[str, List[FrameRow]]` between `is_unchanged()` and `main()`. Three buckets per Stage 2 u5 spec: - `renderable` = rows with `has_index=True` (eligible for render or `skipped_unchanged` decision in u6). - `missing_index_html` = rows with `has_index=False` (catalog gap; IMP-04 follow-up axis — out of scope per Stage 1). - `orphan` = subset of `missing_index_html` where `has_preview=True` (preview.png exists without index.html — stale artifact to flag). - Buckets are intentionally **non-disjoint**: `orphan` is a strict subset of `missing_index_html`. This matches the Stage 2 evidence numbers exactly (`renderable=20`, `missing_index_html=13`, `orphan=1`). Disjoint framing (e.g., orphan=1 + pure_missing=12) was rejected because Stage 2 evidence reports `missing_index_html=13` (inclusive), not 12. - Docstring explicitly states the non-disjoint relationship and cites the Stage 2 evidence counts so u6 / future readers don't accidentally re-sum to 34. - Module docstring updated: `(u1-u4; orphan/manifest added in u5-u6)` → `(u1-u5; manifest added in u6)`. - **No wiring into `main()`** per Stage 2 unit boundary — `categorize()` is provided in isolation. u6 will load the manifest, call `categorize()` + `is_unchanged()` per row, write back per-row status, and emit the per-status stdout coverage report. - `Dict` was already imported in u4. **Zero new imports** added by u5. Selenium remains lazy (u2/u3 contract preserved). - `FrameRow` / `discover` / `_build_driver` / `render_one` / `_sha256_file` / `is_unchanged` / `main` / CLI **untouched**. u1/u2/u3/u4 contracts preserved. **verification**: - `(Get-Content scripts/generate_frame_previews.py | Measure-Object -Line).Lines` → **121** (u5 add = 11 lines, ≤ 35 guardrail). - `python scripts/generate_frame_previews.py --dry-run` → `discovered: total=33 with_index_html=20 with_preview_png=4` (unchanged from u1-u4 baseline at commit `56619a0` — u5 does not alter discovery output and does not wire into main). - `$env:PYTHONPATH = '...\\scripts'; python -c "import generate_frame_previews as m; print('import_ok', hasattr(m,'categorize'), hasattr(m,'is_unchanged'), hasattr(m,'render_one'), hasattr(m,'_build_driver'), hasattr(m,'discover'), hasattr(m,'main'))"` → `import_ok True True True True True True` (lazy-selenium import preserved — module imports without selenium installed; u7 smoke-test path intact). - Live categorize against real `figma_to_html_agent/blocks/`: ``` rows = discover(DEFAULT_BLOCKS_DIR); buckets = categorize(rows) → total=33 renderable=20 missing_index_html=13 orphan=1 → orphan_ids=['1171281192'] ``` Matches Stage 2 evidence verbatim (`orphan=1 (1171281192)`, `missing_index_html=13`, `total=33`, `renderable=20=with_index_html`). - Forbidden-pattern audit `rg -n "^(import|from) |anthropic|openai|jinja|phase_z2|slide_measurer|1280|720|1400|900"` → only stdlib imports (`argparse`, `hashlib`, `sys`, `dataclasses`, `pathlib`, `typing`). No new AI / Jinja / Phase Z runtime / `slide_measurer` / hardcoded slide-px references introduced by u5. - Scope-guard `git diff --name-only -- src/slide_measurer.py src/phase_z2_pipeline.py Front/vite.config.ts templates/phase_z2/frames tests figma_to_html_agent/blocks/_preview_manifest.json` → empty (no tracked-file changes outside `scripts/generate_frame_previews.py`). - `Test-Path 'figma_to_html_agent/blocks/_preview_manifest.json'` → still False (expected; manifest write deferred to u6). **remaining_units**: [u6, u7] **follow_up_issue_candidates**: none from this unit. The 13 `missing_index_html` frames remain an IMP-04 axis (already flagged in Stage 1 out-of-scope). Orphan cleanup (deleting `1171281192/preview.png`) is *not* in IMP-13 scope — u5 only flags it via the manifest in u6; deletion would belong to a separate cleanup axis. rewind_target: continue_same""" payload = json.dumps({"body": body}).encode("utf-8") req = urllib.request.Request( "https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/13/comments", data=payload, headers={ "Authorization": f"token {os.environ['GITEA_TOKEN']}", "Content-Type": "application/json", }, method="POST", ) with urllib.request.urlopen(req) as resp: data = json.loads(resp.read()) print("posted_id", data.get("id"), "created_at", data.get("created_at"))