From 834ed3946d62fdc11866f2cab6e09db1b73e61c9 Mon Sep 17 00:00:00 2001 From: kyeongmin Date: Wed, 13 May 2026 11:07:37 +0900 Subject: [PATCH] test(IMP-04): add F14 render artifact check and fix min-height note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3rd commit in F14 series (calibration point clean pass). Closes the two Codex round 26 (#15435) blockers: 1. min_height_px self-contradiction 2. F14 actual rendered visual artifact absent Per Codex round 28 (#15447) agreement (M1 + --render-to extension) and Claude round 27 (#15438) fix path : Changes : 1. templates/phase_z2/catalog/frame_contracts.yaml — min_height_px 320 → 350. Comment now self-consistent : 70 (badge raster) + 210 (bullet body) + 36 (photo strip) + 30 (padding) = 346 sum + 4 safety buffer = 350. F14 is now F29-class (345) per raster-promoted content density. 2. scripts/smoke_frame_render.py — add `--render-to DIR` dev mode (R3 acceptance gate). Behavior : - StrictUndefined smoke render (unchanged) - reuse production `copy_assets(template_id, run_dir)` so the runtime asset delivery path is exercised (no logic duplication) - wrap partial with minimal viewer HTML (Phase Z token vars + slide- sized wrap, browser-openable) - fail-fast if rendered HTML references a missing local asset (per Codex round 28 §4 recommendation) - save artifact to {DIR}/index.html with {DIR}/assets/{template_id}/* - production render path (phase_z2_pipeline.render_slide) unchanged - small regex fix : asset extraction now captures both `src="..."` and `url("...")` references F14 verification (3rd commit) : - python -m py_compile scripts/smoke_frame_render.py : PASS - python scripts/smoke_frame_render.py --self-check : PASS 4/4 (7446 chars for persona unchanged from a1c06b7) - python scripts/smoke_frame_render.py three_persona_benefits --render-to data/runs/imp04_f14_visual : PASS, 10 asset refs all resolved, 14 raster files copied via production copy_assets() to data/runs/imp04_f14_visual/assets/three_persona_benefits/ - R3 artifact ready for browser visual inspection at data/runs/imp04_f14_visual/index.html (Phase Z slide-sized wrapper + promoted persona partial + 10 referenced assets all on disk) F14 clean pass status : - min_height_px self-consistency : fixed (M1 = 350) - Actual rendered artifact : produced and assets resolved - Visual fidelity inspection : ready for browser/eye review - Earlier MDX02 chain attempt (commit a1c06b7 body) : superseded; MDX02 is not the F14 validation baseline (Claude round 26 / Codex round 26 agreement). MDX03 is the matched baseline; F14 visual inspection now uses the harness artifact path instead. scope-lock guardrails honored : 32-frame target, no V4 logic change, no Phase R' regression, no mapper or composition planner change, no production render path change. The new harness mode is dev verification only, isolated from runtime selection. Refs Gitea #4 (IMP-04 Track A — F14 3rd commit, clean pass gate) --- scripts/smoke_frame_render.py | 116 ++++++++++++++++++ .../phase_z2/catalog/frame_contracts.yaml | 11 +- 2 files changed, 123 insertions(+), 4 deletions(-) diff --git a/scripts/smoke_frame_render.py b/scripts/smoke_frame_render.py index 3e903fb..45b03fc 100644 --- a/scripts/smoke_frame_render.py +++ b/scripts/smoke_frame_render.py @@ -221,6 +221,95 @@ def list_existing_partials() -> list[str]: return sorted(p.stem for p in FAMILIES_DIR.glob("*.html")) +# ─── Render-to artifact (R3 acceptance gate) ──────────────────── + + +def _extract_asset_refs(html: str, template_id: str) -> list[str]: + """Return relative paths `assets/{template_id}/` referenced by HTML. + + Matches both `src="assets/..."` (img tag) and `url("assets/...")` (CSS). + """ + import re + pattern = rf'(?:src=|url\()["\']?(assets/{re.escape(template_id)}/[^"\'\)\s]+)' + return sorted(set(re.findall(pattern, html))) + + +def render_to_dir(template_id: str, slot_payload: dict, out_dir: Path) -> tuple[bool, str]: + """R3 acceptance gate — render partial + copy assets + save artifact. + + Mechanism (Codex round 28 spec) : + 1. smoke render (StrictUndefined Jinja) → HTML + 2. reuse production `copy_assets(template_id, run_dir)` — assets//* 복사 + 3. save HTML to `{out_dir}/index.html` + 4. fail if HTML references a missing local asset (post copy) + 5. production render path 미변경 + + Returns (ok, summary_or_error). + """ + ok, html_or_err = smoke_render(template_id, slot_payload) + if not ok: + return False, f"render failed: {html_or_err}" + + out_dir.mkdir(parents=True, exist_ok=True) + + # Reuse production copy_assets (no logic dup) + import sys + sys.path.insert(0, str(PROJECT_ROOT)) + from src.phase_z2_pipeline import copy_assets + assets_dst = copy_assets(template_id, out_dir) + assets_info = f"assets dir={assets_dst.relative_to(out_dir) if assets_dst else '(none)'}" + + # Verify all referenced assets exist (Codex round 28 — fail-fast missing assets) + refs = _extract_asset_refs(html_or_err, template_id) + missing = [r for r in refs if not (out_dir / r).exists()] + if missing: + return False, ( + f"missing assets (fail-fast per Codex round 28) : {len(missing)} of " + f"{len(refs)} references not resolved : {missing[:3]}{'...' if len(missing) > 3 else ''}" + ) + + # Wrap partial with minimal HTML viewer (browser-openable) + viewer = f""" + + +F14 render artifact — {template_id} + + +
+{html_or_err} +
+
+ R3 acceptance gate artifact — smoke harness `--render-to`. template_id = {template_id}. + Open this file in a browser to visually inspect rendered output + promoted assets. +
+ +""" + + out_html = out_dir / "index.html" + out_html.write_text(viewer, encoding="utf-8") + + return True, ( + f"rendered → {out_html} ({len(html_or_err)} chars partial, " + f"{len(refs)} asset refs all resolved). {assets_info}" + ) + + # ─── CLI ──────────────────────────────────────────────────────── @@ -289,6 +378,11 @@ def main(argv: Optional[list[str]] = None) -> int: help="render every bundled fixture and report.") parser.add_argument("--payload", type=Path, default=None, help="JSON file with slot_payload (else stdin or fixture).") + parser.add_argument("--render-to", type=Path, default=None, metavar="DIR", + help=( + "R3 acceptance gate — render + copy_assets + save artifact " + "to DIR/index.html. fail-fast on missing local assets." + )) args = parser.parse_args(argv) if args.self_check: @@ -296,6 +390,28 @@ def main(argv: Optional[list[str]] = None) -> int: if args.template_id is None: parser.print_usage(sys.stderr) return 2 + + # --render-to mode (R3 acceptance gate) + if args.render_to is not None: + # Determine payload (fixture preferred for render-to) + if args.payload is not None: + payload = json.loads(args.payload.read_text(encoding="utf-8")) + elif args.template_id in SELF_CHECK_FIXTURES: + payload = SELF_CHECK_FIXTURES[args.template_id] + print(f"[info] using bundled fixture for {args.template_id}", file=sys.stderr) + elif not sys.stdin.isatty(): + payload = json.load(sys.stdin) + else: + print(f"error: no payload for '{args.template_id}' " + f"(--payload or stdin or bundled fixture required)", file=sys.stderr) + return 2 + ok, msg = render_to_dir(args.template_id, payload, args.render_to) + if ok: + print(f"PASS {args.template_id} → {msg}") + return 0 + print(f"FAIL {args.template_id} → {msg}", file=sys.stderr) + return 1 + return _cmd_one(args.template_id, args.payload) diff --git a/templates/phase_z2/catalog/frame_contracts.yaml b/templates/phase_z2/catalog/frame_contracts.yaml index f1ab9d9..3cab5e8 100644 --- a/templates/phase_z2/catalog/frame_contracts.yaml +++ b/templates/phase_z2/catalog/frame_contracts.yaml @@ -206,13 +206,16 @@ three_persona_benefits: - constructor - designer - # min_height_px : derive + confirm (Codex round 13 §2.2 + round 24 asset 분류). + # min_height_px : derive + confirm (Codex round 13 §2.2 + round 24 asset 분류 + # + round 26 self-consistency catch + round 28 M1 결정). # Figma frame 1927 px @ scale 0.49213 → 948 px adapted (full frame). # Phase Z slide-body ≤ 585 px → adaptive content fit. - # Content density 2nd refinement : badge raster 70 + bullet body ~210 + photo strip 36 + padding 30 - # = 346 (F29 class). 안전 buffer 포함 = **320**. confirm via V2 MDX 02 validation. + # Content density 2nd refinement (raster promoted) : + # badge raster 70 + bullet body 210 + photo strip 36 + padding 30 = 346 sum + # + 4 safety buffer = **350** (F29 345 와 동등 class). + # confirm via smoke harness `--render-to` artifact (browser visual inspection). visual_hints: - min_height_px: 320 + min_height_px: 350 accepted_content_types: - text_block