"""arm 산출물을 arm-무관 canonical package 로 **규칙기반** 투영(LLM 요약 금지 — 그러면 judge 가 sanitizer 품질을 비교하게 된다). arm 식별 토큰은 제거하되 빈 필드는 구조 누설 방지 위해 유지한다.""" import hashlib import json import os import re import shutil import yaml from . import SANITIZER_VERSION CANON_FIELDS = [ "problem-framing", "user-and-core-task", "explored-directions", "selected-direction", "selection-rationale", "rejected-directions", "locked-invariants", "coded-prototype", "critique-findings", "revisions", "design-system-handoff-readiness", ] # 실질(비면 omission) 필드 SUBSTANTIVE = ["problem-framing", "user-and-core-task", "selected-direction", "coded-prototype"] # arm 을 누설하는 토큰(하네스 스캐폴딩) LEAK_TOKENS = [ r"\brole-id\b", r"\bmethod-execution\b", r"\bcontract-sha256\b", r"\bworkflow-id\b", r"\bactivation\b", r"\b[0-9a-f]{40}\b", r"\barm[ _-]?[ABC]\b", ] def _dig(obj, dotted): cur = obj for k in dotted.split("."): if isinstance(cur, dict) and k in cur: cur = cur[k] else: return None return cur def project(arm_artifacts_dir, extraction_map): """extraction_map: {canon_field: {file, path}}. 규칙기반 추출 — 요약/생성 없음.""" pkg = {} for f in CANON_FIELDS: pkg[f] = [] if f in ("explored-directions", "rejected-directions", "locked-invariants", "critique-findings", "revisions") else None src_count = set() projected = 0 for field, spec in (extraction_map or {}).items(): fp = os.path.join(arm_artifacts_dir, spec["file"]) if not os.path.exists(fp): continue raw = open(fp, "rb").read() sha = hashlib.sha256(raw).hexdigest() data = yaml.safe_load(raw.decode("utf-8")) val = _dig(data, spec["path"]) if val is None: continue prov = [{"artifact-ref": spec["file"], "artifact-sha256": sha, "source-fields": [spec["path"]]}] pkg[field] = {"value": val, "source-artifacts": prov} if not isinstance(pkg[field], list) else val src_count.add(spec["file"]) projected += 1 metrics = {"source-artifact-count": len(src_count), "projected-artifact-count": projected, "omitted-substantive-fields": check_omission(pkg)} return {"candidate-package": pkg, "projection-metrics": metrics, "sanitizer-version": SANITIZER_VERSION} def leak_scan(text): return [tok for tok in LEAK_TOKENS if re.search(tok, text)] def check_omission(package): out = [] for f in SUBSTANTIVE: v = package.get(f) empty = v is None or (isinstance(v, dict) and not v.get("value")) or (isinstance(v, list) and not v) if empty: out.append(f) return out def build_bundle(run_id, candidate_id, package, prototype_dir=None, render=True): """candidate 번들 조립: candidate.yaml + 렌더 png(있으면). 렌더는 preview_ui.py 산출을 복사(재생성 금지 — 결정론). prototype_dir 없거나 render=False 면 design 은 not-evaluable. judge-visible candidate.yaml 에는 candidate-package 만 쓴다(projection-metrics·sanitizer-version 같은 프로세스 메타는 judge 에게 arm 정보를 누설할 수 있어 제외). 쓰기 전 leak_scan 을 통과해야 한다 — 통과 못 하면 채점 자체를 막는다(fail-loud, spec §4.5).""" from . import paths cp = package.get("candidate-package", package) _leaks = leak_scan(yaml.safe_dump(cp, allow_unicode=True)) if _leaks: raise ValueError(f"candidate 누설 토큰 검출 — 채점 금지: {_leaks}") bdir = os.path.join(paths.candidates_dir(run_id), candidate_id) os.makedirs(bdir, exist_ok=True) with open(os.path.join(bdir, "candidate.yaml"), "w", encoding="utf-8") as f: yaml.safe_dump(cp, f, allow_unicode=True, sort_keys=False) renders = [] if render and prototype_dir and os.path.isdir(prototype_dir): for name in ("prototype-desktop.png", "prototype-mobile.png"): src = os.path.join(prototype_dir, name) if os.path.exists(src): shutil.copy2(src, os.path.join(bdir, name)) renders.append(name) manifest = {"design-evaluable": len(renders) >= 1, "renders": renders, "sanitizer-version": SANITIZER_VERSION} with open(os.path.join(bdir, "prototype-manifest.json"), "w", encoding="utf-8") as f: json.dump(manifest, f) return {"bundle-dir": bdir, "renders": renders, "design-evaluable": manifest["design-evaluable"]} def design_status(bundle): """bundle의 design-evaluable 상태를 평가한다.""" return "evaluable" if bundle.get("design-evaluable") else "not-evaluable"