generated from coulomb/repo-seed
Add service readiness contracts
This commit is contained in:
@@ -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
189
src/phase_memory/service.py
Normal 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(),
|
||||
}
|
||||
Reference in New Issue
Block a user