Files
C.E.L_Slide_test2/.orchestrator/tmp/post_u6.py
T

69 lines
7.3 KiB
Python

import os, sys, requests
body = r"""Executing unit: u6
[Claude #9]
**unit_executed**: u6 — manifest writer (`_preview_manifest.json`, schema v1) + stdout coverage report + exit code policy
**files_changed**:
- MOD `D:\ad-hoc\kei\design_agent\scripts\generate_frame_previews.py` (121 → 175 lines per `Measure-Object -Line`; u6 net add = 34 lines, ≤ Stage 2 u6 estimate of 50)
**diff_summary**:
- Added `_load_manifest(path)` helper: returns `{}` when file is absent / non-JSON / non-dict; never raises. Stdlib-only (`json.loads`).
- Added `_render_entry(row, w, h)` helper: returns the schema-v1 per-frame payload `{status:"rendered", index_sha256, index_mtime, preview_mtime, viewport:{w,h}}`. Uses `_sha256_file()` (u4) for hash; reads mtime on the fresh artefacts so the next run's `is_unchanged()` (u4) can hit on byte-identical state.
- Wired `main()` end-to-end (replaces the previous `dry_run`-or-`return 0` stub):
1. **Dry-run path preserved** — added an explicit `return 0` so dry-run still emits exactly `discovered: total=N with_index_html=N with_preview_png=N` and exits 0 with no manifest write (regression guard for u1 contract).
2. **Stale-detect short-circuit**: `prev_frames = _load_manifest(args.manifest).get("frames") or {}`; per-row `is_unchanged(r, last)` check (u4). When True → carry forward the prior entry with `status` overridden to `"skipped_unchanged"`; preserves `index_sha256`/`index_mtime`/`preview_mtime`/`viewport` from the prior run, so re-runs are byte-identical at the per-frame entry level (Stage 1 guardrail "Idempotent: re-run = byte-identical output for unchanged index.html").
3. **Lazy driver build**: `driver = None`; `_build_driver()` (u2) is called only inside the loop the first time a renderable row needs rendering. This means a fully-cached re-run NEVER invokes Selenium → no chromedriver dependency for the steady state.
4. **Render-or-error per row**: `render_one(driver, r)` (u3) → `_render_entry(...)` on success → `counts["rendered"] += 1`; on `Exception` → record `{"status":"error","error":str(exc)}` and `counts["error"] += 1`. Failure is recorded per row; the loop never aborts mid-pass.
5. **Driver cleanup**: `try / finally` quits the driver only if it was built. `driver.quit()` itself is wrapped in `try/except` so a hung driver during shutdown doesn't mask the render outcome.
6. **Orphan + missing categorization**: after the render loop, iterate `buckets["missing_index_html"]` (u5) and label each row `"orphan"` (subset, has preview.png without index.html) or `"missing_index_html"` (no preview either). These are **disjoint per-frame statuses** so the `frames` map is unambiguous, while `summary["missing_index_html"]=13` and `summary["orphan"]=1` preserve the **non-disjoint bucket counts** documented in u5 / Stage 2 evidence.
7. **Manifest writer (schema v1)**: writes `{"schema":1, "generated_at":<ISO>, "blocks_dir":<path>, "summary":{...}, "frames":{...}}` via `json.dumps(..., indent=2, sort_keys=True)`. Sorted keys → deterministic ordering for the `frames` map. `args.manifest.write_text(..., encoding="utf-8")`.
8. **Coverage stdout** (per-status counts): `coverage: total=N renderable=N rendered=N skipped_unchanged=N missing_index_html=N orphan=N error=N`. Single line; matches the Stage 1 "Failure-loud: missing index.html listed in coverage report" guardrail.
9. **Exit code policy**: `return 1 if counts["error"] else 0`. missing_index_html / orphan are **categorized, not failed** (per Stage 1 — they belong to the IMP-04 follow-up axis, not IMP-13). Only an actual render failure forces a non-zero exit.
- Imports updated: `import argparse, hashlib, json, sys` (added `json`); new line `from datetime import datetime, timezone` for the `generated_at` ISO-8601 UTC timestamp. Module docstring `(u1-u5; manifest added in u6)` → `(u1-u6)`.
- u1 (`discover` / `FrameRow` / CLI), u2 (`_build_driver`), u3 (`render_one`), u4 (`_sha256_file` / `is_unchanged`), u5 (`categorize`) **untouched**. All accepted contracts preserved.
**verification**:
- Line count: `(Get-Content scripts/generate_frame_previews.py | Measure-Object -Line).Lines` → **175** (u6 add = 34 lines, ≤ Stage 2 estimate of 50; well within budget).
- **Dry-run regression guard**: `python scripts/generate_frame_previews.py --dry-run` → `discovered: total=33 with_index_html=20 with_preview_png=4` (unchanged from u1-u5 baseline at commit `56619a0` — u6 added the explicit early `return 0` so dry-run is byte-identical with prior units).
- **Module-import smoke** (lazy-selenium contract preserved from u2/u3): `$env:PYTHONPATH = '...\scripts'; python -c "import generate_frame_previews as m; print('import_ok', hasattr(m,'_load_manifest'), hasattr(m,'_render_entry'), 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 True True`. Selenium NOT required at import time → u7 smoke-test path remains open.
- **End-to-end manifest write (empty blocks-dir)**:
```
python scripts/generate_frame_previews.py --blocks-dir <tmp_empty> --manifest <tmp>/_preview_manifest.json
→ coverage: total=0 renderable=0 rendered=0 skipped_unchanged=0 missing_index_html=0 orphan=0 error=0
→ exit=0
→ manifest content: schema=1, generated_at=ISO, blocks_dir=<path>, summary={all zeros}, frames={}
```
Schema-v1 well-formed JSON. No Selenium invoked (no renderable rows).
- **End-to-end categorization smoke** (orphan + missing, no Selenium):
```
blocks_dir = {frame_orphan/preview.png exists, no index.html; frame_missing/<empty>}
→ coverage: total=2 renderable=0 rendered=0 skipped_unchanged=0 missing_index_html=2 orphan=1 error=0
→ exit=0
→ frames: {"frame_orphan":{"status":"orphan","has_preview":true},
"frame_missing":{"status":"missing_index_html","has_preview":false}}
→ summary: {"missing_index_html":2, "orphan":1, ...} (orphan subset of missing per u5 contract)
```
Per-frame statuses disjoint; summary counts non-disjoint matching u5 categorize() contract.
- **Forbidden-pattern audit**: `rg -n "^(import|from) |anthropic|openai|jinja|phase_z2|slide_measurer|1280|720|1400|900" scripts/generate_frame_previews.py` → only stdlib imports (`argparse, hashlib, json, sys, dataclasses, datetime, pathlib, typing`). No AI / Jinja / Phase Z runtime / slide_measurer imports. No `1280`/`720`/`1400`/`900` literal.
- **Scope guard**: `git diff --name-only -- src/slide_measurer.py src/phase_z2_pipeline.py Front/vite.config.ts templates/phase_z2/frames tests` → empty (no tracked-file edits outside scripts/). Only `scripts/generate_frame_previews.py` is touched by this turn.
**remaining_units**: [u7]
**follow_up_issue_candidates**: none from this unit. (Stage 1 already flagged the 13 `missing_index_html` frames as the IMP-04 catalog gap and `data/figma_previews/index.json` corruption as separate axes — both excluded from IMP-13 scope.)
rewind_target: continue_same
"""
token = os.environ.get("GITEA_TOKEN")
if not token:
print("ERROR: GITEA_TOKEN env var missing", file=sys.stderr)
sys.exit(1)
url = "https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/13/comments"
r = requests.post(url, headers={"Authorization": f"token {token}"}, json={"body": body}, timeout=30)
print(r.status_code)
print(r.json().get("html_url") if r.status_code < 300 else r.text)