Tighten issue gates and loop validation criteria
This commit is contained in:
@@ -22,6 +22,9 @@ from src.renderer import render_slide_from_html # type: ignore
|
||||
from src.slide_measurer import measure_rendered_heights # type: ignore
|
||||
|
||||
COMPARISON_MARKER = "comparison-summary-card"
|
||||
COMPARE_KEYS = ["범위", "프로세스", "성과품", "확장성"]
|
||||
CORE_MESSAGE_KEYS = ["DX는 상위 개념", "BIM은 핵심 기술"]
|
||||
IMAGE_REFERENCE_KEY = "DX와 핵심기술간 상호관계"
|
||||
|
||||
|
||||
def read_json(path: Path) -> dict:
|
||||
@@ -67,39 +70,63 @@ def rerender_final_html(generated: dict, context: dict) -> str:
|
||||
return render_slide_from_html(generated, analysis_dict, preset)
|
||||
|
||||
|
||||
def zone_overflow_names(measurement: dict) -> list[str]:
|
||||
zones = measurement.get("zones", {})
|
||||
names: list[str] = []
|
||||
for name in ("body", "sidebar", "footer"):
|
||||
if zones.get(name, {}).get("overflowed"):
|
||||
names.append(name)
|
||||
return names
|
||||
|
||||
|
||||
def validate_outputs(generated: dict, measurement: dict) -> tuple[str, list[str], list[str]]:
|
||||
visible = "\n".join([
|
||||
generated.get("body_html", ""),
|
||||
generated.get("sidebar_html", ""),
|
||||
generated.get("footer_html", ""),
|
||||
])
|
||||
text = strip_tags(visible)
|
||||
body_html = generated.get("body_html", "")
|
||||
sidebar_html = generated.get("sidebar_html", "")
|
||||
footer_html = generated.get("footer_html", "")
|
||||
text = strip_tags("\n".join([body_html, sidebar_html, footer_html]))
|
||||
|
||||
failures: list[str] = []
|
||||
actions: list[str] = []
|
||||
|
||||
if measurement.get("slide", {}).get("overflowed"):
|
||||
failures.append("Verify-Render")
|
||||
actions.append("overflow가 발생한 영역의 budget 또는 HTML 구조를 조정한다.")
|
||||
slide_overflow = measurement.get("slide", {}).get("overflowed")
|
||||
zone_overflows = zone_overflow_names(measurement)
|
||||
|
||||
if "DX와 핵심기술간 상호관계" not in text:
|
||||
failures.append("Verify-Preserve")
|
||||
actions.append("이미지 캡션 또는 이미지 참조 문구를 본문 가시 텍스트로 남긴다.")
|
||||
if slide_overflow:
|
||||
failures.append("Verify-RenderSlide")
|
||||
actions.append("slide 전체 overflow를 해소하도록 layout budget 또는 전체 레이아웃 구조를 조정한다.")
|
||||
|
||||
compare_keys = ["범위", "프로세스", "성과품", "확장성"]
|
||||
if not all(k in text for k in compare_keys):
|
||||
failures.append("Verify-Preserve")
|
||||
actions.append("비교 핵심 4축을 숨김 팝업이 아니라 보이는 요약 블록으로 유지한다.")
|
||||
if zone_overflows:
|
||||
failures.append("Verify-RenderZone")
|
||||
actions.append(f"overflow가 발생한 zone({', '.join(zone_overflows)})의 content budget, block 수, typography를 재조정한다.")
|
||||
|
||||
if "DX는 상위 개념" not in text and "상위 개념" not in text:
|
||||
failures.append("Verify-Purpose")
|
||||
actions.append("핵심 메시지 문장을 더 선명하게 본문 또는 footer에 유지한다.")
|
||||
if not all(key in text for key in CORE_MESSAGE_KEYS):
|
||||
failures.append("Verify-CoreMessage")
|
||||
actions.append("핵심 메시지 `DX는 상위 개념`, `BIM은 핵심 기술`을 가시 텍스트에 직접 노출한다.")
|
||||
|
||||
status = "pass" if not failures else "revise"
|
||||
return status, sorted(set(failures)), actions
|
||||
if IMAGE_REFERENCE_KEY not in text:
|
||||
failures.append("Verify-ImageRef")
|
||||
actions.append("이미지/도해 참조 문구 `DX와 핵심기술간 상호관계`를 숨김 영역이 아닌 가시 블록으로 유지한다.")
|
||||
|
||||
comparison_visible = COMPARISON_MARKER in sidebar_html and all(key in text for key in COMPARE_KEYS)
|
||||
if not comparison_visible:
|
||||
failures.append("Verify-ComparisonVisible")
|
||||
actions.append("비교 핵심 4축(범위, 프로세스, 성과품, 확장성)을 화면에 바로 보이는 요약 블록으로 강제한다.")
|
||||
|
||||
if failures:
|
||||
return "revise", sorted(set(failures)), list(dict.fromkeys(actions))
|
||||
return "pass", [], []
|
||||
|
||||
|
||||
def build_validation_markdown(run_id: str, status: str, failures: list[str], actions: list[str], measurement: dict) -> str:
|
||||
slide_overflow = measurement.get("slide", {}).get("overflowed", True)
|
||||
zones = measurement.get("zones", {})
|
||||
zone_lines = []
|
||||
for name in ("body", "sidebar", "footer"):
|
||||
zone = zones.get(name, {})
|
||||
zone_lines.append(
|
||||
f"- {name}: overflowed={zone.get('overflowed')} excess_px={zone.get('excess_px')} block_count={zone.get('block_count')}"
|
||||
)
|
||||
render_ok = (not slide_overflow) and (not zone_overflow_names(measurement))
|
||||
status_line = "통과" if status == "pass" else "재작업 필요"
|
||||
failure_lines = "\n".join(f"- {f}" for f in failures) if failures else "- 없음"
|
||||
action_lines = "\n".join(f"{i + 1}. {a}" for i, a in enumerate(actions)) if actions else "1. 없음"
|
||||
@@ -112,9 +139,13 @@ def build_validation_markdown(run_id: str, status: str, failures: list[str], act
|
||||
|
||||
## Validation Summary
|
||||
- 실행 경로 검증: 통과
|
||||
- 렌더링/측정 검증: {'통과' if not measurement.get('slide', {}).get('overflowed') else '실패'}
|
||||
- 렌더링/측정 검증: {'통과' if render_ok else '실패'}
|
||||
- 최종 품질 판정: {status_line}
|
||||
|
||||
## Render Gates
|
||||
- slide overflow: {slide_overflow}
|
||||
{chr(10).join(zone_lines)}
|
||||
|
||||
## Measurement
|
||||
```json
|
||||
{json.dumps(measurement, ensure_ascii=False, indent=2)}
|
||||
@@ -176,19 +207,79 @@ def main() -> None:
|
||||
for iteration in range(1, args.max_iterations + 1):
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(repo_root / 'scripts' / 'run_from_artifacts.py'),
|
||||
str(repo_root / "scripts" / "run_from_artifacts.py"),
|
||||
"--input", str(input_file),
|
||||
"--stage1a", str(stage1a),
|
||||
"--stage1b", str(stage1b),
|
||||
"--output-dir", str(output_dir),
|
||||
]
|
||||
subprocess.run(cmd, cwd=str(DESIGN_AGENT_ROOT), check=True)
|
||||
completed = subprocess.run(cmd, cwd=str(DESIGN_AGENT_ROOT), capture_output=True, text=True)
|
||||
|
||||
generated_path = output_dir / "generated_html.json"
|
||||
context_path = output_dir / "context.json"
|
||||
final_html_path = output_dir / "final.html"
|
||||
measurement_path = output_dir / "measurement.json"
|
||||
|
||||
if completed.returncode != 0:
|
||||
status = "fail"
|
||||
failures = ["Exec-Exit"]
|
||||
actions = ["실패한 stage와 stderr를 확인하고 해당 stage부터 재실행한다."]
|
||||
validation_path.write_text(
|
||||
build_validation_markdown(args.run_id, "revise", ["Exec-Exit"], actions, {"slide": {"overflowed": True}, "zones": {}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
step5_body = f"""실행 요약
|
||||
- auto_loop_runner.py iteration {iteration} 실행이 비정상 종료되었다.
|
||||
- 입력: `docs/{args.run_id}/01-input/{input_file.name}`
|
||||
- 종료 코드: {completed.returncode}
|
||||
|
||||
실행 stderr
|
||||
```text
|
||||
{completed.stderr.strip()}
|
||||
```
|
||||
|
||||
산출물 경로
|
||||
- `docs/{args.run_id}/05-execution/`
|
||||
|
||||
KPI / 판정 결과
|
||||
- 판정: fail
|
||||
- iteration: {iteration}
|
||||
- 종료 코드: {completed.returncode}
|
||||
|
||||
실패 분류
|
||||
- Exec-Exit
|
||||
|
||||
수정 액션
|
||||
- 실패한 stage와 stderr를 확인하고 해당 stage부터 재실행
|
||||
|
||||
다음 단계 전달물
|
||||
- 실행 stderr
|
||||
- 실패 시점 정보
|
||||
"""
|
||||
step6_body = f"""실행 요약
|
||||
- iteration {iteration}에서 실행 자체가 실패하여 품질 검증을 완료할 수 없었다.
|
||||
|
||||
산출물 경로
|
||||
- `docs/{args.run_id}/06-validation/validation-result.md`
|
||||
|
||||
KPI / 판정 결과
|
||||
- 판정: fail
|
||||
- 실패 분류: Exec-Exit
|
||||
|
||||
수정 액션
|
||||
- 실패한 stage와 stderr를 확인하고 해당 stage부터 재실행
|
||||
|
||||
다음 단계 전달물
|
||||
- 실행 stderr
|
||||
- 실패 시점 정보
|
||||
- 다음 iteration 여부: 재실행
|
||||
"""
|
||||
write_step_comment(step_comment_bodies[5], step5_body)
|
||||
write_step_comment(step_comment_bodies[6], step6_body)
|
||||
post_comment_if_configured(args.repo_slug, issue_numbers[4], step_comment_bodies[5])
|
||||
post_comment_if_configured(args.repo_slug, issue_numbers[5], step_comment_bodies[6])
|
||||
continue
|
||||
|
||||
generated = read_json(generated_path)
|
||||
context = read_json(context_path)
|
||||
changed = inject_visible_comparison_summary(generated)
|
||||
@@ -204,6 +295,11 @@ def main() -> None:
|
||||
status, failures, actions = validate_outputs(generated, measurement)
|
||||
validation_path.write_text(build_validation_markdown(args.run_id, status, failures, actions, measurement), encoding="utf-8")
|
||||
|
||||
zone_names = zone_overflow_names(measurement)
|
||||
critical_outputs_ok = all(path.exists() and path.stat().st_size > 0 for path in [final_html_path, generated_path, measurement_path, context_path])
|
||||
step5_status = "pass" if completed.returncode == 0 and critical_outputs_ok else "fail"
|
||||
step5_failures = "없음" if step5_status == "pass" else "Exec-Artifact"
|
||||
step5_actions = "- 없음" if step5_status == "pass" else "- 필수 산출물 4종과 저장 경로를 재확인하고 재생성"
|
||||
step5_body = f"""실행 요약
|
||||
- auto_loop_runner.py iteration {iteration}로 실행했다.
|
||||
- 입력: `docs/{args.run_id}/01-input/{input_file.name}`
|
||||
@@ -217,22 +313,26 @@ def main() -> None:
|
||||
- `docs/{args.run_id}/05-execution/context.json`
|
||||
|
||||
KPI / 판정 결과
|
||||
- 판정: pass
|
||||
- 판정: {step5_status}
|
||||
- iteration: {iteration}
|
||||
- 종료 코드: {completed.returncode}
|
||||
- 필수 산출물 4종 유효 여부: {critical_outputs_ok}
|
||||
|
||||
실패 분류
|
||||
- 없음
|
||||
- {step5_failures}
|
||||
|
||||
수정 액션
|
||||
- 없음
|
||||
{step5_actions}
|
||||
|
||||
다음 단계 전달물
|
||||
- 최신 실행 산출물
|
||||
- 최신 measurement
|
||||
- 최신 context
|
||||
"""
|
||||
step6_body = f"""실행 요약
|
||||
- iteration {iteration} 기준으로 최종 산출물과 측정 결과를 다시 검증했다.
|
||||
- 이미지 캡션 보존과 비교 핵심 4축의 가시 보존 여부를 확인했다.
|
||||
- slide overflow: {measurement.get('slide', {}).get('overflowed')}
|
||||
- zone overflow: {', '.join(zone_names) if zone_names else '없음'}
|
||||
- 최종 판정은 `{status}`이다.
|
||||
|
||||
산출물 경로
|
||||
@@ -267,6 +367,3 @@ KPI / 판정 결과
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user