"""Profile execution planning.""" from __future__ import annotations from collections.abc import Iterable from .models import ( Diagnostic, LifecycleAction, LifecycleActionKind, ProfileExecutionPlan, ProfileIntent, ) DEFAULT_LOCAL_ADAPTERS = { "local-event-log", "local-graph-store", "markitect-context-package", "markitect-context-registry", "markitect-memory-graph-fixture", "infospace-workflow-trace-fixtures", "infospace-artifact-neighborhood", } def plan_profile_execution( profile: ProfileIntent, *, available_adapters: Iterable[str] | None = None, ) -> ProfileExecutionPlan: available = set(DEFAULT_LOCAL_ADAPTERS if available_adapters is None else available_adapters) required_adapters = tuple(sorted(set(profile.stores.values()))) missing_adapters = tuple(adapter for adapter in required_adapters if adapter not in available) diagnostics: list[Diagnostic] = [] actions: list[LifecycleAction] = [] if not profile.profile_id: diagnostics.append(Diagnostic("error", "missing_profile_id", "Profile execution requires a profile id.")) for adapter in missing_adapters: diagnostics.append( Diagnostic( "warn", "missing_adapter", "Required adapter is not available; plan will use fallback behavior.", "stores", {"adapter": adapter}, ) ) actions.append( LifecycleAction( LifecycleActionKind.NO_OP, target_id=adapter, reason="adapter unavailable in dry-run planner", metadata={"fallback": _missing_store_fallback(profile)}, ) ) capabilities = tuple(sorted(_capabilities_for(profile))) policy_gates = tuple(_policy_gates(profile)) observability_events = tuple(_observability_events(profile, missing_adapters)) return ProfileExecutionPlan( profile_id=profile.profile_id, enabled_memory_kinds=tuple(profile.memory_kinds), required_adapters=required_adapters, missing_adapters=missing_adapters, capabilities=capabilities, activation_budget=dict(profile.activation), policy_gates=policy_gates, observability_events=observability_events, fallback_behavior={ "missing_runtime_store": _missing_store_fallback(profile), **dict(profile.failure), }, actions=tuple(actions), diagnostics=tuple(diagnostics), metadata={ "stores": dict(profile.stores), "limits": dict(profile.limits), "retention": dict(profile.retention), "refresh": dict(profile.refresh), "compaction": dict(profile.compaction), }, ) def _capabilities_for(profile: ProfileIntent) -> set[str]: capabilities = {"profile.inspect", "profile.plan"} if profile.retention: capabilities.add("retention.plan") if profile.refresh: capabilities.add("refresh.plan") if profile.compaction: capabilities.add("compaction.plan") if profile.activation: capabilities.add("activation.plan") if profile.policy: capabilities.add("policy.gate") return capabilities def _policy_gates(profile: ProfileIntent) -> list[str]: policy = profile.policy gates: list[str] = [] for label in policy.get("required_labels", ()): gates.append(f"label:{label}") durable_writes = policy.get("durable_writes") if durable_writes: gates.append(f"durable_writes:{durable_writes}") if policy.get("secrets_allowed") is False: gates.append("secrets:denied") if policy.get("reauthorization"): gates.append(f"reauthorization:{policy['reauthorization']}") return gates def _observability_events(profile: ProfileIntent, missing_adapters: tuple[str, ...]) -> list[str]: observability = profile.observability events = list(observability.get("events") or observability.get("emit") or ()) if observability.get("emit_events") is True and not events: events.append("phase_memory.profile.planned") if missing_adapters: events.append("phase_memory.adapter.missing") return events def _missing_store_fallback(profile: ProfileIntent) -> str: return str(profile.failure.get("missing_runtime_store") or "degrade-to-dry-run")