Files
company-haness/.claude/tests/test_p3_infra.py
T

156 lines
7.8 KiB
Python

#!/usr/bin/env python3
"""P3 인프라 강제기 — standalone check(pytest 아님). exit 0=통과.
Phase 1: role-working-methods 파일분리 정합(중복0·누락0·미include0).
이후 phase(gen v1/v2·policy engine·enforcement)가 이 파일에 append된다.
"""
import glob
import os
import sys
import yaml
ROOT = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
REG = os.path.join(ROOT, "org-os", "00-role-registry")
RWM_DIR = os.path.join(REG, "role-working-methods")
passed = failed = 0
def check(name, ok):
global passed, failed
if ok:
passed += 1
print(f" ✅ {name}")
else:
failed += 1
print(f" ❌ {name}")
def load_role_methods():
"""index.includes를 병합해 {role-id: entry}. 중복/미include=에러."""
idx = yaml.safe_load(open(os.path.join(RWM_DIR, "index.yaml")))["role-method-contracts"]
merged, srcs = {}, {}
for inc in idx["includes"]:
d = yaml.safe_load(open(os.path.join(RWM_DIR, inc))) or {}
for rid, entry in (d.get("role-working-methods") or {}).items():
assert rid not in merged, f"중복 role-id {rid} ({srcs.get(rid)} & {inc})"
merged[rid] = entry
srcs[rid] = inc
files_on_disk = {os.path.basename(p) for p in glob.glob(os.path.join(RWM_DIR, "*.yaml"))} - {"index.yaml"}
assert files_on_disk == set(idx["includes"]), \
f"미include/유령 파일: {files_on_disk ^ set(idx['includes'])}"
return merged
print("== Phase 1: role-working-methods 파일분리 ==")
_rm = load_role_methods()
_fams = yaml.safe_load(open(os.path.join(REG, "capability-families.yaml")))["capability-families"]["families"]
_bound = set()
for f in _fams:
_bound |= set(f["member-role-ids"])
check("파일분리 병합 = 75 agent-bound 역할(중복0·미include0)", set(_rm) == _bound and len(_rm) == 75)
# 내용 불변: 분할본이 기존 단일 파일과 동일(v1 fallback 잔존 시 대조)
_single = os.path.join(REG, "role-working-methods.yaml")
if os.path.exists(_single):
_orig = yaml.safe_load(open(_single))["role-working-methods"]
check("분할이 기존 단일 파일 내용 보존(동일 집합)", set(_rm) == set(_orig))
check("각 역할 working-method 내용 불변(DES-PROD 샘플)",
_rm.get("DES-PROD", {}).get("working-method") == _orig.get("DES-PROD", {}).get("working-method"))
print("== Phase 1: artifact vocabulary + method-execution schema ==")
_av = yaml.safe_load(open(os.path.join(ROOT, "org-os/06-agent-work/artifact-type-vocabulary.yaml")))["artifact-types"]
check("artifact vocabulary has core types",
all(t in _av for t in ["product-decision", "direction-input-brief", "selected-direction",
"locked-invariants", "interaction-state-model", "api-contract",
"design-decision-record"]))
check("artifact-type entries declare producer-roles",
all(isinstance(v.get("producer-roles"), list) and v["producer-roles"] for v in _av.values()))
import json # noqa: E402
_me = json.load(open(os.path.join(ROOT, ".claude/schemas/method-execution.schema.json")))
_mp = _me.get("properties", {})
check("method-execution schema requires step-results + method-id + contract-sha256",
set(["role-id", "method-id", "contract-sha256", "step-results"]).issubset(set(_me.get("required", []))))
check("step-results item requires step-id+status",
set(["step-id", "status"]).issubset(
set(_mp["step-results"]["items"]["required"])))
check("decisions require structured alternatives (option-id) not just count",
"option-id" in str(_mp.get("decisions", {})))
check("handoffs is array (다수 consumer)",
_mp.get("handoffs", {}).get("type") == "array")
print("== Phase 1: Contract v2 스키마 규약(gate catalog) ==")
_idx = yaml.safe_load(open(os.path.join(RWM_DIR, "index.yaml")))
_cs = _idx.get("contract-v2-schema", {})
check("index defines machine-check vocabulary",
set(["artifact-field-present", "artifact-fields-absent", "artifact-exists"]).issubset(
set(_cs.get("machine-check-vocabulary", []))))
check("index defines enforcement levels + activation states",
_cs.get("enforcement-levels") == ["hard", "warning", "instructional"]
and _cs.get("activation-states") == ["draft", "active", "retired"])
check("index defines v2 profile-fields",
"workflow" in _cs.get("profile-fields", []) and "handoff-contract" in _cs.get("profile-fields", []))
print("== Phase 2: method-skill-registry + gen_method_skills ==")
_reg = yaml.safe_load(open(os.path.join(REG, "method-skill-registry.yaml")))["method-skill-registry"]
check("registry covers 75 agent-bound roles",
set(_reg["roles"]) == _bound and len(_reg["roles"]) == 75)
check("registry generated-dir FLAT (.claude/skills, Phase0)",
_reg["generated-dir"] == ".claude/skills")
check("registry method-skill 이름 규약 <role>-method",
all(_reg["roles"][r]["method-skill"] == r.lower() + "-method" for r in _reg["roles"]))
import importlib.util # noqa: E402
_spec = importlib.util.spec_from_file_location("gms", os.path.join(ROOT, ".claude/hooks/gen_method_skills.py"))
_gms = importlib.util.module_from_spec(_spec)
sys.path.insert(0, os.path.join(ROOT, ".claude/hooks"))
_spec.loader.exec_module(_gms)
_skills = _gms.build_all()
check("gen_method_skills builds 75", len(_skills) == 75)
# v1 flat 렌더 내용 보존 — v1(계약 version!=2) 역할을 동적으로 골라 검증(v2 역할은 _render_v2 사용).
# Phase 6 완료로 전 역할이 v2면 v1 flat 경로를 타는 역할이 없다 → 검증 대상 0(회귀는 v1 재등장 시 재활성).
_v1r = next((r for r, e in _rm.items()
if (e.get("method-contract") or {}).get("version") != 2 and e.get("working-method")), None)
if _v1r:
check(f"v1 flat 렌더 내용 보존({_v1r} working-method)",
all(s in _skills[_v1r.lower() + "-method"] for s in _rm[_v1r]["working-method"]))
else:
check("v1 flat 렌더 검증 — v1 역할 0(전 역할 v2 계약, Phase 6 완료)", True)
_gen = os.path.join(ROOT, _reg["generated-dir"])
_ondisk = [os.path.basename(os.path.dirname(p)) for p in glob.glob(os.path.join(_gen, "*", "SKILL.md"))
if os.path.basename(os.path.dirname(p)).endswith("-method")]
check("75 method-skills on disk (flat) + 수제 skill 공존",
len(_ondisk) == 75 and os.path.exists(os.path.join(_gen, "design-craft", "SKILL.md")))
print("== Phase 2: 무결성(skill_refs · doctor · lint) ==")
_srspec = importlib.util.spec_from_file_location("skill_refs", os.path.join(ROOT, ".claude/hooks/skill_refs.py"))
_sr = importlib.util.module_from_spec(_srspec)
_srspec.loader.exec_module(_sr)
_known = _sr.known_skill_names(ROOT)
check("skill_refs finds method + capability skills",
"des-prod-method" in _known and "design-craft" in _known)
check("parse_skills handles list + '[a, b]'",
_sr.parse_skills(["a", "b"]) == ["a", "b"] and _sr.parse_skills("[x, y]") == ["x", "y"])
# gen_agents 카드 skills 참조가 전부 실존(고아/미해결 0)
_cards = glob.glob(os.path.join(ROOT, ".claude/agents/*.md"))
_unres = []
for _c in _cards:
_fm = yaml.safe_load(open(_c).read().split("---\n")[1]) or {}
for _s in _sr.parse_skills(_fm.get("skills")):
if _s not in _known:
_unres.append(f"{os.path.basename(_c)}:{_s}")
check("모든 카드 skills: 참조 해소(미해결 0)", _unres == [])
check("75 concrete agents (family metadata cards removed)", len(_cards) == 75)
import subprocess # noqa: E402
_dr = subprocess.run([sys.executable, os.path.join(ROOT, ".claude/hooks/doctor.py")],
capture_output=True, text=True,
env={**os.environ, "CLAUDE_PROJECT_DIR": ROOT, "ORGOS_WORKSPACE": "_sandbox"})
check("doctor method-skill 배선 OK", "method-skill 배선 OK" in _dr.stdout and _dr.returncode == 0)
print(f"\n{passed} passed · {failed} failed")
sys.exit(1 if failed else 0)