302 lines
13 KiB
Python
302 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""Compile human-owned Pack and contract sources into runtime registries and architecture docs.
|
|
|
|
Generated output lives under ``org-os/generated`` and must not be edited manually.
|
|
``--check`` fails when any generated file drifts from its sources.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import os
|
|
import sys
|
|
from typing import Any
|
|
|
|
import yaml
|
|
|
|
ROOT = os.environ.get("CLAUDE_PROJECT_DIR") or os.path.dirname(
|
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
)
|
|
PACK_INDEX = os.path.join(ROOT, "org-os", "packs", "pack-index.yaml")
|
|
REG = os.path.join(ROOT, "org-os", "00-role-registry")
|
|
WORK = os.path.join(ROOT, "org-os", "06-agent-work")
|
|
OUT = os.path.join(ROOT, "org-os", "generated")
|
|
|
|
|
|
def load(path: str) -> dict[str, Any]:
|
|
return yaml.safe_load(open(path, encoding="utf-8")) or {}
|
|
|
|
|
|
def sha(path: str) -> str:
|
|
digest = hashlib.sha256()
|
|
with open(path, "rb") as handle:
|
|
for chunk in iter(lambda: handle.read(65536), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def method_sources() -> list[str]:
|
|
directory = os.path.join(REG, "role-working-methods")
|
|
index = os.path.join(directory, "index.yaml")
|
|
includes = (load(index).get("role-method-contracts") or {}).get("includes", []) or []
|
|
return [index] + [os.path.join(directory, value) for value in includes]
|
|
|
|
|
|
def architecture_views(packs: dict[str, Any], source_hashes: dict[str, str]) -> dict[str, str]:
|
|
"""Build four generated views without duplicating the exact YAML registries."""
|
|
source_fingerprint = hashlib.sha256(
|
|
"\n".join(f"{name}:{digest}" for name, digest in sorted(source_hashes.items())).encode()
|
|
).hexdigest()
|
|
generated_header = (
|
|
"# generated by .claude/hooks/compile_orgos_registry.py — do not edit\n"
|
|
f"# source-sha256: {source_fingerprint}"
|
|
)
|
|
static_rows = [
|
|
generated_header,
|
|
"direction: right",
|
|
'sources: "Human-owned contracts" {',
|
|
' roles: "roles + profiles"',
|
|
' families: "family candidate pools"',
|
|
' packs: "pack index"',
|
|
' methods: "working methods"',
|
|
' artifacts: "artifact contracts"',
|
|
"}",
|
|
'compiler: "Org OS compiler"',
|
|
'generated: "Generated registries + architecture views"',
|
|
'runtime: "Common execution kernel" {',
|
|
' intake: "intake classifier"',
|
|
' planner: "role / budget planner"',
|
|
' context: "context package binder"',
|
|
' state: "event + state services"',
|
|
' observer: "usage observer"',
|
|
"}",
|
|
"sources -> compiler -> generated -> runtime",
|
|
'domain_packs: "Domain packs" {',
|
|
]
|
|
for pack_name, definition in packs["packs"].items():
|
|
node = "pack_" + pack_name.replace("-", "_")
|
|
label = f"{pack_name} [{definition['plane']}]\\n{len(definition.get('family-ids', []))} families"
|
|
static_rows.append(f' {node}: "{label}"')
|
|
static_rows += ["}", "domain_packs -> sources.packs", "runtime.planner -> domain_packs", ""]
|
|
|
|
runtime = generated_header + """
|
|
direction: right
|
|
request: "Request"
|
|
intake: "Deterministic intake\\nlight | substantial | strategic"
|
|
planner: "Minimum-sufficient role planner\\ncoverage + budget + independence"
|
|
package: "Immutable context package\\nrole + tools + paths + SHA"
|
|
agent: "Concrete role agent"
|
|
projection: "Projection-first report"
|
|
review: "Independent reviewer"
|
|
ledger: "Append-only event / usage ledgers"
|
|
request -> intake -> planner -> package -> agent -> projection
|
|
projection -> review: "when required"
|
|
intake -> ledger
|
|
planner -> ledger
|
|
package -> ledger
|
|
agent -> ledger
|
|
review -> ledger
|
|
"""
|
|
authority = generated_header + """
|
|
direction: down
|
|
control_plane: "Control plane" {
|
|
intake: "classify"
|
|
planner: "select / budget"
|
|
guard: "bind tools + paths"
|
|
state: "record truth"
|
|
}
|
|
decision_plane: "Decision plane" {
|
|
decider: "concrete decision role"
|
|
authority: "approve one-way-door decisions"
|
|
}
|
|
delivery_plane: "Design + delivery planes" {
|
|
producer: "concrete producer role"
|
|
artifact: "versioned artifact"
|
|
}
|
|
assurance_plane: "Assurance plane" {
|
|
reviewer: "different concrete reviewer role"
|
|
verdict: "evidence-backed verdict"
|
|
}
|
|
control_plane.planner -> delivery_plane.producer: "assign"
|
|
control_plane.guard -> delivery_plane.producer: "constrain"
|
|
delivery_plane.artifact -> assurance_plane.reviewer: "review"
|
|
assurance_plane.verdict -> decision_plane.decider: "escalate if authority needed"
|
|
decision_plane.authority -> control_plane.state: "immutable decision event"
|
|
"""
|
|
events = generated_header + """
|
|
direction: right
|
|
commands: "Commands" {
|
|
selection: "SelectionPlanCreated"
|
|
spawn: "SpawnBindingPending / Claimed"
|
|
artifact: "ArtifactSubmitted / Reviewed"
|
|
decision: "DecisionRecorded"
|
|
}
|
|
event_store: "Append-only event store"
|
|
materializer: "Deterministic materializer"
|
|
views: "Materialized views" {
|
|
workflow: "workflow state"
|
|
registry: "subagent registry"
|
|
usage: "token + context metrics"
|
|
}
|
|
rehydration: "Tiered rehydration\\nprojection -> evidence index -> source"
|
|
commands -> event_store -> materializer -> views -> rehydration
|
|
event_store -> materializer: "replay"
|
|
"""
|
|
return {
|
|
"static-components.d2": "\n".join(static_rows),
|
|
"runtime-sequence.d2": runtime,
|
|
"authority-swimlane.d2": authority,
|
|
"event-model.d2": events,
|
|
}
|
|
|
|
|
|
def compile_outputs() -> dict[str, str]:
|
|
packs = load(PACK_INDEX)["org-os-packs"]
|
|
roles_path = os.path.join(REG, "roles.yaml")
|
|
profiles_path = os.path.join(REG, "role-profiles.yaml")
|
|
families_path = os.path.join(REG, "capability-families.yaml")
|
|
artifacts_path = os.path.join(WORK, "generated", "artifact-registry.yaml")
|
|
contracts_path = os.path.join(WORK, "workflow-contracts.yaml")
|
|
roles = load(roles_path)["role-registry"]
|
|
profiles = load(profiles_path)["role-profiles"]
|
|
family_doc = load(families_path)["capability-families"]
|
|
artifacts = load(artifacts_path)["artifact-registry"]
|
|
contracts = load(contracts_path)["workflow-contracts"]
|
|
families = {item["family-id"]: item for item in family_doc["families"]}
|
|
role_map = {item["role-id"]: item for item in roles["roles"]}
|
|
profile_map = {item["role-id"]: item for item in profiles["profiles"]}
|
|
|
|
ownership: dict[str, dict[str, str]] = {}
|
|
for pack_name, definition in packs["packs"].items():
|
|
for family_id in definition.get("family-ids", []) or []:
|
|
if family_id in ownership:
|
|
raise ValueError(f"family belongs to multiple packs: {family_id}")
|
|
if family_id not in families:
|
|
raise ValueError(f"pack references unknown family: {family_id}")
|
|
ownership[family_id] = {"pack": pack_name, "plane": definition["plane"]}
|
|
missing = sorted(set(families) - set(ownership))
|
|
if missing:
|
|
raise ValueError(f"families missing from pack index: {missing}")
|
|
|
|
source_paths = [PACK_INDEX, roles_path, profiles_path, families_path, artifacts_path, contracts_path] + method_sources()
|
|
source_hashes = {os.path.relpath(path, ROOT): sha(path) for path in source_paths}
|
|
compiled_families = []
|
|
bound_roles = set()
|
|
for family_id, family in families.items():
|
|
entry = dict(family)
|
|
entry.update(ownership[family_id])
|
|
entry["execution-identity"] = "concrete-role-only"
|
|
entry["agent-card"] = None
|
|
compiled_families.append(entry)
|
|
for role_id in entry.get("member-role-ids", []) or []:
|
|
if role_id in bound_roles:
|
|
raise ValueError(f"role belongs to multiple families: {role_id}")
|
|
if role_id not in role_map or role_id not in profile_map:
|
|
raise ValueError(f"family role lacks role/profile: {role_id}")
|
|
bound_roles.add(role_id)
|
|
if bound_roles != set(role_map):
|
|
raise ValueError(f"family coverage mismatch: {sorted(set(role_map) ^ bound_roles)}")
|
|
|
|
role_entries = []
|
|
family_by_role = {role_id: family_id for family_id, family in families.items()
|
|
for role_id in family.get("member-role-ids", []) or []}
|
|
for role_id, role in role_map.items():
|
|
family_id = family_by_role[role_id]
|
|
role_entries.append({
|
|
**role,
|
|
"family-id": family_id,
|
|
"pack": ownership[family_id]["pack"],
|
|
"plane": ownership[family_id]["plane"],
|
|
"agent-card": f".claude/agents/{role_id.lower()}.md",
|
|
})
|
|
|
|
methods = {}
|
|
for path in method_sources()[1:]:
|
|
methods.update(load(path).get("role-working-methods", {}) or {})
|
|
if set(methods) != set(role_map):
|
|
raise ValueError("method registry must cover every concrete role exactly once")
|
|
|
|
commands = {}
|
|
stages = {}
|
|
for workflow_name, workflow in (contracts.get("workflows") or {}).items():
|
|
stages[workflow_name] = list((workflow.get("stages") or {}).keys())
|
|
for stage, definition in (workflow.get("stages") or {}).items():
|
|
command = definition.get("command") if isinstance(definition, dict) else None
|
|
if command:
|
|
commands.setdefault(command, []).append({"workflow": workflow_name, "stage": stage})
|
|
|
|
generated = {
|
|
"role-registry.yaml": {
|
|
"generated-role-registry": {"version": 1, "generated-by": ".claude/hooks/compile_orgos_registry.py",
|
|
"source-sha256": source_hashes, "role-count": len(role_entries), "roles": role_entries}},
|
|
"family-registry.yaml": {
|
|
"generated-family-registry": {"version": 1, "generated-by": ".claude/hooks/compile_orgos_registry.py",
|
|
"source-sha256": source_hashes, "family-count": len(compiled_families),
|
|
"families": compiled_families}},
|
|
"method-registry.yaml": {
|
|
"generated-method-registry": {"version": 1, "generated-by": ".claude/hooks/compile_orgos_registry.py",
|
|
"source-sha256": source_hashes, "role-count": len(methods), "roles": methods}},
|
|
"architecture-index.yaml": {
|
|
"architecture-index": {"version": 1, "generated-by": ".claude/hooks/compile_orgos_registry.py",
|
|
"source-sha256": source_hashes,
|
|
"counts": {"roles": len(role_entries), "families": len(compiled_families),
|
|
"agent-cards": len(role_entries),
|
|
"artifact-kinds": int(artifacts.get("artifact-kind-count") or len(artifacts.get("artifact-kinds", {}))),
|
|
"packs": len(packs["packs"]), "planes": len(packs["planes"])},
|
|
"packs": packs["packs"], "workflow-stages": stages, "command-map": commands}},
|
|
}
|
|
outputs = {name: yaml.safe_dump(doc, sort_keys=False, allow_unicode=True)
|
|
for name, doc in generated.items()}
|
|
index = generated["architecture-index.yaml"]["architecture-index"]
|
|
rows = ["# Generated architecture index", "", "이 파일은 `compile_orgos_registry.py`가 생성합니다. 수기 수정 금지.", "",
|
|
"| Registry | Count |", "|---|---:|",
|
|
f"| Concrete roles / agent cards | {index['counts']['roles']} |",
|
|
f"| Family metadata pools | {index['counts']['families']} |",
|
|
f"| Domain packs | {index['counts']['packs']} |",
|
|
f"| Responsibility planes | {index['counts']['planes']} |",
|
|
f"| Artifact kinds | {index['counts']['artifact-kinds']} |", "",
|
|
"## Packs", "", "| Pack | Plane | Families |", "|---|---|---|"]
|
|
for pack_name, definition in packs["packs"].items():
|
|
rows.append(f"| {pack_name} | {definition['plane']} | {', '.join(definition['family-ids'])} |")
|
|
rows += [
|
|
"", "## Architecture views", "",
|
|
"- `static-components.d2` — source, compiler, kernel, Pack 정적 구성",
|
|
"- `runtime-sequence.d2` — intake부터 projection/review까지의 실행 흐름",
|
|
"- `authority-swimlane.d2` — control/decision/delivery/assurance 권한 경계",
|
|
"- `event-model.d2` — append-only event와 materialized view 관계",
|
|
"", "Family는 actor가 아니며 `.claude/agents`에는 concrete role card만 생성됩니다.", "",
|
|
]
|
|
outputs["README.generated.md"] = "\n".join(rows)
|
|
outputs.update(architecture_views(packs, source_hashes))
|
|
return outputs
|
|
|
|
|
|
def main() -> int:
|
|
check = "--check" in sys.argv[1:]
|
|
try:
|
|
outputs = compile_outputs()
|
|
except Exception as exc:
|
|
sys.stderr.write(f"[orgos-registry] compile failed: {exc}\n")
|
|
return 2
|
|
drift = []
|
|
for name, expected in outputs.items():
|
|
path = os.path.join(OUT, name)
|
|
actual = open(path, encoding="utf-8").read() if os.path.exists(path) else None
|
|
if actual != expected:
|
|
drift.append(name)
|
|
if not check:
|
|
os.makedirs(OUT, exist_ok=True)
|
|
with open(path, "w", encoding="utf-8") as handle:
|
|
handle.write(expected)
|
|
if check and drift:
|
|
sys.stderr.write(f"[orgos-registry] generated drift: {', '.join(drift)}\n")
|
|
return 2
|
|
print(
|
|
"OK orgos registry: 4 registries + generated README + 4 architecture views "
|
|
f"({'checked' if check else 'written'})"
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|