"""Service-readiness contracts, config, health, and conformance helpers.""" from __future__ import annotations from dataclasses import dataclass, field from typing import Any from .adapters import AllowAllPolicyGateway, InMemoryMemoryEventLog, InMemoryMemoryGraphStore, NoopContextPackageCompiler, RecordingAuditSink from .models import Diagnostic, MemoryEvent, MemoryNode, PolicyDecision, ProfileIntent from .runtime import PhaseMemoryRuntime SERVICE_CONTRACT_SCHEMA = "phase_memory.service.contracts.v1" HEALTH_REPORT_SCHEMA = "phase_memory.health.report.v1" KONTEXTUAL_DELEGATION_SCHEMA = "phase_memory.kontextual.delegation.v1" SERVICE_OPERATIONS = { "profile.plan": {"request": ["profile"], "response": "runtime_envelope"}, "graph.import": {"request": ["graph"], "response": "runtime_envelope"}, "graph.lifecycle.plan": {"request": ["graph", "parameters"], "response": "runtime_envelope"}, "lifecycle.apply": {"request": ["actions", "review_record"], "response": "runtime_envelope"}, "graph.activation.plan": {"request": ["graph", "budget"], "response": "runtime_envelope"}, "package.compile": {"request": ["selection"], "response": "runtime_envelope"}, "audit.query": {"request": ["filters"], "response": "audit_events"}, "health.check": {"request": [], "response": "health_report"}, } @dataclass(frozen=True) class RuntimeConfig: local_store_path: str = ".phase-memory-local" adapter_registry: dict[str, str] = field(default_factory=lambda: {"graph_store": "memory", "event_log": "memory"}) policy_mode: str = "allow-all" audit_sink_mode: str = "recording" package_compiler_mode: str = "noop" semantic_index_mode: str = "disabled" dry_run_default: bool = True trust_zone_labels: tuple[str, ...] = ("local",) @classmethod def local_default(cls) -> "RuntimeConfig": return cls() def diagnostics(self) -> tuple[Diagnostic, ...]: diagnostics: list[Diagnostic] = [] if not self.local_store_path: diagnostics.append(Diagnostic("error", "missing_store_path", "Runtime config requires a local store path.", "local_store_path")) if self.policy_mode not in {"allow-all", "external"}: diagnostics.append(Diagnostic("error", "unsupported_policy_mode", "Unsupported policy mode.", "policy_mode", {"policy_mode": self.policy_mode})) if self.semantic_index_mode not in {"disabled", "external"}: diagnostics.append(Diagnostic("error", "unsupported_semantic_index_mode", "Unsupported semantic index mode.", "semantic_index_mode", {"semantic_index_mode": self.semantic_index_mode})) return tuple(diagnostics) def to_dict(self) -> dict[str, Any]: return { "local_store_path": self.local_store_path, "adapter_registry": dict(self.adapter_registry), "policy_mode": self.policy_mode, "audit_sink_mode": self.audit_sink_mode, "package_compiler_mode": self.package_compiler_mode, "semantic_index_mode": self.semantic_index_mode, "dry_run_default": self.dry_run_default, "trust_zone_labels": list(self.trust_zone_labels), } def service_contracts() -> dict[str, Any]: return {"schema_version": SERVICE_CONTRACT_SCHEMA, "operations": SERVICE_OPERATIONS} def runtime_from_config(config: RuntimeConfig | None = None) -> PhaseMemoryRuntime: config = config or RuntimeConfig.local_default() # First service-ready slice keeps adapters dependency-light. External # adapter resolution belongs behind the registry in later deployments. return PhaseMemoryRuntime() def health_report(runtime: PhaseMemoryRuntime, *, config: RuntimeConfig | None = None) -> dict[str, Any]: config = config or RuntimeConfig.local_default() nodes = runtime.graph_store.list_nodes() stale = [node for node in nodes if node.lifecycle.value == "stale"] pending_review = [node for node in nodes if node.lifecycle.value == "review_needed"] diagnostics = list(config.diagnostics()) return { "schema_version": HEALTH_REPORT_SCHEMA, "ok": not any(diagnostic.severity == "error" for diagnostic in diagnostics), "adapters": { "graph_store": runtime.graph_store.__class__.__name__, "event_log": runtime.event_log.__class__.__name__, "policy_gateway": runtime.policy_gateway.__class__.__name__, "audit_sink": runtime.audit_sink.__class__.__name__, "package_compiler": runtime.package_compiler.__class__.__name__, }, "config": config.to_dict(), "store": { "node_count": len(nodes), "stale_memory_count": len(stale), "pending_review_count": len(pending_review), }, "diagnostics": [diagnostic.to_dict() for diagnostic in diagnostics], } class LocalServiceRunner: """Minimal optional service runner shape without web framework dependency.""" def __init__(self, runtime: PhaseMemoryRuntime | None = None, config: RuntimeConfig | None = None) -> None: self.config = config or RuntimeConfig.local_default() self.runtime = runtime or runtime_from_config(self.config) def handle(self, operation: str, payload: dict[str, Any] | None = None) -> dict[str, Any]: payload = payload or {} if operation == "health.check": return health_report(self.runtime, config=self.config) if operation == "profile.plan": return self.runtime.plan_profile(payload["profile"], source_ref=payload.get("source_ref", "service")) if operation == "graph.import": return self.runtime.import_graph(payload["graph"], source_ref=payload.get("source_ref", "service")) if operation == "graph.activation.plan": budget = payload.get("budget", {}) return self.runtime.plan_activation( payload["graph"], max_items=int(budget["max_items"]), max_tokens=int(budget["max_tokens"]), profile_id=payload.get("profile_id"), ) raise ValueError(f"Unsupported service operation: {operation}") def kontextual_delegation_envelope( *, operation: str, graph_id: str = "", profile_id: str = "", policy_decision: dict[str, Any] | None = None, audit_ref: str = "", ) -> dict[str, Any]: return { "schema_version": KONTEXTUAL_DELEGATION_SCHEMA, "operation": operation, "phase_memory_owns": ["phase_policy", "lifecycle_planning", "activation_planning"], "kontextual_owns": ["durable_records", "permission_aware_retrieval", "long_lived_storage"], "graph_id": graph_id, "profile_id": profile_id, "policy_decision": dict(policy_decision or {}), "audit_ref": audit_ref, "imports": {"avoid_circular_imports": True, "exchange": "json_envelopes"}, } def assert_graph_store_conformance(store) -> None: profile = ProfileIntent(profile_id="conformance-profile") node = MemoryNode("node.conformance", "decision", "Conformance node") store.save_profile(profile) store.save_node(node) assert store.get_profile(profile.profile_id).profile_id == profile.profile_id assert store.get_node(node.node_id).node_id == node.node_id assert store.list_nodes(kind="decision") def assert_event_log_conformance(log) -> None: event = MemoryEvent("event.conformance", "recorded") log.append(event) assert log.list_events(kind="recorded")[0].event_id == event.event_id def assert_context_compiler_conformance(compiler) -> None: response = compiler.compile_selection({"id": "selection.conformance", "nodes": [], "events": []}) assert "package_id" in response or "package_ref" in response def assert_policy_gateway_conformance(gateway) -> None: decision = gateway.authorize(action="read", resource="node.conformance") assert isinstance(decision, PolicyDecision) def assert_audit_sink_conformance(sink) -> None: receipt = sink.record({"operation": "conformance"}) assert receipt.get("recorded") is True def default_conformance_adapters() -> dict[str, Any]: return { "graph_store": InMemoryMemoryGraphStore(), "event_log": InMemoryMemoryEventLog(), "context_compiler": NoopContextPackageCompiler(), "policy_gateway": AllowAllPolicyGateway(), "audit_sink": RecordingAuditSink(), }