V4 signal = restructure 1 (4 MDX sample). Catalog-completeness activation —
Codex round 51 guardrail (per-frame source-evidence check, not blind F16
reuse). Source confirmed as 2x2 paired-rows BIM problem layout, distinct
from F16's quadrant-4 framing.
3-layer architecture (matrix §0) :
- V4 = matching authority — restructure tier signal; runtime activation
prepares Phase Z to assemble this frame when V4 ranks it.
- figma_to_html (1171281194) = source/evidence — 4 BIM problem cards in
2x2 grid (개념 부재 / 잘못된 접근방식 / 방향성 상실 / 전제조건 오류).
- Phase Z = runtime — adds catalog + partial + smoke fixture.
Builder reuse :
- `quadrant_flat_slots` reused with pad_to=4 + `issue_{n}_label/body` keys.
- `quadrant_item` parser reused.
- F16 quadrant pattern reused, but the partial is a 2-row × 2-column
problem-theme grid (red/orange/amber/deep-amber), not the F16 TL/TR/BL/BR
quadrant visual. Source-evidence-driven decision per Codex round 51 §10.
3 file changes :
1. templates/phase_z2/families/bim_current_problems_paired.html
- 2x2 CSS grid with per-issue problem theme.
- PROMOTED CSS : per-cell warning gradient (red/orange/amber/deep-amber),
title gradient (zone-title family), "!" bullet markers in per-cell color.
- NOT PROMOTED : Figma source banner / numbered badges / texture —
figma_to_html source evidence preserved for future fidelity review.
- ADAPTED : Figma absolute positioning → CSS grid 2x2, token-fixed
typography.
2. templates/phase_z2/catalog/frame_contracts.yaml — F17 contract appended
- frame_id=1171281194, family=cards, source_shape=top_bullets, strict 4,
role_order=[issue_1..issue_4].
- visual_hints.min_height_px = 350 (F16/F14 class — 2-row × 2-col density).
- accepted_content_types = [text_block].
- payload.builder = quadrant_flat_slots reuse with issue_{n}_* keys.
3. scripts/smoke_frame_render.py — bundled fixture for F17.
Verification :
- python scripts/smoke_frame_render.py --self-check : PASS 11/11 (F17 at
3856 chars CSS-only)
- python scripts/smoke_frame_render.py bim_current_problems_paired
--render-to data/runs/imp04_f17_visual : PASS, 0 raster refs
- python run_mdx03_pipeline.py --phase-z2 --run-id imp04_f17_regression :
PASS (MDX 03 V4 rank-1 unchanged; F17 not in MDX 03 V4 selection so
this is non-impacting regression check)
scope-lock honored : V4 logic / mapper / production render / Phase R' /
AI/Kei / 10 existing partials all unchanged.
4-class status :
- class 1 readiness : ✅
- class 2 content-fit : watch — paragraph-heavy source (each issue body
is multi-line Korean text). Compact 2x2 cell may need wrap. max-content
fit checked via R3 artifact.
- class 3 : N/A
- class 4 : N/A
Refs Gitea #4 (IMP-04 Track A frame 8 — V4 RS tier, source-evidence-confirmed)
534 lines
21 KiB
Python
534 lines
21 KiB
Python
"""IMP-04 per-frame Jinja smoke harness (StrictUndefined).
|
|
|
|
scope-lock 16 조건 (Gitea #4) §11 / §13 :
|
|
- smoke = isolated Jinja partial render with StrictUndefined
|
|
- production render path (`phase_z2_pipeline.render_slide`) 미변경 — 본 harness 만 strict
|
|
- builder output keys ↔ partial Jinja variables 정합 확인이 본 harness 의 목적
|
|
- asset path / undefined variable / template syntax 실패 시 즉시 error
|
|
|
|
Usage :
|
|
# sanity check existing frames (mock payloads bundled below)
|
|
python scripts/smoke_frame_render.py --self-check
|
|
|
|
# check a specific template with mock payload from stdin
|
|
python scripts/smoke_frame_render.py three_parallel_requirements < mock.json
|
|
|
|
# check all template_ids in `templates/phase_z2/families/` against bundled mocks
|
|
python scripts/smoke_frame_render.py --self-check
|
|
|
|
Exit codes :
|
|
0 = all renders succeeded (StrictUndefined passed)
|
|
1 = at least one render failed
|
|
2 = invalid input (template_id 미존재 등)
|
|
|
|
본 harness 는 IMP-04 의 per-frame 6-step gate Step 5 의 자동 실행 단위.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from jinja2 import (
|
|
Environment,
|
|
FileSystemLoader,
|
|
StrictUndefined,
|
|
TemplateError,
|
|
UndefinedError,
|
|
select_autoescape,
|
|
)
|
|
|
|
PROJECT_ROOT = Path(__file__).parent.parent
|
|
TEMPLATE_DIR = PROJECT_ROOT / "templates" / "phase_z2"
|
|
FAMILIES_DIR = TEMPLATE_DIR / "families"
|
|
|
|
|
|
# ─── Mock payloads (existing 3 frames — sanity baseline) ────────
|
|
|
|
# mock = real builder output shape (refer mapper.py builders).
|
|
# StrictUndefined requires every referenced attribute to exist; optional fields
|
|
# must be present with empty/None values so `{% if x %}` evaluates falsy.
|
|
_MOCK_THREE_PARALLEL = {
|
|
"title": "1. DX 시행을 위한 필수 요건",
|
|
"pillars": [
|
|
{
|
|
"label": "기술 (Technology)",
|
|
"label_main": "기술",
|
|
"label_paren": "Technology",
|
|
"color_class": "tech",
|
|
"sections": [
|
|
{
|
|
"heading": "디지털 도구",
|
|
"text_lines": [
|
|
{"text": "BIM 활용", "indent": 0},
|
|
{"text": "클라우드 협업", "indent": 0},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
"label": "사람 (People)",
|
|
"label_main": "사람",
|
|
"label_paren": "People",
|
|
"color_class": "people",
|
|
"sections": [
|
|
{
|
|
"heading": "역량 강화",
|
|
"text_lines": [
|
|
{"text": "전문가 양성", "indent": 0},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
"label": "자연 (Nature)",
|
|
"label_main": "자연",
|
|
"label_paren": "Nature",
|
|
"color_class": "nature",
|
|
"sections": [
|
|
{
|
|
"heading": "환경 적응",
|
|
"text_lines": [
|
|
{"text": "지역적 제약 고려", "indent": 0},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}
|
|
|
|
_MOCK_PROCESS_PRODUCT = {
|
|
"title": "2. Process의 혁신과 Product의 변화",
|
|
"banner_left": "Process",
|
|
"banner_right": "Product",
|
|
"process": {
|
|
"sections": [
|
|
{"title": "과정 1", "transforms": [], "text_lines": [{"text": "AS-IS 정리", "indent": 0}]},
|
|
{"title": "과정 2", "transforms": [], "text_lines": [{"text": "TO-BE 전환", "indent": 0}]},
|
|
{"title": "과정 3", "transforms": [], "text_lines": [{"text": "검증 단계", "indent": 0}]},
|
|
],
|
|
},
|
|
"product": {
|
|
"sections": [
|
|
{"title": "결과 1", "transforms": [], "text_lines": [{"text": "BIM 모델", "indent": 0}], "footnote": "", "sub_title": ""},
|
|
{"title": "결과 2", "transforms": [], "text_lines": [{"text": "통합 협업", "indent": 0}], "footnote": "", "sub_title": ""},
|
|
{"title": "결과 3", "transforms": [], "text_lines": [{"text": "실시간 검증", "indent": 0}], "footnote": "", "sub_title": ""},
|
|
],
|
|
},
|
|
}
|
|
|
|
_MOCK_QUADRANT = {
|
|
"title": "BIM 도입 4 문제",
|
|
"center_quote": "",
|
|
"quadrant_1_label": "기술 부족",
|
|
"quadrant_1_headline": "",
|
|
"quadrant_1_body": [{"text": "디지털 도구 미숙", "indent": 0}],
|
|
"quadrant_2_label": "인력 부족",
|
|
"quadrant_2_headline": "",
|
|
"quadrant_2_body": [{"text": "전문가 부재", "indent": 0}],
|
|
"quadrant_3_label": "프로세스",
|
|
"quadrant_3_headline": "",
|
|
"quadrant_3_body": [{"text": "업무 흐름 단절", "indent": 0}],
|
|
"quadrant_4_label": "표준화",
|
|
"quadrant_4_headline": "",
|
|
"quadrant_4_body": [{"text": "가이드 부재", "indent": 0}],
|
|
}
|
|
|
|
# IMP-04 frame 1 — three_persona_benefits (frame 14, frame_id=1171281191).
|
|
# Builder = items_with_role + quadrant_item parser → persona dict = {label, body, color_class}.
|
|
_MOCK_THREE_PERSONA_BENEFITS = {
|
|
"title": "주체별 기대효과",
|
|
"personas": [
|
|
{
|
|
"label": "발주자",
|
|
"color_class": "client",
|
|
"body": [
|
|
{"text": "민원, 재 작업 등의 예방 및 최소화", "indent": 0},
|
|
{"text": "직관화로 품질 향상 및 안정성 제고", "indent": 0},
|
|
{"text": "수행공정의 쉬운이해로 관리 편의성 증진", "indent": 0},
|
|
{"text": "실무자와 발주자간의 소통 오류 최소화", "indent": 0},
|
|
],
|
|
},
|
|
{
|
|
"label": "시공자",
|
|
"color_class": "constructor",
|
|
"body": [
|
|
{"text": "시공 오류예방 및 공사 Risk 최소화", "indent": 0},
|
|
{"text": "시각화로 안전성 제고 및 품질 향상", "indent": 0},
|
|
{"text": "건설 관계자들 간의 의사소통 강화", "indent": 0},
|
|
],
|
|
},
|
|
{
|
|
"label": "설계자",
|
|
"color_class": "designer",
|
|
"body": [
|
|
{"text": "직관적 시각화로 원활한 소통", "indent": 0},
|
|
{"text": "3D 모델 활용으로 오류 최소화", "indent": 0},
|
|
{"text": "발주자와의 상호 신뢰 증진", "indent": 0},
|
|
],
|
|
},
|
|
],
|
|
}
|
|
|
|
# Track A frame 2 — construction_goals_three_circle_intersection (frame 12).
|
|
# Builder = cycle_intersect_3 + quadrant_item parser (label only).
|
|
# slot_payload : title, circle_1_label, circle_2_label, circle_3_label, intersection.
|
|
_MOCK_CONSTRUCTION_GOALS = {
|
|
"title": "건설산업의 목표 (BIM 의 목적)",
|
|
"circle_1_label": "안전과 품질",
|
|
"circle_2_label": "생산성 향상",
|
|
"circle_3_label": "소통과 신뢰",
|
|
"intersection": "3요소가 조화를 이룰 때 BIM 의 궁극적 목표 달성",
|
|
}
|
|
|
|
# Track A frame 3 — construction_bim_three_usage (frame 11).
|
|
# Builder = quadrant_flat_slots reuse (pad_to=3, category_N_label/body keys).
|
|
_MOCK_CONSTRUCTION_BIM_USAGE = {
|
|
"title": "시공단계 BIM 모델·정보 활용 구분",
|
|
"category_1_label": "모델기반",
|
|
"category_1_body": [
|
|
{"text": "최종 목적물의 3D 형상정보 활용", "indent": 0},
|
|
],
|
|
"category_2_label": "객체기반",
|
|
"category_2_body": [
|
|
{"text": "Model 개별 객체의 건설정보 활용", "indent": 0},
|
|
],
|
|
"category_3_label": "위치기반",
|
|
"category_3_body": [
|
|
{"text": "공사 중 위치정보 활용", "indent": 0},
|
|
],
|
|
}
|
|
|
|
# Track A frame 4 — bim_dx_comparison_table (frame 18).
|
|
# NEW builder = compare_table_2col + NEW parser = compare_row_2col_item.
|
|
# slot_payload : title, col_a_label, col_b_label, rows=[{label, col_a, col_b}].
|
|
_MOCK_BIM_DX_COMPARISON = {
|
|
"title": "BIM 과 DX 의 이해",
|
|
"col_a_label": "BIM",
|
|
"col_b_label": "DX",
|
|
"rows": [
|
|
{"label": "범위", "col_a": "Only 3D", "col_b": "BIM << DX (ENG. + Mgmt 포함)"},
|
|
{"label": "S/W", "col_a": "상용 S/W (Revit 등)", "col_b": "상용 + 전용 40~80개"},
|
|
{"label": "프로세스", "col_a": "기존 2D 설계방식 유지", "col_b": "근본적 문제의식 통한 개선"},
|
|
{"label": "성과물", "col_a": "3D 모델 중심", "col_b": "공학 정보 + 콘텐츠 연계"},
|
|
{"label": "활용", "col_a": "분야별 단절", "col_b": "전 생애주기 활용 시스템"},
|
|
{"label": "수행개념", "col_a": "수동적 / 집단적", "col_b": "적극·구체적 실현 방안"},
|
|
],
|
|
}
|
|
|
|
# Track A frame 5 — dx_sw_necessity_three_perspectives (frame 20).
|
|
# Builder reuse = quadrant_flat_slots (F11 pattern) — pad_to=3, perspective_N keys.
|
|
_MOCK_DX_SW_NECESSITY = {
|
|
"title": "디지털 전환(DX)은 S/W가 필수다",
|
|
"perspective_1_label": "BIM 전면설계",
|
|
"perspective_1_body": [
|
|
{"text": "건설산업 생산성 향상", "indent": 0},
|
|
{"text": "고부가가치 산업 전환", "indent": 0},
|
|
],
|
|
"perspective_2_label": "디지털 전환 S/W",
|
|
"perspective_2_body": [
|
|
{"text": "노동집약형 업무 탈피", "indent": 0},
|
|
{"text": "S/W 고도화 + 투자 필요", "indent": 0},
|
|
],
|
|
"perspective_3_label": "고부가가치 산업전환",
|
|
"perspective_3_body": [
|
|
{"text": "기본기술 이해 발전 필요", "indent": 0},
|
|
{"text": "DX 통한 Process 혁신", "indent": 0},
|
|
],
|
|
}
|
|
|
|
# Track A frame 6 — info_management_what_how_when (frame 8).
|
|
# V4-zero catalog-completeness activation (Codex round 47 guardrail).
|
|
# Builder reuse = quadrant_flat_slots (F11/F20 pattern) — section_N keys.
|
|
_MOCK_INFO_MGMT = {
|
|
"title": "효율적인 정보의 관리와 활용 (What/How/When)",
|
|
"section_1_label": "무슨 정보 (What)",
|
|
"section_1_body": [
|
|
{"text": "수량 / 단가 / 공사일정 등 계획 정보", "indent": 0},
|
|
{"text": "일일 작업 / 자원 등 공사 실행 정보", "indent": 0},
|
|
],
|
|
"section_2_label": "어떻게 연계 (How)",
|
|
"section_2_body": [
|
|
{"text": "3D 형상 산출속성 연계", "indent": 0},
|
|
{"text": "시방규정 + S/W 통합", "indent": 0},
|
|
],
|
|
"section_3_label": "언제 사용 (When)",
|
|
"section_3_body": [
|
|
{"text": "착수 전 공정/시공계획 수립", "indent": 0},
|
|
{"text": "공사 후 실적 관리 + 문서 작성", "indent": 0},
|
|
],
|
|
}
|
|
|
|
SELF_CHECK_FIXTURES: dict[str, dict] = {
|
|
"three_parallel_requirements": _MOCK_THREE_PARALLEL,
|
|
"process_product_two_way": _MOCK_PROCESS_PRODUCT,
|
|
"bim_issues_quadrant_four": _MOCK_QUADRANT,
|
|
"three_persona_benefits": _MOCK_THREE_PERSONA_BENEFITS,
|
|
"construction_goals_three_circle_intersection": _MOCK_CONSTRUCTION_GOALS,
|
|
"construction_bim_three_usage": _MOCK_CONSTRUCTION_BIM_USAGE,
|
|
"bim_dx_comparison_table": _MOCK_BIM_DX_COMPARISON,
|
|
"dx_sw_necessity_three_perspectives": _MOCK_DX_SW_NECESSITY,
|
|
"info_management_what_how_when": _MOCK_INFO_MGMT,
|
|
"sw_reality_three_emphasis": {
|
|
"title": "현존 상용 S/W 의 현실",
|
|
"emphasis_1_label": "토목 전문성 부족",
|
|
"emphasis_1_body": [{"text": "건축용 S/W 일부 수정 적용", "indent": 0}],
|
|
"emphasis_2_label": "비효율성",
|
|
"emphasis_2_body": [{"text": "범용 개발 + 전문가용 한계", "indent": 0}],
|
|
"emphasis_3_label": "실무 적용 불가",
|
|
"emphasis_3_body": [{"text": "특수성 반영 어려움", "indent": 0}],
|
|
},
|
|
"bim_current_problems_paired": {
|
|
"title": "현황 및 문제점",
|
|
"issue_1_label": "개념 부재",
|
|
"issue_1_body": [{"text": "CAD 확장판 오인", "indent": 0}],
|
|
"issue_2_label": "잘못된 접근방식",
|
|
"issue_2_body": [{"text": "도구로만 인식", "indent": 0}],
|
|
"issue_3_label": "방향성 상실",
|
|
"issue_3_body": [{"text": "대형 S/W 의존", "indent": 0}],
|
|
"issue_4_label": "전제조건 오류",
|
|
"issue_4_body": [{"text": "건축·토목 혼용", "indent": 0}],
|
|
},
|
|
}
|
|
|
|
|
|
# ─── Core ───────────────────────────────────────────────────────
|
|
|
|
|
|
def _make_env() -> Environment:
|
|
"""StrictUndefined Jinja env — production render path 와 동등 loader,
|
|
단 undefined behavior 만 strict.
|
|
"""
|
|
return Environment(
|
|
loader=FileSystemLoader(str(TEMPLATE_DIR)),
|
|
autoescape=select_autoescape(["html"]),
|
|
undefined=StrictUndefined,
|
|
)
|
|
|
|
|
|
def smoke_render(template_id: str, slot_payload: dict) -> tuple[bool, str]:
|
|
"""Isolated StrictUndefined Jinja render.
|
|
|
|
Returns (ok, html_or_error_text).
|
|
"""
|
|
env = _make_env()
|
|
try:
|
|
partial = env.get_template(f"families/{template_id}.html")
|
|
except Exception as exc: # noqa: BLE001 — surface any loader error
|
|
return False, f"TemplateLoad: {exc!r}"
|
|
|
|
try:
|
|
html = partial.render(slot_payload=slot_payload)
|
|
except UndefinedError as exc:
|
|
return False, f"UndefinedError (missing variable): {exc}"
|
|
except TemplateError as exc:
|
|
return False, f"TemplateError: {exc}"
|
|
except Exception as exc: # noqa: BLE001
|
|
return False, f"RenderError: {exc!r}"
|
|
return True, html
|
|
|
|
|
|
def list_existing_partials() -> list[str]:
|
|
"""Return template_id list (filename stems without .html) under families/."""
|
|
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}/<filename>` 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/<template_id>/* 복사
|
|
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"""<!DOCTYPE html>
|
|
<html lang="ko"><head>
|
|
<meta charset="UTF-8">
|
|
<title>Phase Z render artifact — {template_id}</title>
|
|
<style>
|
|
body {{ margin: 0; padding: 20px; background: #e8ecf0;
|
|
font-family: 'Noto Sans KR', sans-serif; word-break: keep-all; }}
|
|
.viewer-wrap {{ width: 1180px; max-width: 100%; margin: 0 auto;
|
|
background: #fff; box-shadow: 0 4px 20px rgba(0,0,0,.15);
|
|
padding: 40px; min-height: 350px; box-sizing: border-box; }}
|
|
.viewer-note {{ max-width: 1180px; margin: 10px auto 0; padding: 8px 12px;
|
|
background: #fffbe8; border-left: 4px solid #f5b400;
|
|
font-size: 12px; color: #5a4500; }}
|
|
/* Phase Z token CSS — minimal viewer override (production uses real tokens) */
|
|
:root {{
|
|
--font-zone-title: 28px; --lh-zone-title: 1.3;
|
|
--font-sub-title: 18px; --lh-sub-title: 1.4;
|
|
--font-caption: 11px;
|
|
--font-body: 11px; --lh-body: 1.4;
|
|
}}
|
|
</style>
|
|
</head><body>
|
|
<div class="viewer-wrap">
|
|
{html_or_err}
|
|
</div>
|
|
<div class="viewer-note">
|
|
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.
|
|
</div>
|
|
</body></html>
|
|
"""
|
|
|
|
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 ────────────────────────────────────────────────────────
|
|
|
|
|
|
def _cmd_self_check() -> int:
|
|
"""Run smoke render against every bundled fixture and report.
|
|
|
|
Exit 0 if all PASS, 1 otherwise. Frames with no fixture are SKIPPED
|
|
(reported separately so the per-frame gate is explicit).
|
|
"""
|
|
existing = list_existing_partials()
|
|
print(f"== smoke harness self-check ({len(existing)} partial(s) found) ==")
|
|
fail_count = 0
|
|
skip_count = 0
|
|
for tpl in existing:
|
|
if tpl not in SELF_CHECK_FIXTURES:
|
|
print(f" SKIP {tpl} (no bundled fixture)")
|
|
skip_count += 1
|
|
continue
|
|
ok, msg = smoke_render(tpl, SELF_CHECK_FIXTURES[tpl])
|
|
if ok:
|
|
print(f" PASS {tpl} ({len(msg)} chars)")
|
|
else:
|
|
print(f" FAIL {tpl} → {msg}")
|
|
fail_count += 1
|
|
|
|
# Also surface fixtures with no corresponding partial (catches typos)
|
|
for fixture in SELF_CHECK_FIXTURES:
|
|
if fixture not in existing:
|
|
print(f" ORPHAN {fixture} (fixture but no partial)")
|
|
fail_count += 1
|
|
|
|
print(f"-- summary: PASS={len(existing) - fail_count - skip_count} "
|
|
f"FAIL={fail_count} SKIP={skip_count} --")
|
|
return 0 if fail_count == 0 else 1
|
|
|
|
|
|
def _cmd_one(template_id: str, payload_path: Optional[Path]) -> int:
|
|
if payload_path is not None:
|
|
slot_payload = json.loads(payload_path.read_text(encoding="utf-8"))
|
|
elif not sys.stdin.isatty():
|
|
slot_payload = json.load(sys.stdin)
|
|
elif template_id in SELF_CHECK_FIXTURES:
|
|
slot_payload = SELF_CHECK_FIXTURES[template_id]
|
|
print(f"[info] using bundled fixture for {template_id}", file=sys.stderr)
|
|
else:
|
|
print(f"error: no payload provided for '{template_id}' "
|
|
f"(pipe JSON via stdin or use --payload)", file=sys.stderr)
|
|
return 2
|
|
|
|
ok, msg = smoke_render(template_id, slot_payload)
|
|
if ok:
|
|
print(f"PASS {template_id} ({len(msg)} chars rendered)", file=sys.stderr)
|
|
sys.stdout.write(msg)
|
|
return 0
|
|
print(f"FAIL {template_id} → {msg}", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
def main(argv: Optional[list[str]] = None) -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="IMP-04 per-frame Jinja smoke harness (StrictUndefined).",
|
|
)
|
|
parser.add_argument("template_id", nargs="?",
|
|
help="frame template_id (omit when --self-check).")
|
|
parser.add_argument("--self-check", action="store_true",
|
|
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:
|
|
return _cmd_self_check()
|
|
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)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|