Files

151 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""Mint an IMMUTABLE report path — 매 실행 = 새 버전 파일(덮어쓰기 아님).
보고서는 한 번 쓰면 불변(guard_tools가 덮어쓰기 차단). 새 결과는 항상 새 파일로 남겨
감사 추적(누가·언제·무엇)을 보존한다. 파일은 워크플로별 폴더에 UTC 타임스탬프로 생성한다.
보고서는 불변 SNAPSHOT이다(상태를 이 파일에서 바꾸지 않는다). "이게 최신 시도인가 /
어느 게 수락됐나 / 무엇을 대체(supersede)했나"는 append-only 이벤트(acceptance_log.py)로
따로 기록한다. 여기서는 각 스냅샷에 계보(lineage) 필드만 심는다:
- attempt-id : 이 (workflow, role) 쌍에서 몇 번째 시도인가(1부터, 파일 수로 파생)
- supersedes-report-id : (선택) 이 시도가 대체하는 이전 report-id (--supersedes 로 전달)
Usage:
new_report.py --workflow WF --role ROLE
-> completion-records/<WF>/<ROLE>-<UTCstamp>.report.yaml (없으면 dir 생성) 경로를 출력
new_report.py --workflow WF --role ROLE --stub
-> 위 경로에 report-id/created-at/workflow-id/role-id/attempt-id가 채워진 최소 스텁을 생성까지
new_report.py --workflow WF --role ROLE --stub --supersedes PRIOR-REPORT-ID
-> 스텁에 supersedes-report-id 를 추가로 기록(계보 연결)
"""
import glob
import os
import sys
from datetime import datetime, timezone
ROOT = os.environ.get("CLAUDE_PROJECT_DIR") or os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import _workspace as W # noqa: E402
CR = W.records_dir()
def mint(workflow, role):
wdir = os.path.join(CR, workflow)
os.makedirs(wdir, exist_ok=True)
# attempt-id: 이 (workflow, role) 쌍의 기존 스냅샷 수 + 1 (몇 번째 시도인가).
# 불변 스냅샷 모델 — 파일은 덮어쓰지 않으므로 개수가 곧 시도 횟수.
attempt = len(glob.glob(os.path.join(wdir, f"{role}-*.report.yaml"))) + 1
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
base = f"{role}-{stamp}"
path = os.path.join(wdir, base + ".report.yaml")
n = 1
while os.path.exists(path): # never overwrite
path = os.path.join(wdir, f"{base}-{n}.report.yaml")
n += 1
report_id = os.path.basename(path)[:-len(".report.yaml")]
return path, report_id, stamp, attempt
KNOWN_TYPES = {"decision", "work", "completion", "review", "blocked", "design", "build", "spec", "workflow-artifact"}
def main():
args = sys.argv[1:]
workflow = role = supersedes = rtype = artifact_kind = stage = None
stub = "--stub" in args
i = 0
while i < len(args):
if args[i] == "--workflow":
workflow = args[i + 1]; i += 2
elif args[i] == "--role":
role = args[i + 1]; i += 2
elif args[i] == "--supersedes":
supersedes = args[i + 1]; i += 2
elif args[i] == "--type":
rtype = args[i + 1]; i += 2
elif args[i] == "--artifact-kind":
artifact_kind = args[i + 1]; i += 2
elif args[i] == "--stage":
stage = args[i + 1]; i += 2
else:
i += 1
if not workflow or not role:
sys.stderr.write(
"usage: new_report.py --workflow WF --role ROLE [--stub] [--type TYPE] "
"[--artifact-kind KIND --stage STAGE] "
"[--supersedes PRIOR-REPORT-ID]\n"
)
sys.exit(1)
if artifact_kind:
if not stage:
sys.stderr.write("[new_report] --artifact-kind 사용 시 --stage 필수\n")
sys.exit(1)
rtype = "workflow-artifact"
# report-type 은 필수(P0-5). --stub 에서 미지정이면 'work'로 두되 경고 — 커맨드는 산출물에
# 맞는 정확한 유형(decision/design/build/spec/completion/review/blocked)을 넘겨야 한다.
if rtype and rtype not in KNOWN_TYPES:
sys.stderr.write(f"[new_report] 경고: 미지 report-type '{rtype}' — 알려진 유형 {sorted(KNOWN_TYPES)} 권장.\n")
if stub and not rtype:
rtype = "work"
sys.stderr.write("[new_report] 경고: --type 미지정 — 스텁 report-type=work 로 발급. 산출물에 맞는 --type 을 넘겨라.\n")
path, report_id, stamp, attempt = mint(workflow, role)
if stub:
if artifact_kind:
lines = [
"report-type: workflow-artifact\n",
f"artifact-kind: {artifact_kind}\n",
"artifact-version: 1\n",
"identity:\n",
f" artifact-id: {report_id}\n",
f" workflow-id: {workflow}\n",
f" stage: {stage}\n",
f" producer-role-id: {role}\n",
f"created-at: {stamp}\n",
f"attempt-id: {attempt}\n",
]
else:
lines = [
f"report-type: {rtype}\n",
f"report-id: {report_id}\n",
f"workflow-id: {workflow}\n",
f"role-id: {role}\n",
f"created-at: {stamp}\n",
f"attempt-id: {attempt}\n",
]
if supersedes:
lines.append(f"supersedes-report-id: {supersedes}\n")
if rtype == "work":
# work.schema.json requires a top-level work-summary; scaffold it so a --stub
# work report is schema-valid out of the box (otherwise the first write BLOCKs
# on the missing required field — the exact block hit during a synthesis pass).
lines.append('work-summary: ""\n')
if artifact_kind:
lines.append("payload: {}\n")
lines += [
"projection-version: 1\n",
"decision-summary:\n",
" bottom-line: \"\"\n",
" recommendation: \"\"\n",
" decision-needed: false\n",
" confidence: Med\n",
"evidence-index: []\n",
"dissent: []\n",
"open-risks: []\n",
"artifact-refs: []\n",
"report-header:\n",
" bottom-line: \"\"\n",
" decision-needed: { needed: false, approver: }\n",
" confidence: { value: Med, derived-from: evidence }\n",
" risks: []\n",
" evidence: []\n",
]
with open(path, "w") as f:
f.write("".join(lines))
print(os.path.relpath(path, ROOT))
if __name__ == "__main__":
main()