Implement live-shaped readiness workplan

This commit is contained in:
2026-05-19 01:06:41 +02:00
parent 3a52b3df41
commit 635d999621
21 changed files with 1507 additions and 54 deletions

View File

@@ -10,6 +10,7 @@ from .bridge import (
package_response_envelope,
)
from .contracts import graph_from_markitect, profile_from_markitect
from .evaluation import EVALUATION_REPORT_SCHEMA, evaluation_threshold_report
from .external_adapters import (
ADAPTER_PACK_MANIFEST_SCHEMA,
ExternalAdapterPack,
@@ -20,9 +21,17 @@ from .external_adapters import (
FakeKontextualRuntimeRegistry,
FakeMarkitectPackageCompiler,
FakeTelemetryAuditSink,
LiveShapedKontextualEventLog,
LiveShapedKontextualGraphStore,
LiveShapedKontextualRuntimeRegistry,
LiveShapedMarkitectPackageCompiler,
LiveShapedPermissionSemanticIndex,
LiveShapedPolicyGateway,
LiveShapedTelemetryAuditSink,
adapter_pack_manifest,
fake_external_adapter_pack,
fake_external_runtime_config,
live_shaped_adapter_pack,
validate_adapter_pack_manifest,
)
from .lifecycle import (
@@ -67,6 +76,7 @@ from .retrieval import (
select_event_path,
)
from .service import LocalServiceRunner, RuntimeAdapterBundle, RuntimeConfig, health_report, resolve_runtime_adapters, runtime_from_config, service_contracts
from .service_binding import READINESS_REPORT_SCHEMA, SERVICE_BINDING_SCHEMA, ServiceBinding, ServiceResponse, service_binding_from_config
from .planner import plan_profile_execution
from .runtime import PhaseMemoryRuntime
@@ -75,6 +85,7 @@ __all__ = [
"ADAPTER_PACK_MANIFEST_SCHEMA",
"Diagnostic",
"ExternalAdapterPack",
"EVALUATION_REPORT_SCHEMA",
"FakeExternalEventLog",
"FakeExternalGraphStore",
"FakeExternalPolicyGateway",
@@ -82,6 +93,13 @@ __all__ = [
"FakeKontextualRuntimeRegistry",
"FakeMarkitectPackageCompiler",
"FakeTelemetryAuditSink",
"LiveShapedKontextualEventLog",
"LiveShapedKontextualGraphStore",
"LiveShapedKontextualRuntimeRegistry",
"LiveShapedMarkitectPackageCompiler",
"LiveShapedPermissionSemanticIndex",
"LiveShapedPolicyGateway",
"LiveShapedTelemetryAuditSink",
"LifecycleAction",
"LifecycleActionKind",
"LifecycleState",
@@ -113,6 +131,7 @@ __all__ = [
"compact_path",
"create_path",
"graph_from_markitect",
"evaluation_threshold_report",
"merge_path",
"make_review_record",
"plan_activation",
@@ -127,6 +146,7 @@ __all__ = [
"profile_from_markitect",
"fake_external_adapter_pack",
"fake_external_runtime_config",
"live_shaped_adapter_pack",
"adapter_pack_manifest",
"validate_adapter_pack_manifest",
"path_event",
@@ -140,9 +160,14 @@ __all__ = [
"RuntimeConfig",
"LocalServiceRunner",
"RuntimeAdapterBundle",
"READINESS_REPORT_SCHEMA",
"SERVICE_BINDING_SCHEMA",
"ServiceBinding",
"ServiceResponse",
"health_report",
"resolve_runtime_adapters",
"runtime_from_config",
"service_binding_from_config",
"service_contracts",
]

View File

@@ -3,13 +3,19 @@
from __future__ import annotations
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from .models import Diagnostic, MemoryEdge, MemoryEvent, MemoryGraph, MemoryNode, MemoryPath, PolicyDecision, ProfileIntent
from .utils import parse_iso_datetime, stable_digest, utc_now_iso
LOCAL_STORE_SCHEMA = "phase_memory.local_store.v1"
LOCAL_STORE_METADATA_FILE = "phase-memory.json"
LOCAL_STORE_MIGRATION_PLAN_SCHEMA = "phase_memory.local_store.migration_plan.v1"
LOCAL_STORE_MIGRATION_RESULT_SCHEMA = "phase_memory.local_store.migration_result.v1"
AUDIT_EXPORT_BATCH_SCHEMA = "phase_memory.audit.export_batch.v1"
AUDIT_RETENTION_PLAN_SCHEMA = "phase_memory.audit.retention_plan.v1"
class InMemoryMemoryGraphStore:
@@ -145,6 +151,109 @@ class FileBackedMemoryGraphStore:
def metadata(self) -> dict[str, Any]:
return _read_json(self.root / LOCAL_STORE_METADATA_FILE)
def migration_plan(self) -> dict[str, Any]:
metadata_path = self.root / LOCAL_STORE_METADATA_FILE
diagnostics = list(self.metadata_diagnostics())
metadata: dict[str, Any] = {}
schema_version = ""
if metadata_path.exists():
try:
metadata = _read_json(metadata_path)
schema_version = str(metadata.get("schema_version") or "")
except json.JSONDecodeError:
pass
actions: list[dict[str, Any]] = []
if not any(diagnostic.code == "corrupt_store_metadata" for diagnostic in diagnostics):
if schema_version != LOCAL_STORE_SCHEMA:
actions.append(
{
"id": "set_schema_version",
"action": "set_schema_version",
"from_schema_version": schema_version,
"to_schema_version": LOCAL_STORE_SCHEMA,
}
)
planned = metadata.get("planned_migrations") or metadata.get("migrations") or ()
for item in planned:
migration_id = str(item)
actions.append(
{
"id": f"complete_planned:{migration_id}",
"action": "complete_planned_migration",
"migration": migration_id,
}
)
plan_id = f"store-migration:{stable_digest([str(self.root), schema_version, actions])}"
return {
"schema_version": LOCAL_STORE_MIGRATION_PLAN_SCHEMA,
"id": plan_id,
"store_path": str(self.root),
"metadata_path": str(metadata_path),
"current_schema_version": schema_version,
"target_schema_version": LOCAL_STORE_SCHEMA,
"valid": not any(diagnostic.severity == "error" for diagnostic in diagnostics),
"dry_run": True,
"actions": actions,
"diagnostics": [diagnostic.to_dict() for diagnostic in diagnostics],
}
def apply_migration_plan(self, plan: dict[str, Any] | None = None, *, actor: str = "local") -> dict[str, Any]:
plan = dict(plan or self.migration_plan())
diagnostics = [dict(item) for item in plan.get("diagnostics", ())]
errors = [item for item in diagnostics if item.get("severity") == "error"]
if errors:
return {
"schema_version": LOCAL_STORE_MIGRATION_RESULT_SCHEMA,
"plan_id": plan.get("id", ""),
"store_path": str(self.root),
"applied": False,
"changed": False,
"actions": [],
"diagnostics": diagnostics,
}
metadata_path = self.root / LOCAL_STORE_METADATA_FILE
try:
metadata = _read_json(metadata_path) if metadata_path.exists() else {}
except json.JSONDecodeError:
metadata = {}
actions = [dict(item) for item in plan.get("actions", ())]
completed = list(metadata.get("completed_migrations") or ())
for action in actions:
if action.get("action") == "set_schema_version":
metadata["schema_version"] = LOCAL_STORE_SCHEMA
if action.get("action") == "complete_planned_migration":
completed.append(str(action.get("migration") or ""))
if actions:
metadata["schema_version"] = LOCAL_STORE_SCHEMA
metadata["migrations"] = []
metadata.pop("planned_migrations", None)
if completed:
metadata["completed_migrations"] = sorted({item for item in completed if item})
metadata["last_migration"] = {
"plan_id": str(plan.get("id") or ""),
"actor": actor,
"applied_at": utc_now_iso(),
"actions": [str(action.get("id") or "") for action in actions],
}
_write_json(metadata_path, metadata)
return {
"schema_version": LOCAL_STORE_MIGRATION_RESULT_SCHEMA,
"plan_id": plan.get("id", ""),
"store_path": str(self.root),
"applied": True,
"changed": bool(actions),
"actions": actions,
"metadata": metadata,
"diagnostics": diagnostics,
}
def repair_diagnostics(self, *, events: list[MemoryEvent] | None = None) -> tuple[Diagnostic, ...]:
diagnostics: list[Diagnostic] = []
nodes, node_diagnostics = _read_records(self.nodes_dir, MemoryNode.from_mapping, record_type="node")
@@ -323,6 +432,13 @@ class RecordingAuditSink:
def retention_metadata(self) -> dict[str, Any]:
return {"mode": "in_memory", "retention_days": None}
def retention_plan(self, *, retention_days: int | None = None, now: datetime | None = None) -> dict[str, Any]:
return audit_retention_plan(self.events, retention_days=retention_days, now=now, retention=self.retention_metadata())
def export_batch(self, **filters: Any) -> dict[str, Any]:
events = self.query(**filters)
return audit_export_batch(events, filters=filters, retention=self.retention_metadata())
class JsonlAuditSink:
def __init__(self, path: str | Path) -> None:
@@ -352,6 +468,13 @@ class JsonlAuditSink:
def retention_metadata(self) -> dict[str, Any]:
return {"mode": "jsonl", "path": str(self.path), "retention_days": None}
def retention_plan(self, *, retention_days: int | None = None, now: datetime | None = None) -> dict[str, Any]:
return audit_retention_plan(self.query(), retention_days=retention_days, now=now, retention=self.retention_metadata())
def export_batch(self, **filters: Any) -> dict[str, Any]:
events = self.query(**filters)
return audit_export_batch(events, filters=filters, retention=self.retention_metadata())
class InMemorySemanticIndex:
def __init__(self) -> None:
@@ -429,6 +552,54 @@ def filter_audit_events(events: list[dict[str, Any]], **filters: Any) -> list[di
return [dict(event) for event in events if _audit_event_matches(event, filters)]
def audit_export_batch(
events: list[dict[str, Any]],
*,
filters: dict[str, Any] | None = None,
retention: dict[str, Any] | None = None,
) -> dict[str, Any]:
return {
"schema_version": AUDIT_EXPORT_BATCH_SCHEMA,
"id": f"audit-export:{stable_digest([filters or {}, events])}",
"filters": dict(filters or {}),
"count": len(events),
"events": [dict(event) for event in events],
"retention": dict(retention or {}),
}
def audit_retention_plan(
events: list[dict[str, Any]],
*,
retention_days: int | None = None,
now: datetime | None = None,
retention: dict[str, Any] | None = None,
) -> dict[str, Any]:
retention = dict(retention or {})
if retention_days is None:
retention_days = retention.get("retention_days")
now = now or datetime.now(timezone.utc)
eligible: list[str] = []
retained: list[str] = []
for event in events:
event_id = str(event.get("operation_id") or event.get("id") or stable_digest(event))
age = _event_age_days(event, now=now)
if retention_days is not None and age is not None and age >= int(retention_days):
eligible.append(event_id)
else:
retained.append(event_id)
return {
"schema_version": AUDIT_RETENTION_PLAN_SCHEMA,
"id": f"audit-retention:{stable_digest([retention_days, eligible, retained])}",
"retention_days": retention_days,
"eligible_count": len(eligible),
"retained_count": len(retained),
"eligible_operation_ids": eligible,
"retained_operation_ids": retained,
"retention": retention,
}
def _audit_event_matches(event: dict[str, Any], filters: dict[str, Any]) -> bool:
operation = filters.get("operation")
if operation is not None and event.get("operation") != operation:
@@ -455,3 +626,10 @@ def _audit_event_matches(event: dict[str, Any], filters: dict[str, Any]) -> bool
if allowed is not None and bool(event.get("allowed")) is not bool(allowed):
return False
return True
def _event_age_days(event: dict[str, Any], *, now: datetime) -> int | None:
timestamp = parse_iso_datetime(str(event.get("timestamp") or ""))
if timestamp is None:
return None
return max((now - timestamp).days, 0)

View File

@@ -0,0 +1,161 @@
"""Evaluation threshold reports for deterministic scenario fixtures."""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any
from .adapters import InMemorySemanticIndex
from .contracts import graph_from_markitect
from .models import Diagnostic, MemoryPath
from .retrieval import activation_quality_report, select_event_path
from .runtime import PhaseMemoryRuntime
EVALUATION_REPORT_SCHEMA = "phase_memory.evaluation.threshold_report.v1"
DEFAULT_THRESHOLDS = {
"policy_denial_count": 1,
"lifecycle_action_count": 3,
"path_event_count": 1,
"semantic_hit_count": 1,
"budget_omission_count": 1,
"source_span_coverage": 1.0,
"explanation_coverage": 1.0,
}
def evaluation_threshold_report(data: dict[str, Any], *, thresholds: dict[str, float] | None = None) -> dict[str, Any]:
thresholds = {**DEFAULT_THRESHOLDS, **dict(thresholds or {})}
scenarios = list(data.get("scenarios") or ())
metrics = {
"scenario_count": len(scenarios),
"policy_denial_count": 0,
"lifecycle_action_count": 0,
"path_event_count": 0,
"semantic_hit_count": 0,
"budget_omission_count": 0,
"source_span_coverage": 0.0,
"explanation_coverage": 0.0,
}
scenario_reports: list[dict[str, Any]] = []
for scenario in scenarios:
scenario_id = str(scenario.get("id") or "")
if scenario_id == "policy-denied-activation":
report = _policy_scenario(scenario)
elif scenario_id == "profile-lifecycle-rules":
report = _lifecycle_scenario(scenario)
elif scenario_id == "budget-path-and-semantic-hints":
report = _budget_scenario(scenario)
else:
report = {"id": scenario_id, "metrics": {}, "diagnostics": [{"severity": "warn", "code": "unknown_scenario", "message": "Scenario is not recognized by this report."}]}
scenario_reports.append(report)
for key, value in report.get("metrics", {}).items():
if key in metrics and isinstance(value, (int, float)):
metrics[key] += value
diagnostics = _threshold_diagnostics(metrics, thresholds)
return {
"schema_version": EVALUATION_REPORT_SCHEMA,
"valid": not diagnostics,
"metrics": metrics,
"thresholds": thresholds,
"scenarios": scenario_reports,
"diagnostics": [diagnostic.to_dict() for diagnostic in diagnostics],
}
def _policy_scenario(scenario: dict[str, Any]) -> dict[str, Any]:
runtime = PhaseMemoryRuntime()
response = runtime.plan_activation(
scenario["graph"],
max_items=int(scenario["profile"].get("activation", {}).get("max_items") or 4),
max_tokens=int(scenario["profile"].get("activation", {}).get("max_tokens") or 60),
profile_id=scenario["profile"]["id"],
policy_context={"denied_labels": ["restricted"], "secrets_allowed": False, "trust_zone": "local"},
)
denials = response["data"]["policy_denials"]
return {
"id": scenario["id"],
"metrics": {"policy_denial_count": len(denials)},
"diagnostics": response["diagnostics"],
}
def _lifecycle_scenario(scenario: dict[str, Any]) -> dict[str, Any]:
runtime = PhaseMemoryRuntime()
response = runtime.plan_lifecycle_with_profile(
scenario["profile"],
scenario["graph"],
refresh_digests={"life.decision": "decision-new"},
now=datetime(2026, 5, 18, tzinfo=timezone.utc),
)
return {
"id": scenario["id"],
"metrics": {"lifecycle_action_count": len(response["data"]["dry_run_actions"])},
"diagnostics": response["diagnostics"],
}
def _budget_scenario(scenario: dict[str, Any]) -> dict[str, Any]:
runtime = PhaseMemoryRuntime()
graph = graph_from_markitect(scenario["graph"]).value
activation = runtime.plan_activation(
scenario["graph"],
max_items=int(scenario["profile"]["activation"]["max_items"]),
max_tokens=int(scenario["profile"]["activation"]["max_tokens"]),
profile_id=scenario["profile"]["id"],
priority_node_ids=tuple(scenario["expect"]["selected_node_ids"]),
)
plan = activation["data"]["activation_plan"]
quality = activation_quality_report(_activation_plan_from_response(activation), expected_node_ids=tuple(scenario["expect"]["selected_node_ids"]))
path_events = select_event_path(graph.events, MemoryPath.from_mapping(scenario["path"]), max_events=2)
index = InMemorySemanticIndex()
index.upsert_nodes(list(graph.nodes))
semantic_hits = index.query(graph_id=graph.graph_id, query="semantic restart", limit=2)
return {
"id": scenario["id"],
"metrics": {
"path_event_count": len(path_events),
"semantic_hit_count": 1 if semantic_hits and semantic_hits[0]["id"] == scenario["expect"]["semantic_top_id"] else 0,
"budget_omission_count": len(plan["omitted"]),
"source_span_coverage": quality["source_span_coverage"],
"explanation_coverage": quality["explanation_coverage"],
},
"diagnostics": activation["diagnostics"],
}
def _activation_plan_from_response(response: dict[str, Any]):
from .models import ActivationPlan
data = response["data"]["activation_plan"]
return ActivationPlan(
plan_id=data["plan_id"],
graph_id=data["graph_id"],
selected_node_ids=tuple(data["selected_node_ids"]),
selected_event_ids=tuple(data["selected_event_ids"]),
omitted=tuple(data["omitted"]),
token_estimate=data["token_estimate"],
max_items=data["max_items"],
max_tokens=data["max_tokens"],
selection=response["data"]["package_request"]["selection"],
diagnostics=(),
)
def _threshold_diagnostics(metrics: dict[str, Any], thresholds: dict[str, float]) -> tuple[Diagnostic, ...]:
diagnostics: list[Diagnostic] = []
for key, threshold in sorted(thresholds.items()):
actual = float(metrics.get(key) or 0)
if actual < float(threshold):
diagnostics.append(
Diagnostic(
"error",
"evaluation_threshold_failed",
"Evaluation metric did not meet its threshold.",
key,
{"actual": actual, "threshold": threshold},
)
)
return tuple(diagnostics)

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from .adapters import InMemoryMemoryEventLog, InMemoryMemoryGraphStore, InMemorySemanticIndex, filter_audit_events
from .adapters import InMemoryMemoryEventLog, InMemoryMemoryGraphStore, InMemorySemanticIndex, audit_export_batch, audit_retention_plan, filter_audit_events
from .models import Diagnostic, PolicyDecision
from .service import RuntimeConfig
from .utils import stable_digest
@@ -47,6 +47,7 @@ class ExternalAdapterPack:
capabilities: tuple[str, ...] = ()
ownership_boundaries: dict[str, str] = field(default_factory=dict)
required_conformance: dict[str, str] = field(default_factory=dict)
capability_requirements: dict[str, tuple[str, ...]] = field(default_factory=dict)
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
@@ -56,6 +57,7 @@ class ExternalAdapterPack:
"capabilities": list(self.capabilities),
"ownership_boundaries": dict(self.ownership_boundaries),
"required_conformance": dict(self.required_conformance),
"capability_requirements": {key: list(value) for key, value in sorted(self.capability_requirements.items())},
"metadata": dict(self.metadata),
}
@@ -137,6 +139,13 @@ class FakeTelemetryAuditSink:
"retention_days": self.retention_days,
}
def retention_plan(self, *, retention_days: int | None = None, now=None) -> dict[str, Any]:
return audit_retention_plan(self.events, retention_days=retention_days, now=now, retention=self.retention_metadata())
def export_batch(self, **filters: Any) -> dict[str, Any]:
events = self.query(**filters)
return audit_export_batch(events, filters=filters, retention=self.retention_metadata())
class FakeKontextualRuntimeRegistry:
def __init__(self) -> None:
@@ -192,6 +201,99 @@ def fake_external_adapter_pack() -> ExternalAdapterPack:
)
class LiveShapedKontextualGraphStore(FakeExternalGraphStore):
"""Live-shaped Kontextual graph store fixture with deterministic behavior."""
class LiveShapedKontextualEventLog(FakeExternalEventLog):
"""Live-shaped Kontextual event log fixture with deterministic behavior."""
class LiveShapedPermissionSemanticIndex(FakeExternalSemanticIndex):
"""Live-shaped retrieval fixture that keeps tests local and deterministic."""
class LiveShapedMarkitectPackageCompiler(FakeMarkitectPackageCompiler):
def compile_selection(self, selection: dict[str, Any]) -> dict[str, Any]:
response = super().compile_selection(selection)
package_ref = f"markitect-live-shaped:{stable_digest(selection)}"
return {
**response,
"package_id": package_ref,
"package_ref": package_ref,
"compiler": self.__class__.__name__,
"transport": "fixture",
}
class LiveShapedPolicyGateway(FakeExternalPolicyGateway):
"""Live-shaped policy gateway fixture with explicit deny action support."""
class LiveShapedTelemetryAuditSink(FakeTelemetryAuditSink):
def retention_metadata(self) -> dict[str, Any]:
return {
"mode": "live_shaped_telemetry",
"retention_days": self.retention_days,
"transport": "fixture",
}
class LiveShapedKontextualRuntimeRegistry(FakeKontextualRuntimeRegistry):
def publish_runtime_envelope(self, envelope: dict[str, Any]) -> dict[str, Any]:
reference = f"kontextual-live-shaped:{stable_digest(envelope)}"
stored = dict(envelope)
self._envelopes[reference] = stored
return {
"published": True,
"reference": reference,
"adapter": self.__class__.__name__,
"envelope": stored,
}
def live_shaped_adapter_pack() -> ExternalAdapterPack:
capability_requirements = {
"graph_store": ("kontextual.graph-store.live-shaped",),
"event_log": ("kontextual.event-log.live-shaped",),
"policy_gateway": ("policy.gateway.live-shaped",),
"audit_sink": ("telemetry.audit.live-shaped",),
"package_compiler": ("markitect.package.compile.live-shaped",),
"semantic_index": ("semantic-index.live-shaped",),
"runtime_registry": ("kontextual.runtime.registry.live-shaped",),
}
ownership_boundaries = {
"graph_store": "kontextual owns durable graph records; live fixture preserves phase-memory graph semantics",
"event_log": "kontextual owns event durability; live fixture preserves phase-memory event shape",
"policy_gateway": "external policy owns decision enforcement; phase-memory owns operation context",
"audit_sink": "external telemetry owns retention/export; phase-memory owns audit event schema",
"package_compiler": "markitect owns package compilation; phase-memory owns activation selection",
"semantic_index": "external retrieval owns index ranking; phase-memory owns policy-filtered activation",
"runtime_registry": "kontextual owns registry transport; phase-memory owns runtime envelope contract",
}
return ExternalAdapterPack(
name="live-shaped",
adapters={
"graph_store": LiveShapedKontextualGraphStore(),
"event_log": LiveShapedKontextualEventLog(),
"policy_gateway": LiveShapedPolicyGateway(),
"audit_sink": LiveShapedTelemetryAuditSink(retention_days=90),
"package_compiler": LiveShapedMarkitectPackageCompiler(),
"semantic_index": LiveShapedPermissionSemanticIndex(),
"runtime_registry": LiveShapedKontextualRuntimeRegistry(),
},
capabilities=tuple(sorted({capability for values in capability_requirements.values() for capability in values})),
ownership_boundaries=ownership_boundaries,
required_conformance=dict(ADAPTER_CONFORMANCE_HELPERS),
capability_requirements=capability_requirements,
metadata={
"intended_for": "local live-shaped compatibility tests",
"requires_credentials": False,
"network_required": False,
},
)
def fake_external_runtime_config() -> RuntimeConfig:
return RuntimeConfig(
adapter_registry={
@@ -213,6 +315,7 @@ def fake_external_runtime_config() -> RuntimeConfig:
def adapter_pack_manifest(pack: ExternalAdapterPack) -> dict[str, Any]:
capability_requirements = _capability_requirements(pack)
return {
"schema_version": ADAPTER_PACK_MANIFEST_SCHEMA,
"name": pack.name,
@@ -222,7 +325,7 @@ def adapter_pack_manifest(pack: ExternalAdapterPack) -> dict[str, Any]:
key: {
"class": pack.adapters[key].__class__.__name__,
"ownership": pack.ownership_boundaries.get(key, ""),
"required_capabilities": list(ADAPTER_REQUIRED_CAPABILITIES.get(key, ())),
"required_capabilities": list(capability_requirements.get(key, ())),
"required_conformance": pack.required_conformance.get(key, ADAPTER_CONFORMANCE_HELPERS.get(key, "")),
}
for key in sorted(pack.adapters)
@@ -233,6 +336,7 @@ def adapter_pack_manifest(pack: ExternalAdapterPack) -> dict[str, Any]:
def validate_adapter_pack_manifest(pack: ExternalAdapterPack) -> tuple[Diagnostic, ...]:
diagnostics: list[Diagnostic] = []
capabilities = set(pack.capabilities)
capability_requirements = _capability_requirements(pack)
for adapter in ADAPTER_PACK_REQUIRED_ADAPTERS:
if adapter not in pack.adapters:
diagnostics.append(
@@ -265,7 +369,7 @@ def validate_adapter_pack_manifest(pack: ExternalAdapterPack) -> tuple[Diagnosti
{"adapter": adapter},
)
)
for capability in ADAPTER_REQUIRED_CAPABILITIES.get(adapter, ()):
for capability in capability_requirements.get(adapter, ()):
if capability not in capabilities:
diagnostics.append(
Diagnostic(
@@ -277,3 +381,9 @@ def validate_adapter_pack_manifest(pack: ExternalAdapterPack) -> tuple[Diagnosti
)
)
return tuple(diagnostics)
def _capability_requirements(pack: ExternalAdapterPack) -> dict[str, tuple[str, ...]]:
if pack.capability_requirements:
return {key: tuple(value) for key, value in pack.capability_requirements.items()}
return dict(ADAPTER_REQUIRED_CAPABILITIES)

View File

@@ -22,6 +22,8 @@ class MemoryOperation(str, Enum):
ACTIVATION_PLAN = "graph.activation.plan"
PACKAGE_COMPILE = "package.compile"
AUDIT_QUERY = "audit.query"
AUDIT_EXPORT = "audit.export"
AUDIT_RETENTION_PLAN = "audit.retention.plan"
LIFECYCLE_APPLY = "lifecycle.apply"
STABILIZATION = "memory.stabilize"
COMPACTION = "memory.compact"
@@ -30,6 +32,8 @@ class MemoryOperation(str, Enum):
ARCHIVE = "memory.archive"
GRAPH_EXPORT = "graph.export"
STORE_REPAIR = "store.repair.diagnostics"
STORE_MIGRATION_PLAN = "store.migration.plan"
STORE_MIGRATION_APPLY = "store.migration.apply"
POLICY_OPERATION_POINTS = tuple(operation.value for operation in MemoryOperation)

View File

@@ -38,6 +38,8 @@ from .utils import compact_dict, stable_digest, to_plain
RUNTIME_ENVELOPE_SCHEMA = "phase_memory.runtime.envelope.v1"
AUDIT_QUERY_SCHEMA = "phase_memory.audit.query.v1"
AUDIT_EXPORT_SCHEMA = "phase_memory.audit.export.v1"
AUDIT_RETENTION_SCHEMA = "phase_memory.audit.retention.v1"
PACKAGE_REQUEST_SCHEMA = MARKITECT_PACKAGE_REQUEST_SCHEMA
@@ -300,6 +302,103 @@ class PhaseMemoryRuntime:
"source": {"ref": source_ref},
}
def export_audit_events(self, filters: dict[str, Any] | None = None, *, source_ref: str = "audit") -> dict[str, Any]:
filters = _audit_filters(filters or {})
policy = self.policy_gateway.authorize(
action="audit.export",
resource="audit:events",
context={"source_ref": source_ref, "dry_run": True, "filters": filters},
)
if policy.allowed and hasattr(self.audit_sink, "export_batch"):
batch = self.audit_sink.export_batch(**filters)
diagnostics: tuple[Diagnostic, ...] = ()
elif policy.allowed:
events, diagnostics = _query_audit_sink(self.audit_sink, filters)
batch = {
"schema_version": "phase_memory.audit.export_batch.v1",
"filters": filters,
"count": len(events),
"events": events,
"retention": _audit_retention_metadata(self.audit_sink),
}
else:
diagnostics = ()
batch = {"filters": filters, "count": 0, "events": [], "retention": _audit_retention_metadata(self.audit_sink)}
operation_id = f"op:{stable_digest(['audit.export', source_ref, filters])}"
audit = self.audit_sink.record(
audit_event(
operation_id=operation_id,
operation="audit.export",
subject={"kind": "audit_events", "id": stable_digest(filters)},
policy_decision=policy,
dry_run=True,
source_ref=source_ref,
)
)
return {
"schema_version": AUDIT_EXPORT_SCHEMA,
"operation_id": operation_id,
"operation": "audit.export",
"dry_run": True,
"valid": policy.allowed and not any(diagnostic.severity == "error" for diagnostic in diagnostics),
"batch": batch,
"policy_decision": _policy_to_dict(policy),
"audit_receipt": audit,
"diagnostics": [diagnostic.to_dict() for diagnostic in diagnostics],
"source": {"ref": source_ref},
}
def audit_retention_plan(
self,
*,
retention_days: int | None = None,
now: datetime | None = None,
source_ref: str = "audit",
) -> dict[str, Any]:
policy = self.policy_gateway.authorize(
action="audit.retention.plan",
resource="audit:events",
context={"source_ref": source_ref, "dry_run": True, "retention_days": retention_days},
)
diagnostics: tuple[Diagnostic, ...] = ()
if policy.allowed and hasattr(self.audit_sink, "retention_plan"):
plan = self.audit_sink.retention_plan(retention_days=retention_days, now=now)
elif policy.allowed:
plan = {}
diagnostics = (
Diagnostic(
"error",
"audit_retention_plan_unsupported",
"Audit sink does not expose retention planning.",
self.audit_sink.__class__.__name__,
),
)
else:
plan = {}
operation_id = f"op:{stable_digest(['audit.retention.plan', source_ref, retention_days, plan])}"
audit = self.audit_sink.record(
audit_event(
operation_id=operation_id,
operation="audit.retention.plan",
subject={"kind": "audit_events", "id": "retention"},
policy_decision=policy,
dry_run=True,
source_ref=source_ref,
)
)
return {
"schema_version": AUDIT_RETENTION_SCHEMA,
"operation_id": operation_id,
"operation": "audit.retention.plan",
"dry_run": True,
"valid": policy.allowed and not any(diagnostic.severity == "error" for diagnostic in diagnostics),
"plan": plan,
"policy_decision": _policy_to_dict(policy),
"audit_receipt": audit,
"diagnostics": [diagnostic.to_dict() for diagnostic in diagnostics],
"source": {"ref": source_ref},
}
def export_graph(self, *, graph_id: str = "local", source_ref: str = "local-store") -> dict[str, Any]:
events = self.event_log.list_events()
if hasattr(self.graph_store, "export_graph"):
@@ -335,6 +434,63 @@ class PhaseMemoryRuntime:
data={"diagnostic_count": len(diagnostics)},
)
def plan_store_migration(self, *, source_ref: str = "local-store") -> dict[str, Any]:
if hasattr(self.graph_store, "migration_plan"):
plan = self.graph_store.migration_plan()
diagnostics = _diagnostics_from_dicts(plan.get("diagnostics", ()))
else:
plan = {}
diagnostics = (
Diagnostic(
"error",
"store_migration_unsupported",
"Graph store does not expose local migration planning.",
self.graph_store.__class__.__name__,
),
)
return self._envelope(
"store.migration.plan",
subject_kind="local_store",
subject_id=source_ref,
valid=not any(diagnostic.severity == "error" for diagnostic in diagnostics),
diagnostics=diagnostics,
source_ref=source_ref,
data={"migration_plan": plan},
)
def apply_store_migration(
self,
migration_plan: dict[str, Any] | None = None,
*,
actor: str = "local",
source_ref: str = "local-store",
) -> dict[str, Any]:
if hasattr(self.graph_store, "apply_migration_plan"):
result = self.graph_store.apply_migration_plan(migration_plan, actor=actor)
diagnostics = _diagnostics_from_dicts(result.get("diagnostics", ()))
valid = bool(result.get("applied")) and not any(diagnostic.severity == "error" for diagnostic in diagnostics)
else:
result = {}
diagnostics = (
Diagnostic(
"error",
"store_migration_unsupported",
"Graph store does not expose local migration apply.",
self.graph_store.__class__.__name__,
),
)
valid = False
return self._envelope(
"store.migration.apply",
subject_kind="local_store",
subject_id=source_ref,
valid=valid,
diagnostics=diagnostics,
source_ref=source_ref,
dry_run=False,
data={"migration_result": result},
)
def apply_lifecycle_actions(
self,
actions: Iterable[LifecycleAction | dict[str, Any]],
@@ -487,6 +643,21 @@ def _policy_to_dict(decision: PolicyDecision) -> dict[str, Any]:
return decision.to_dict() if hasattr(decision, "to_dict") else to_plain(decision)
def _diagnostics_from_dicts(items: Iterable[dict[str, Any]]) -> tuple[Diagnostic, ...]:
diagnostics: list[Diagnostic] = []
for item in items:
diagnostics.append(
Diagnostic(
str(item.get("severity") or "info"),
str(item.get("code") or "diagnostic"),
str(item.get("message") or ""),
str(item.get("path") or ""),
dict(item.get("metadata") or {}),
)
)
return tuple(diagnostics)
def _audit_filters(filters: dict[str, Any]) -> dict[str, Any]:
allowed_keys = {
"operation",

View File

@@ -335,6 +335,8 @@ def health_report(runtime: PhaseMemoryRuntime, *, config: RuntimeConfig | None =
class LocalServiceRunner:
"""Minimal optional service runner shape without web framework dependency."""
SUPPORTED_OPERATIONS = tuple(SERVICE_OPERATIONS)
def __init__(
self,
runtime: PhaseMemoryRuntime | None = None,
@@ -345,6 +347,10 @@ class LocalServiceRunner:
self.config = config or RuntimeConfig.local_default()
self.runtime = runtime or runtime_from_config(self.config, external_adapters=external_adapters)
@classmethod
def supported_operations(cls) -> tuple[str, ...]:
return cls.SUPPORTED_OPERATIONS
def handle(self, operation: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
payload = payload or {}
if operation == "health.check":

View File

@@ -0,0 +1,164 @@
"""Optional framework-neutral service binding for local embeddings."""
from __future__ import annotations
import json
from dataclasses import dataclass, field
from io import BytesIO
from typing import Any, Callable
from .service import HEALTH_REPORT_SCHEMA, LocalServiceRunner, RuntimeConfig, health_report, service_contracts
SERVICE_BINDING_SCHEMA = "phase_memory.service.binding.v1"
READINESS_REPORT_SCHEMA = "phase_memory.service.readiness.v1"
@dataclass(frozen=True)
class ServiceResponse:
status: int
body: dict[str, Any]
headers: dict[str, str] = field(default_factory=lambda: {"content-type": "application/json"})
def to_wsgi(self) -> tuple[str, list[tuple[str, str]], bytes]:
status_text = _status_text(self.status)
payload = json.dumps(self.body, sort_keys=True, separators=(",", ":")).encode("utf-8")
headers = {**self.headers, "content-length": str(len(payload))}
return status_text, list(headers.items()), payload
class ServiceBinding:
"""HTTP-shaped adapter around ``LocalServiceRunner`` without a server dependency."""
def __init__(self, runner: LocalServiceRunner | None = None) -> None:
self.runner = runner or LocalServiceRunner()
def readiness(self) -> dict[str, Any]:
health = health_report(self.runner.runtime, config=self.runner.config)
contracts = service_contracts()
supported = set(self.runner.supported_operations())
declared = set(contracts["operations"])
unsupported = sorted(declared - supported)
extra = sorted(supported - declared)
diagnostics = list(health["diagnostics"])
if unsupported:
diagnostics.append(
{
"severity": "error",
"code": "unsupported_service_operations",
"message": "Service binding cannot dispatch every declared operation.",
"metadata": {"operations": unsupported},
}
)
if extra:
diagnostics.append(
{
"severity": "warn",
"code": "undocumented_service_operations",
"message": "Service binding exposes operations not declared in service contracts.",
"metadata": {"operations": extra},
}
)
ok = health["ok"] and not any(item.get("severity") == "error" for item in diagnostics)
return {
"schema_version": READINESS_REPORT_SCHEMA,
"ok": ok,
"health": health,
"contracts": contracts,
"supported_operations": sorted(supported),
"unsupported_operations": unsupported,
"extra_operations": extra,
"diagnostics": diagnostics,
}
def route(self, method: str, path: str, body: dict[str, Any] | None = None) -> ServiceResponse:
method = method.upper()
path = _normalize_path(path)
body = body or {}
if method == "GET" and path == "/health":
health = health_report(self.runner.runtime, config=self.runner.config)
return ServiceResponse(200 if health["ok"] else 503, health)
if method == "GET" and path == "/ready":
readiness = self.readiness()
return ServiceResponse(200 if readiness["ok"] else 503, readiness)
if method == "GET" and path == "/contracts":
return ServiceResponse(200, service_contracts())
if method == "POST" and path == "/operations":
operation = str(body.get("operation") or "")
return self._dispatch(operation, dict(body.get("payload") or {}))
if method == "POST" and path.startswith("/operations/"):
operation = path.removeprefix("/operations/").replace("/", ".")
return self._dispatch(operation, body)
return ServiceResponse(
404,
{
"schema_version": SERVICE_BINDING_SCHEMA,
"ok": False,
"error": "not_found",
"method": method,
"path": path,
},
)
def as_wsgi_app(self) -> Callable:
def app(environ: dict[str, Any], start_response: Callable) -> list[bytes]:
method = str(environ.get("REQUEST_METHOD") or "GET")
path = str(environ.get("PATH_INFO") or "/")
body = _read_wsgi_json(environ)
response = self.route(method, path, body)
status, headers, payload = response.to_wsgi()
start_response(status, headers)
return [payload]
return app
def _dispatch(self, operation: str, payload: dict[str, Any]) -> ServiceResponse:
if operation not in service_contracts()["operations"]:
return ServiceResponse(
404,
{
"schema_version": SERVICE_BINDING_SCHEMA,
"ok": False,
"error": "unsupported_operation",
"operation": operation,
},
)
try:
return ServiceResponse(200, self.runner.handle(operation, payload))
except (KeyError, TypeError, ValueError) as exc:
return ServiceResponse(
400,
{
"schema_version": SERVICE_BINDING_SCHEMA,
"ok": False,
"error": "invalid_request",
"operation": operation,
"message": str(exc),
},
)
def service_binding_from_config(config: RuntimeConfig | None = None) -> ServiceBinding:
return ServiceBinding(LocalServiceRunner(config=config or RuntimeConfig.local_default()))
def _normalize_path(path: str) -> str:
normalized = "/" + path.strip("/")
return normalized if normalized != "/" else "/"
def _status_text(status: int) -> str:
labels = {200: "OK", 400: "Bad Request", 404: "Not Found", 503: "Service Unavailable"}
return f"{status} {labels.get(status, 'Unknown')}"
def _read_wsgi_json(environ: dict[str, Any]) -> dict[str, Any]:
try:
length = int(environ.get("CONTENT_LENGTH") or 0)
except ValueError:
length = 0
stream = environ.get("wsgi.input") or BytesIO()
raw = stream.read(length) if length else b""
if not raw:
return {}
return json.loads(raw.decode("utf-8"))