Add service readiness contracts

This commit is contained in:
2026-05-18 20:33:45 +02:00
parent 1efb7d4c13
commit 2a4301d22f
7 changed files with 437 additions and 20 deletions

View File

@@ -45,6 +45,7 @@ from .retrieval import (
retrieve_graph_neighborhood,
select_event_path,
)
from .service import RuntimeConfig, health_report, service_contracts
from .planner import plan_profile_execution
from .runtime import PhaseMemoryRuntime
@@ -96,6 +97,9 @@ __all__ = [
"plan_neighborhood_activation",
"retrieve_graph_neighborhood",
"select_event_path",
"RuntimeConfig",
"health_report",
"service_contracts",
]
__version__ = "0.1.0"

189
src/phase_memory/service.py Normal file
View File

@@ -0,0 +1,189 @@
"""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(),
}