"""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])