Files
company-haness/.claude/hooks/orgos/planning/lens_policy.py
T

146 lines
5.6 KiB
Python

"""Shared divergent-lens policy derived from the role registries.
The policy deliberately reasons about registered family/role capabilities. A
caller supplied lens label is never sufficient evidence that a role can carry
that lens.
"""
from __future__ import annotations
import os
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.dirname(os.path.dirname(os.path.abspath(__file__)))))
)
REG = os.path.join(ROOT, "org-os", "00-role-registry")
TIERS = os.path.join(ROOT, "org-os", "06-agent-work", "governance-tiers.yaml")
def _load(path: str) -> dict[str, Any]:
try:
with open(path, encoding="utf-8") as handle:
return yaml.safe_load(handle) or {}
except Exception:
return {}
def registries() -> tuple[dict[str, Any], dict[str, Any], list[str]]:
families_doc = _load(os.path.join(REG, "capability-families.yaml"))
lenses_doc = _load(os.path.join(REG, "lens-registry.yaml"))
families = {
str(item.get("family-id")): item
for item in (families_doc.get("capability-families", {}) or {}).get("families", []) or []
if isinstance(item, dict) and item.get("family-id")
}
lens_items = (lenses_doc.get("lens-registry", {}) or {}).get("lenses", []) or []
lenses = {
str(item.get("lens-id")): item
for item in lens_items if isinstance(item, dict) and item.get("lens-id")
}
order = [str(item.get("lens-id")) for item in lens_items
if isinstance(item, dict) and item.get("lens-id")]
return families, lenses, order
def normalize_family_ids(values: Any) -> list[str]:
if isinstance(values, str):
values = [values]
if not isinstance(values, list):
return []
return [str(value).strip().upper() for value in values if str(value or "").strip()]
def candidate_family_errors(values: Any, *, tier: str, mode: str,
enforce_lens_floor: bool = False) -> list[str]:
family_ids = normalize_family_ids(values)
families, _lenses, _order = registries()
errors: list[str] = []
if not family_ids:
return ["candidate-families는 비어 있지 않은 등록 family 목록이어야 한다"]
duplicates = sorted({value for value in family_ids if family_ids.count(value) > 1})
unknown = sorted(set(family_ids) - set(families))
if duplicates:
errors.append(f"candidate-families 중복: {duplicates}")
if unknown:
errors.append(f"candidate-families 미등록 family: {unknown}")
if errors or (str(mode).lower() != "divergent" and not enforce_lens_floor):
return errors
available = available_lenses(family_ids)
policy = divergent_policy(tier)
minimum = policy.get("min-distinct-lenses")
if isinstance(minimum, int) and len(available) < minimum:
errors.append(
f"candidate-families 이론 렌즈 커버리지 부족: {len(available)} < tier {tier} 최소 {minimum}"
)
if policy.get("contrarian-required") and "LENS-CONTRARIAN" not in available:
errors.append(
"candidate-families에 contrarian rotation을 맡을 audit-capable family가 없다"
)
return errors
def divergent_policy(tier: str) -> dict[str, Any]:
doc = _load(TIERS).get("governance-tiers", {}) or {}
return (((doc.get("tiers") or {}).get(str(tier).lower()) or {}).get("divergent") or {})
def available_lenses(family_ids: list[str]) -> set[str]:
"""Return lenses the candidate set can actually assign.
Contrarian is special: the registry intentionally has no fixed carrier. It
becomes available only when the candidate set contains an audit-capable
family that can be rotated in independently.
"""
families, lenses, _order = registries()
selected = {family_id for family_id in family_ids if family_id in families}
result: set[str] = set()
for lens_id, lens in lenses.items():
if set(lens.get("carrier-families", []) or []) & selected:
result.add(lens_id)
for family_id in selected:
result.update(families[family_id].get("carries-lenses", []) or [])
if any(families[family_id].get("audit-capable") for family_id in selected):
result.add("LENS-CONTRARIAN")
return result
def required_lenses(family_ids: list[str], *, tier: str, mode: str) -> set[str]:
if str(mode).lower() != "divergent":
return set()
available = available_lenses(family_ids)
policy = divergent_policy(tier)
minimum = policy.get("min-distinct-lenses")
contrarian = bool(policy.get("contrarian-required"))
if minimum == "all-relevant":
return available
count = int(minimum or 0)
required: list[str] = []
if contrarian and "LENS-CONTRARIAN" in available:
required.append("LENS-CONTRARIAN")
_families, _lenses, order = registries()
required.extend(lens for lens in order if lens in available and lens not in required)
return set(required[:count])
def family_for_role(role_id: str) -> tuple[str | None, dict[str, Any] | None]:
families, _lenses, _order = registries()
wanted = str(role_id or "").upper()
for family_id, family in families.items():
if wanted in {str(value).upper() for value in family.get("member-role-ids", []) or []}:
return family_id, family
return None, None
def role_can_carry_lens(role_id: str, lens_id: str) -> bool:
family_id, family = family_for_role(role_id)
if not family_id or not family:
return False
lens_id = str(lens_id or "").upper()
if lens_id == "LENS-CONTRARIAN":
return bool(family.get("audit-capable"))
return lens_id in available_lenses([family_id])