#!/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 이름 규약 -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)