"""Master Capability Registry v0 — minting, derivation, identity lifecycle. STORED on the registry: identities, sources, relation types, policy versions, lifecycle events, provenance. DERIVED (never stored): confidence/status, via `evaluate_relation` under a versioned policy. Python 3.9 compatible (no `|` unions). """ from __future__ import annotations from typing import Dict, List, Optional, Set from pydantic import BaseModel, Field from .policy import DEFAULT_POLICY from .schemas import ( AssertionStatus, CapabilityCandidate, CapabilityRelation, Confidence, DerivedAssessment, IdentityLifecycleEvent, LifecycleEventType, LifecycleState, MasterCapability, PolicyVersion, Provenance, ) class CapabilityRegistry(BaseModel): # NOTE: no confidence/coverage field anywhere — those are DERIVED, never stored. capabilities: Dict[str, MasterCapability] = Field(default_factory=dict) relations: List[CapabilityRelation] = Field(default_factory=list) lifecycle_events: List[IdentityLifecycleEvent] = Field(default_factory=list) policy: PolicyVersion = Field(default_factory=lambda: DEFAULT_POLICY) next_serial: int = 1 def _mcap_id(serial: int) -> str: return "MCAP-%05d" % serial def _next_event_id(registry: "CapabilityRegistry") -> str: return "evt-%d" % (len(registry.lifecycle_events) + 1) def mint_capability( registry: CapabilityRegistry, candidate: CapabilityCandidate, provenance: Optional[Provenance] = None, name: str = "", definition: str = "", category: str = "", domains: Optional[List[str]] = None, ) -> MasterCapability: """Assign the next stable MCAP id to a candidate and register it (with provenance).""" cap_id = _mcap_id(registry.next_serial) cap = MasterCapability( capability_id=cap_id, name=name or candidate.normalized or candidate.raw_term, definition=definition, category=category, domains=domains or [], provenance=provenance or Provenance(author="system", basis="minted from candidate '%s'" % candidate.raw_term), ) registry.capabilities[cap_id] = cap registry.next_serial += 1 return cap def evaluate_relation( relation: CapabilityRelation, policy: Optional[PolicyVersion] = None ) -> DerivedAssessment: """Derive (status, confidence) from (relationship_type, evidence_kind) under a versioned policy. Deterministic; result is returned, never stored.""" pol = policy if policy is not None else DEFAULT_POLICY status = AssertionStatus.UNKNOWN confidence = Confidence.LOW found = False for rule in pol.rules: if ( rule.relationship_type == relation.relationship_type and rule.evidence_kind == relation.evidence_kind ): status, confidence, found = rule.status, rule.confidence, True break expl = "%s + %s under %s -> %s/%s%s" % ( relation.relationship_type.value, relation.evidence_kind.value, pol.policy_version, status.value, confidence.value, "" if found else " (no rule)", ) return DerivedAssessment( target_capability_id=relation.target_capability_id, status=status, confidence=confidence, policy_version=pol.policy_version, explanation=expl, ) def resolve( registry: CapabilityRegistry, capability_id: str, _seen: Optional[Set[str]] = None ) -> Optional[MasterCapability]: """Follow redirects (from merge/deprecate) to the current canonical capability.""" seen = _seen if _seen is not None else set() if capability_id in seen: return None # redirect cycle guard seen.add(capability_id) cap = registry.capabilities.get(capability_id) if cap is None: return None if cap.redirect_to: return resolve(registry, cap.redirect_to, seen) # terminal: only an ACTIVE capability resolves; a deprecated dead-end -> None return cap if cap.state == LifecycleState.ACTIVE else None def deprecate_capability( registry: CapabilityRegistry, capability_id: str, redirect_to: Optional[str] = None, provenance: Optional[Provenance] = None, ) -> IdentityLifecycleEvent: cap = registry.capabilities.get(capability_id) if cap is None: raise KeyError(capability_id) cap.state = LifecycleState.DEPRECATED cap.redirect_to = redirect_to event = IdentityLifecycleEvent( event_id=_next_event_id(registry), event_type=LifecycleEventType.REDIRECT if redirect_to else LifecycleEventType.DEPRECATE, from_ids=[capability_id], to_ids=[redirect_to] if redirect_to else [], provenance=provenance or Provenance(author="system", basis="deprecate %s" % capability_id), ) registry.lifecycle_events.append(event) return event def merge_capabilities( registry: CapabilityRegistry, from_id: str, into_id: str, provenance: Optional[Provenance] = None, ) -> IdentityLifecycleEvent: """Merge `from_id` into `into_id`: deprecate `from_id` with a redirect to `into_id`.""" if from_id not in registry.capabilities or into_id not in registry.capabilities: raise KeyError("%s or %s" % (from_id, into_id)) frm = registry.capabilities[from_id] frm.state = LifecycleState.DEPRECATED frm.redirect_to = into_id event = IdentityLifecycleEvent( event_id=_next_event_id(registry), event_type=LifecycleEventType.MERGE, from_ids=[from_id], to_ids=[into_id], provenance=provenance or Provenance(author="system", basis="merge %s -> %s" % (from_id, into_id)), ) registry.lifecycle_events.append(event) return event def split_capability( registry: CapabilityRegistry, from_id: str, into_ids: List[str], primary: Optional[str] = None, provenance: Optional[Provenance] = None, ) -> IdentityLifecycleEvent: """Split `from_id` into several capabilities. The old id deprecates; it redirects to `primary` only if one is given (else it resolves to None — split is ambiguous).""" if from_id not in registry.capabilities: raise KeyError(from_id) frm = registry.capabilities[from_id] frm.state = LifecycleState.DEPRECATED frm.redirect_to = primary event = IdentityLifecycleEvent( event_id=_next_event_id(registry), event_type=LifecycleEventType.SPLIT, from_ids=[from_id], to_ids=list(into_ids), provenance=provenance or Provenance(author="system", basis="split %s" % from_id), ) registry.lifecycle_events.append(event) return event