generated from coulomb/repo-seed
Implement live-shaped readiness workplan
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user