129 lines
4.3 KiB
Python
129 lines
4.3 KiB
Python
"""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")
|