Add phase_memory.management for cross-store discovery and windowed activity reporting. Extend the phase-memory CLI with stores list and report commands, plus make report-7d for the default weekly operator view.
713 lines
No EOL
26 KiB
Python
713 lines
No EOL
26 KiB
Python
"""Federated store discovery and activity reporting."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
from typing import Any, Mapping
|
|
|
|
from .adapters import LOCAL_STORE_SCHEMA, FileBackedMemoryGraphStore, JsonlAuditSink, JsonlMemoryEventLog
|
|
from .models import Diagnostic
|
|
from .ops_warden import OPS_WARDEN_PROFILE_ID, OPS_WARDEN_RUNTIME_SCHEMA, OpsWardenMemoryStore, default_memory_store_path
|
|
from .utils import parse_iso_datetime, stable_digest, utc_now_iso
|
|
|
|
STORE_REGISTRY_SCHEMA = "phase_memory.management.store_registry.v1"
|
|
STORE_LIST_SCHEMA = "phase_memory.management.store_list.v1"
|
|
FEDERATED_REPORT_SCHEMA = "phase_memory.management.federated_report.v1"
|
|
|
|
STORE_KIND_OPS_WARDEN = "ops_warden_coordination"
|
|
STORE_KIND_LOCAL_GRAPH = "local_graph"
|
|
|
|
DEFAULT_OPS_WARDEN_STORE_ID = "ops-warden-default"
|
|
TOP_ROUTE_IDS = 10
|
|
DETAIL_EPISODE_LIMIT = 20
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MemoryStoreDescriptor:
|
|
store_id: str
|
|
path: Path
|
|
store_kind: str
|
|
profile_id: str = ""
|
|
label: str = ""
|
|
source: str = "discovered"
|
|
|
|
|
|
def default_registry_path(environ: Mapping[str, str] | None = None) -> Path:
|
|
environ = environ or os.environ
|
|
override = str(environ.get("PHASE_MEMORY_REGISTRY") or "").strip()
|
|
if override:
|
|
return Path(override).expanduser()
|
|
xdg_data = str(environ.get("XDG_DATA_HOME") or "").strip()
|
|
base = Path(xdg_data).expanduser() if xdg_data else Path.home() / ".local" / "share"
|
|
return base / "phase-memory" / "stores.json"
|
|
|
|
|
|
def classify_store(path: str | Path) -> str | None:
|
|
root = Path(path).expanduser().resolve()
|
|
local_metadata = root / "phase-memory.json"
|
|
if local_metadata.is_file():
|
|
try:
|
|
data = json.loads(local_metadata.read_text(encoding="utf-8"))
|
|
except json.JSONDecodeError:
|
|
return None
|
|
if str(data.get("schema_version") or "") == LOCAL_STORE_SCHEMA:
|
|
return STORE_KIND_LOCAL_GRAPH
|
|
|
|
ops_metadata = root / "metadata.json"
|
|
if ops_metadata.is_file():
|
|
try:
|
|
data = json.loads(ops_metadata.read_text(encoding="utf-8"))
|
|
except json.JSONDecodeError:
|
|
return None
|
|
if str(data.get("schema_version") or "") == OPS_WARDEN_RUNTIME_SCHEMA:
|
|
return STORE_KIND_OPS_WARDEN
|
|
return None
|
|
|
|
|
|
def load_store_registry(
|
|
path: str | Path | None = None,
|
|
environ: Mapping[str, str] | None = None,
|
|
) -> dict[str, Any]:
|
|
registry_path = Path(path).expanduser() if path is not None else default_registry_path(environ)
|
|
diagnostics: list[dict[str, Any]] = []
|
|
if not registry_path.is_file():
|
|
return {
|
|
"schema_version": STORE_REGISTRY_SCHEMA,
|
|
"valid": True,
|
|
"registry_path": str(registry_path),
|
|
"entries": [],
|
|
"diagnostics": diagnostics,
|
|
}
|
|
try:
|
|
payload = json.loads(registry_path.read_text(encoding="utf-8"))
|
|
except json.JSONDecodeError as exc:
|
|
return {
|
|
"schema_version": STORE_REGISTRY_SCHEMA,
|
|
"valid": False,
|
|
"registry_path": str(registry_path),
|
|
"entries": [],
|
|
"diagnostics": [
|
|
Diagnostic(
|
|
"warn",
|
|
"corrupt_store_registry",
|
|
"Store registry file is not valid JSON.",
|
|
str(registry_path),
|
|
{"error": str(exc)},
|
|
).to_dict()
|
|
],
|
|
}
|
|
if not isinstance(payload, dict):
|
|
diagnostics.append(
|
|
Diagnostic(
|
|
"warn",
|
|
"invalid_store_registry_shape",
|
|
"Store registry must be a JSON object.",
|
|
str(registry_path),
|
|
).to_dict()
|
|
)
|
|
return {
|
|
"schema_version": STORE_REGISTRY_SCHEMA,
|
|
"valid": False,
|
|
"registry_path": str(registry_path),
|
|
"entries": [],
|
|
"diagnostics": diagnostics,
|
|
}
|
|
|
|
schema = str(payload.get("schema_version") or "")
|
|
if schema and schema != STORE_REGISTRY_SCHEMA:
|
|
diagnostics.append(
|
|
Diagnostic(
|
|
"warn",
|
|
"unknown_store_registry_schema",
|
|
"Store registry declares an unknown schema version.",
|
|
str(registry_path),
|
|
{"schema_version": schema},
|
|
).to_dict()
|
|
)
|
|
|
|
entries: list[dict[str, Any]] = []
|
|
for item in payload.get("stores") or ():
|
|
if not isinstance(item, dict):
|
|
continue
|
|
store_path = str(item.get("path") or "").strip()
|
|
store_kind = str(item.get("store_kind") or "").strip()
|
|
store_id = str(item.get("store_id") or "").strip()
|
|
if not store_path or not store_kind or not store_id:
|
|
diagnostics.append(
|
|
Diagnostic(
|
|
"warn",
|
|
"incomplete_registry_entry",
|
|
"Registry entry must include store_id, path, and store_kind.",
|
|
store_id or store_path or "unknown",
|
|
).to_dict()
|
|
)
|
|
continue
|
|
entries.append(item)
|
|
return {
|
|
"schema_version": STORE_REGISTRY_SCHEMA,
|
|
"valid": not any(item.get("severity") == "error" for item in diagnostics),
|
|
"registry_path": str(registry_path),
|
|
"entries": entries,
|
|
"diagnostics": diagnostics,
|
|
}
|
|
|
|
|
|
def _store_id_for_path(path: Path, environ: Mapping[str, str] | None = None) -> str:
|
|
resolved = path.expanduser().resolve()
|
|
default_ops = default_memory_store_path(environ).expanduser().resolve()
|
|
if resolved == default_ops:
|
|
return DEFAULT_OPS_WARDEN_STORE_ID
|
|
return f"store:{stable_digest(str(resolved))}"
|
|
|
|
|
|
def _descriptor_from_path(
|
|
path: Path,
|
|
*,
|
|
store_kind: str,
|
|
source: str,
|
|
profile_id: str = "",
|
|
label: str = "",
|
|
store_id: str = "",
|
|
environ: Mapping[str, str] | None = None,
|
|
) -> MemoryStoreDescriptor:
|
|
resolved = path.expanduser().resolve()
|
|
return MemoryStoreDescriptor(
|
|
store_id=store_id or _store_id_for_path(resolved, environ),
|
|
path=resolved,
|
|
store_kind=store_kind,
|
|
profile_id=profile_id,
|
|
label=label,
|
|
source=source,
|
|
)
|
|
|
|
|
|
def discover_memory_stores(
|
|
environ: Mapping[str, str] | None = None,
|
|
*,
|
|
registry_path: str | Path | None = None,
|
|
extra_paths: tuple[str | Path, ...] = (),
|
|
) -> tuple[list[MemoryStoreDescriptor], list[dict[str, Any]]]:
|
|
environ = environ or os.environ
|
|
diagnostics: list[dict[str, Any]] = []
|
|
by_path: dict[str, MemoryStoreDescriptor] = {}
|
|
|
|
def add_descriptor(descriptor: MemoryStoreDescriptor) -> None:
|
|
key = str(descriptor.path)
|
|
existing = by_path.get(key)
|
|
if existing is None:
|
|
by_path[key] = descriptor
|
|
return
|
|
if descriptor.source == "registry" and existing.source != "registry":
|
|
by_path[key] = descriptor
|
|
|
|
default_ops = default_memory_store_path(environ)
|
|
kind = classify_store(default_ops)
|
|
if kind:
|
|
profile_id = OPS_WARDEN_PROFILE_ID if kind == STORE_KIND_OPS_WARDEN else ""
|
|
add_descriptor(
|
|
_descriptor_from_path(
|
|
default_ops,
|
|
store_kind=kind,
|
|
source="default",
|
|
profile_id=profile_id,
|
|
label="ops-warden coordination store",
|
|
store_id=DEFAULT_OPS_WARDEN_STORE_ID,
|
|
environ=environ,
|
|
)
|
|
)
|
|
|
|
registry = load_store_registry(registry_path, environ=environ)
|
|
diagnostics.extend(registry.get("diagnostics", ()))
|
|
registry_file = Path(str(registry.get("registry_path") or "")).expanduser()
|
|
for entry in registry.get("entries", ()):
|
|
entry_path = Path(str(entry.get("path") or "")).expanduser()
|
|
if not entry_path.is_absolute() and registry_file.is_file():
|
|
entry_path = (registry_file.parent / entry_path).resolve()
|
|
if not entry_path.is_dir():
|
|
diagnostics.append(
|
|
Diagnostic(
|
|
"warn",
|
|
"registry_store_missing",
|
|
"Registry entry path does not exist.",
|
|
str(entry.get("store_id") or entry_path),
|
|
{"path": str(entry_path)},
|
|
).to_dict()
|
|
)
|
|
continue
|
|
declared_kind = str(entry.get("store_kind") or "")
|
|
actual_kind = classify_store(entry_path)
|
|
if actual_kind and actual_kind != declared_kind:
|
|
diagnostics.append(
|
|
Diagnostic(
|
|
"warn",
|
|
"registry_store_kind_mismatch",
|
|
"Registry store_kind does not match on-disk classification.",
|
|
str(entry.get("store_id") or entry_path),
|
|
{"declared": declared_kind, "actual": actual_kind},
|
|
).to_dict()
|
|
)
|
|
store_kind = actual_kind or declared_kind
|
|
if not store_kind:
|
|
continue
|
|
add_descriptor(
|
|
_descriptor_from_path(
|
|
entry_path,
|
|
store_kind=store_kind,
|
|
source="registry",
|
|
profile_id=str(entry.get("profile_id") or ""),
|
|
label=str(entry.get("label") or ""),
|
|
store_id=str(entry.get("store_id") or ""),
|
|
environ=environ,
|
|
)
|
|
)
|
|
|
|
env_paths = str(environ.get("PHASE_MEMORY_STORE_PATHS") or "").strip()
|
|
path_candidates = [item.strip() for item in env_paths.split(":") if item.strip()]
|
|
path_candidates.extend(str(item) for item in extra_paths)
|
|
for candidate in path_candidates:
|
|
candidate_path = Path(candidate).expanduser()
|
|
if not candidate_path.is_dir():
|
|
continue
|
|
store_kind = classify_store(candidate_path)
|
|
if not store_kind:
|
|
continue
|
|
add_descriptor(
|
|
_descriptor_from_path(
|
|
candidate_path,
|
|
store_kind=store_kind,
|
|
source="env",
|
|
environ=environ,
|
|
)
|
|
)
|
|
|
|
stores = sorted(by_path.values(), key=lambda item: (item.store_kind, item.store_id))
|
|
return stores, diagnostics
|
|
|
|
|
|
def resolve_store_reference(
|
|
reference: str,
|
|
stores: list[MemoryStoreDescriptor],
|
|
environ: Mapping[str, str] | None = None,
|
|
) -> MemoryStoreDescriptor | None:
|
|
ref = str(reference or "").strip()
|
|
if not ref:
|
|
return None
|
|
for store in stores:
|
|
if ref == store.store_id or ref == str(store.path):
|
|
return store
|
|
candidate = Path(ref).expanduser()
|
|
if candidate.is_dir():
|
|
store_kind = classify_store(candidate)
|
|
if store_kind:
|
|
return _descriptor_from_path(candidate, store_kind=store_kind, source="explicit", environ=environ)
|
|
for store in stores:
|
|
if candidate.resolve() == store.path:
|
|
return store
|
|
return None
|
|
|
|
|
|
def compute_report_window(
|
|
*,
|
|
days: int,
|
|
window_end: datetime | None = None,
|
|
) -> tuple[datetime, datetime]:
|
|
if days < 1:
|
|
raise ValueError("days must be at least 1")
|
|
end = window_end or datetime.now(timezone.utc)
|
|
if end.tzinfo is None:
|
|
end = end.replace(tzinfo=timezone.utc)
|
|
else:
|
|
end = end.astimezone(timezone.utc)
|
|
start = end - timedelta(days=days)
|
|
return start, end
|
|
|
|
|
|
def _event_in_window(timestamp: str | None, *, window_start: datetime, window_end: datetime) -> bool | None:
|
|
parsed = parse_iso_datetime(timestamp)
|
|
if parsed is None:
|
|
return None
|
|
return window_start <= parsed <= window_end
|
|
|
|
|
|
def _read_jsonl(path: Path) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
if not path.is_file():
|
|
return [], []
|
|
events: list[dict[str, Any]] = []
|
|
diagnostics: list[dict[str, Any]] = []
|
|
for line_number, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
|
if not raw.strip():
|
|
continue
|
|
try:
|
|
data = json.loads(raw)
|
|
except json.JSONDecodeError as exc:
|
|
diagnostics.append(
|
|
Diagnostic(
|
|
"warn",
|
|
"malformed_jsonl_line",
|
|
"JSONL line is not valid JSON.",
|
|
f"{path}:line:{line_number}",
|
|
{"error": str(exc)},
|
|
).to_dict()
|
|
)
|
|
continue
|
|
if isinstance(data, dict):
|
|
events.append(data)
|
|
return events, diagnostics
|
|
|
|
|
|
def _increment(counter: dict[str, int], key: str) -> None:
|
|
normalized = key or "unknown"
|
|
counter[normalized] = counter.get(normalized, 0) + 1
|
|
|
|
|
|
def _top_counter(counter: dict[str, int], *, limit: int) -> dict[str, int]:
|
|
return dict(sorted(counter.items(), key=lambda item: (-item[1], item[0]))[:limit])
|
|
|
|
|
|
def aggregate_ops_warden_activity(
|
|
store: MemoryStoreDescriptor,
|
|
*,
|
|
window_start: datetime,
|
|
window_end: datetime,
|
|
) -> dict[str, Any]:
|
|
events_path = store.path / "events.jsonl"
|
|
raw_events, line_diagnostics = _read_jsonl(events_path)
|
|
diagnostics = list(line_diagnostics)
|
|
by_session_kind: dict[str, int] = {}
|
|
by_command: dict[str, int] = {}
|
|
by_outcome: dict[str, int] = {}
|
|
by_route_id: dict[str, int] = {}
|
|
episode_timeline: list[dict[str, Any]] = []
|
|
first_activity_at = ""
|
|
last_activity_at = ""
|
|
in_window = 0
|
|
missing_timestamp = 0
|
|
|
|
for event in raw_events:
|
|
timestamp = str(event.get("recorded_at") or "")
|
|
in_range = _event_in_window(timestamp, window_start=window_start, window_end=window_end)
|
|
if in_range is None:
|
|
missing_timestamp += 1
|
|
diagnostics.append(
|
|
Diagnostic(
|
|
"warn",
|
|
"episode_missing_timestamp",
|
|
"Episode lacks a parseable recorded_at timestamp.",
|
|
str(event.get("event_id") or "unknown"),
|
|
).to_dict()
|
|
)
|
|
continue
|
|
if not in_range:
|
|
continue
|
|
in_window += 1
|
|
_increment(by_session_kind, str(event.get("session_kind") or ""))
|
|
_increment(by_command, str(event.get("command") or ""))
|
|
_increment(by_outcome, str(event.get("outcome") or ""))
|
|
_increment(by_route_id, str(event.get("route_id") or ""))
|
|
if not first_activity_at or timestamp < first_activity_at:
|
|
first_activity_at = timestamp
|
|
if not last_activity_at or timestamp > last_activity_at:
|
|
last_activity_at = timestamp
|
|
episode_timeline.append(
|
|
{
|
|
"event_id": str(event.get("event_id") or ""),
|
|
"recorded_at": timestamp,
|
|
"session_kind": str(event.get("session_kind") or ""),
|
|
"command": str(event.get("command") or ""),
|
|
"route_id": str(event.get("route_id") or ""),
|
|
"outcome": str(event.get("outcome") or ""),
|
|
}
|
|
)
|
|
|
|
episode_timeline.sort(key=lambda item: item.get("recorded_at", ""), reverse=True)
|
|
metadata_path = store.path / "metadata.json"
|
|
schema_version = ""
|
|
if metadata_path.is_file():
|
|
try:
|
|
schema_version = str(json.loads(metadata_path.read_text(encoding="utf-8")).get("schema_version") or "")
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
return {
|
|
"store_id": store.store_id,
|
|
"store_kind": store.store_kind,
|
|
"path": str(store.path),
|
|
"profile_id": store.profile_id or OPS_WARDEN_PROFILE_ID,
|
|
"schema_version": schema_version,
|
|
"episode_count": in_window,
|
|
"audit_event_count": 0,
|
|
"missing_timestamp_count": missing_timestamp,
|
|
"first_activity_at": first_activity_at,
|
|
"last_activity_at": last_activity_at,
|
|
"by_session_kind": dict(sorted(by_session_kind.items())),
|
|
"by_command": dict(sorted(by_command.items())),
|
|
"by_outcome": dict(sorted(by_outcome.items())),
|
|
"by_route_id": _top_counter(by_route_id, limit=TOP_ROUTE_IDS),
|
|
"by_operation": {},
|
|
"episode_timeline": episode_timeline[:DETAIL_EPISODE_LIMIT],
|
|
"repair_diagnostics": [],
|
|
"diagnostics": diagnostics,
|
|
}
|
|
|
|
|
|
def aggregate_local_graph_activity(
|
|
store: MemoryStoreDescriptor,
|
|
*,
|
|
window_start: datetime,
|
|
window_end: datetime,
|
|
) -> dict[str, Any]:
|
|
events_path = store.path / "events.jsonl"
|
|
audit_path = store.path / "audit.jsonl"
|
|
raw_events, event_line_diagnostics = _read_jsonl(events_path)
|
|
raw_audit, audit_line_diagnostics = _read_jsonl(audit_path)
|
|
diagnostics = list(event_line_diagnostics)
|
|
diagnostics.extend(audit_line_diagnostics)
|
|
|
|
by_session_kind: dict[str, int] = {}
|
|
by_command: dict[str, int] = {}
|
|
by_outcome: dict[str, int] = {}
|
|
by_operation: dict[str, int] = {}
|
|
episode_timeline: list[dict[str, Any]] = []
|
|
first_activity_at = ""
|
|
last_activity_at = ""
|
|
episode_count = 0
|
|
audit_count = 0
|
|
missing_timestamp = 0
|
|
|
|
for event in raw_events:
|
|
timestamp = str(event.get("timestamp") or event.get("recorded_at") or "")
|
|
in_range = _event_in_window(timestamp, window_start=window_start, window_end=window_end)
|
|
if in_range is None:
|
|
missing_timestamp += 1
|
|
diagnostics.append(
|
|
Diagnostic(
|
|
"warn",
|
|
"episode_missing_timestamp",
|
|
"Event lacks a parseable timestamp.",
|
|
str(event.get("event_id") or event.get("id") or "unknown"),
|
|
).to_dict()
|
|
)
|
|
continue
|
|
if not in_range:
|
|
continue
|
|
episode_count += 1
|
|
kind = str(event.get("kind") or event.get("session_kind") or "")
|
|
_increment(by_session_kind, kind)
|
|
_increment(by_command, str(event.get("command") or kind))
|
|
_increment(by_outcome, str(event.get("outcome") or event.get("state") or ""))
|
|
if not first_activity_at or timestamp < first_activity_at:
|
|
first_activity_at = timestamp
|
|
if not last_activity_at or timestamp > last_activity_at:
|
|
last_activity_at = timestamp
|
|
episode_timeline.append(
|
|
{
|
|
"event_id": str(event.get("event_id") or event.get("id") or ""),
|
|
"recorded_at": timestamp,
|
|
"session_kind": kind,
|
|
"command": str(event.get("command") or kind),
|
|
"route_id": "",
|
|
"outcome": str(event.get("outcome") or event.get("state") or ""),
|
|
}
|
|
)
|
|
|
|
for event in raw_audit:
|
|
timestamp = str(event.get("timestamp") or event.get("recorded_at") or "")
|
|
in_range = _event_in_window(timestamp, window_start=window_start, window_end=window_end)
|
|
if in_range is None:
|
|
missing_timestamp += 1
|
|
diagnostics.append(
|
|
Diagnostic(
|
|
"warn",
|
|
"audit_missing_timestamp",
|
|
"Audit event lacks a parseable timestamp.",
|
|
str(event.get("operation_id") or event.get("operation") or "unknown"),
|
|
).to_dict()
|
|
)
|
|
continue
|
|
if not in_range:
|
|
continue
|
|
audit_count += 1
|
|
_increment(by_operation, str(event.get("operation") or ""))
|
|
if not first_activity_at or timestamp < first_activity_at:
|
|
first_activity_at = timestamp
|
|
if not last_activity_at or timestamp > last_activity_at:
|
|
last_activity_at = timestamp
|
|
|
|
episode_timeline.sort(key=lambda item: item.get("recorded_at", ""), reverse=True)
|
|
graph_store = FileBackedMemoryGraphStore(store.path)
|
|
repair = graph_store.repair_diagnostics()
|
|
repair_diagnostics = [
|
|
item.to_dict()
|
|
for item in repair
|
|
if item.severity in {"error", "warn"}
|
|
]
|
|
schema_version = ""
|
|
try:
|
|
schema_version = str(graph_store.metadata().get("schema_version") or "")
|
|
except (json.JSONDecodeError, OSError):
|
|
pass
|
|
|
|
return {
|
|
"store_id": store.store_id,
|
|
"store_kind": store.store_kind,
|
|
"path": str(store.path),
|
|
"profile_id": store.profile_id,
|
|
"schema_version": schema_version,
|
|
"episode_count": episode_count,
|
|
"audit_event_count": audit_count,
|
|
"missing_timestamp_count": missing_timestamp,
|
|
"first_activity_at": first_activity_at,
|
|
"last_activity_at": last_activity_at,
|
|
"by_session_kind": dict(sorted(by_session_kind.items())),
|
|
"by_command": dict(sorted(by_command.items())),
|
|
"by_outcome": dict(sorted(by_outcome.items())),
|
|
"by_route_id": {},
|
|
"by_operation": dict(sorted(by_operation.items())),
|
|
"episode_timeline": episode_timeline[:DETAIL_EPISODE_LIMIT],
|
|
"repair_diagnostics": repair_diagnostics,
|
|
"diagnostics": diagnostics,
|
|
}
|
|
|
|
|
|
def aggregate_store_activity(
|
|
store: MemoryStoreDescriptor,
|
|
*,
|
|
window_start: datetime,
|
|
window_end: datetime,
|
|
) -> dict[str, Any]:
|
|
if store.store_kind == STORE_KIND_OPS_WARDEN:
|
|
return aggregate_ops_warden_activity(store, window_start=window_start, window_end=window_end)
|
|
return aggregate_local_graph_activity(store, window_start=window_start, window_end=window_end)
|
|
|
|
|
|
def build_store_list(
|
|
environ: Mapping[str, str] | None = None,
|
|
*,
|
|
registry_path: str | Path | None = None,
|
|
extra_paths: tuple[str | Path, ...] = (),
|
|
) -> dict[str, Any]:
|
|
stores, diagnostics = discover_memory_stores(environ, registry_path=registry_path, extra_paths=extra_paths)
|
|
return {
|
|
"schema_version": STORE_LIST_SCHEMA,
|
|
"valid": not any(item.get("severity") == "error" for item in diagnostics),
|
|
"store_count": len(stores),
|
|
"stores": [
|
|
{
|
|
"store_id": store.store_id,
|
|
"path": str(store.path),
|
|
"store_kind": store.store_kind,
|
|
"profile_id": store.profile_id,
|
|
"label": store.label,
|
|
"source": store.source,
|
|
}
|
|
for store in stores
|
|
],
|
|
"diagnostics": diagnostics,
|
|
}
|
|
|
|
|
|
def build_federated_report(
|
|
*,
|
|
days: int = 7,
|
|
focus_store_id: str | None = None,
|
|
focus_store_path: str | Path | None = None,
|
|
environ: Mapping[str, str] | None = None,
|
|
registry_path: str | Path | None = None,
|
|
extra_paths: tuple[str | Path, ...] = (),
|
|
window_end: datetime | None = None,
|
|
) -> dict[str, Any]:
|
|
environ = environ or os.environ
|
|
stores, discovery_diagnostics = discover_memory_stores(
|
|
environ,
|
|
registry_path=registry_path,
|
|
extra_paths=extra_paths,
|
|
)
|
|
diagnostics = list(discovery_diagnostics)
|
|
|
|
focus_reference = str(focus_store_id or focus_store_path or "").strip()
|
|
focus_store: MemoryStoreDescriptor | None = None
|
|
if focus_reference:
|
|
focus_store = resolve_store_reference(focus_reference, stores, environ=environ)
|
|
if focus_store is None:
|
|
diagnostics.append(
|
|
Diagnostic(
|
|
"error",
|
|
"unknown_store_reference",
|
|
"Requested store was not discovered and could not be classified.",
|
|
focus_reference,
|
|
).to_dict()
|
|
)
|
|
return {
|
|
"schema_version": FEDERATED_REPORT_SCHEMA,
|
|
"valid": False,
|
|
"window_days": days,
|
|
"window_start": "",
|
|
"window_end": "",
|
|
"focus_store_id": focus_reference,
|
|
"store_count": 0,
|
|
"aggregate": {},
|
|
"stores": [],
|
|
"store_detail": None,
|
|
"diagnostics": diagnostics,
|
|
}
|
|
stores = [focus_store]
|
|
|
|
window_start, window_end_dt = compute_report_window(days=days, window_end=window_end)
|
|
summaries: list[dict[str, Any]] = []
|
|
aggregate = {
|
|
"episode_count": 0,
|
|
"audit_event_count": 0,
|
|
"active_store_count": 0,
|
|
"by_outcome": {},
|
|
"by_session_kind": {},
|
|
"by_operation": {},
|
|
}
|
|
by_outcome: dict[str, int] = {}
|
|
by_session_kind: dict[str, int] = {}
|
|
by_operation: dict[str, int] = {}
|
|
|
|
store_detail: dict[str, Any] | None = None
|
|
for store in stores:
|
|
summary = aggregate_store_activity(store, window_start=window_start, window_end=window_end_dt)
|
|
diagnostics.extend(summary.get("diagnostics", ()))
|
|
public_summary = {
|
|
key: value
|
|
for key, value in summary.items()
|
|
if key not in {"episode_timeline", "repair_diagnostics", "diagnostics"}
|
|
}
|
|
summaries.append(public_summary)
|
|
aggregate["episode_count"] += int(summary.get("episode_count") or 0)
|
|
aggregate["audit_event_count"] += int(summary.get("audit_event_count") or 0)
|
|
if int(summary.get("episode_count") or 0) or int(summary.get("audit_event_count") or 0):
|
|
aggregate["active_store_count"] += 1
|
|
for key, value in (summary.get("by_outcome") or {}).items():
|
|
by_outcome[key] = by_outcome.get(key, 0) + int(value)
|
|
for key, value in (summary.get("by_session_kind") or {}).items():
|
|
by_session_kind[key] = by_session_kind.get(key, 0) + int(value)
|
|
for key, value in (summary.get("by_operation") or {}).items():
|
|
by_operation[key] = by_operation.get(key, 0) + int(value)
|
|
if focus_store is not None and store.store_id == focus_store.store_id:
|
|
store_detail = summary
|
|
|
|
aggregate["by_outcome"] = dict(sorted(by_outcome.items()))
|
|
aggregate["by_session_kind"] = dict(sorted(by_session_kind.items()))
|
|
aggregate["by_operation"] = dict(sorted(by_operation.items()))
|
|
|
|
return {
|
|
"schema_version": FEDERATED_REPORT_SCHEMA,
|
|
"valid": not any(item.get("severity") == "error" for item in diagnostics),
|
|
"window_days": days,
|
|
"window_start": window_start.replace(microsecond=0).isoformat(),
|
|
"window_end": window_end_dt.replace(microsecond=0).isoformat(),
|
|
"focus_store_id": focus_store.store_id if focus_store else "",
|
|
"store_count": len(summaries),
|
|
"aggregate": aggregate,
|
|
"stores": summaries,
|
|
"store_detail": store_detail,
|
|
"diagnostics": diagnostics,
|
|
"generated_at": utc_now_iso(),
|
|
} |