111 lines
4.7 KiB
Python
111 lines
4.7 KiB
Python
"""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"
|