generated from coulomb/repo-seed
Implement live-shaped readiness workplan
This commit is contained in:
@@ -96,5 +96,6 @@ for retrieval and evaluation behavior, [docs/service-readiness.md](docs/service-
|
||||
for service and adapter contracts, [docs/lifecycle-rules.md](docs/lifecycle-rules.md)
|
||||
for profile-driven lifecycle rules, [docs/external-adapter-packs.md](docs/external-adapter-packs.md)
|
||||
for fake external integration packs, [docs/operational-readiness.md](docs/operational-readiness.md)
|
||||
for the local end-to-end operational recipe, [docs/maturity-scorecard.md](docs/maturity-scorecard.md)
|
||||
for the local end-to-end operational recipe, [docs/api-compatibility.md](docs/api-compatibility.md)
|
||||
for public API compatibility expectations, [docs/maturity-scorecard.md](docs/maturity-scorecard.md)
|
||||
for the current maturity assessment, and [SCOPE.md](SCOPE.md) for repository boundaries.
|
||||
|
||||
46
docs/api-compatibility.md
Normal file
46
docs/api-compatibility.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# API Compatibility
|
||||
|
||||
Updated: 2026-05-19
|
||||
|
||||
`phase-memory` now treats its embedding surface as a compatibility contract for
|
||||
local integrations.
|
||||
|
||||
## Stable Surface
|
||||
|
||||
The public surface is:
|
||||
|
||||
- exports listed in `phase_memory.__all__`;
|
||||
- `PhaseMemoryRuntime` JSON-serializable runtime envelopes;
|
||||
- `LocalServiceRunner.handle(operation, payload)`;
|
||||
- every operation declared by `service_contracts()["operations"]`;
|
||||
- `ServiceBinding` and `service_binding_from_config` as optional
|
||||
framework-neutral binding helpers;
|
||||
- `RuntimeConfig`, `resolve_runtime_adapters`, and `runtime_from_config`;
|
||||
- adapter pack manifests and `validate_adapter_pack_manifest`;
|
||||
- evaluation threshold reports from `evaluation_threshold_report`.
|
||||
|
||||
The snapshot in `tests/fixtures/public-api-snapshot.json` intentionally fails
|
||||
when exports or service operations change. Update the snapshot only when the
|
||||
change is deliberate and documented.
|
||||
|
||||
## Adding Operations
|
||||
|
||||
When adding a service operation:
|
||||
|
||||
1. Add the operation to `SERVICE_OPERATIONS`.
|
||||
2. Add a `LocalServiceRunner` dispatch path.
|
||||
3. Add binding and runner tests that exercise the operation without starting a
|
||||
network listener.
|
||||
4. Update the API snapshot.
|
||||
5. Note the change in docs or release notes.
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
Breaking changes should include:
|
||||
|
||||
- a migration note in this document or release notes;
|
||||
- a compatibility test update explaining the intentional break;
|
||||
- StateHub workplan evidence for why the break is needed.
|
||||
|
||||
Avoid changing runtime envelope keys, service operation names, adapter manifest
|
||||
schema fields, or fixture schema names without a migration path.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Phase Memory Maturity Scorecard
|
||||
|
||||
Updated: 2026-05-18
|
||||
Updated: 2026-05-19
|
||||
|
||||
## Purpose
|
||||
|
||||
@@ -26,51 +26,51 @@ to 5.
|
||||
|
||||
## Current Score
|
||||
|
||||
Overall maturity: **4.0 / 5**
|
||||
Overall maturity: **4.2 / 5**
|
||||
|
||||
Two sub-scores make the result easier to reason about:
|
||||
|
||||
- Local integration maturity: **4.3 / 5**
|
||||
- Operational maturity: **3.5 / 5**
|
||||
- Local integration maturity: **4.5 / 5**
|
||||
- Operational maturity: **3.8 / 5**
|
||||
|
||||
The repo is strong as a deterministic local library and service-boundary core.
|
||||
It is not yet production-operational because the external adapters are fakes,
|
||||
service bindings are framework-neutral shapes rather than deployable endpoints,
|
||||
and migration behavior is diagnostic rather than an operator-applied migration
|
||||
system.
|
||||
It is not yet production-operational because adapter coverage is still
|
||||
live-shaped rather than credentialed live integration, and service bindings are
|
||||
framework-neutral embedding surfaces rather than a deployed service.
|
||||
|
||||
## Dimension Scorecard
|
||||
|
||||
| Dimension | Score | Target | Evidence | Needed Next |
|
||||
| --- | ---: | ---: | --- | --- |
|
||||
| Intent and boundaries | 4.4 | 5.0 | `INTENT.md`, `SCOPE.md`, `README.md`, architecture docs, adjacent-repo boundary docs | Keep docs current as live adapters and service bindings clarify real ownership. |
|
||||
| Package and API foundation | 4.3 | 4.5 | Python package, public exports, runtime facade, CLI, service runner export, service config, dependency-light tests | Add public export compatibility checks and release notes discipline. |
|
||||
| Package and API foundation | 4.5 | 4.8 | Python package, public exports, runtime facade, CLI, service runner export, service config, dependency-light tests, public API snapshot | Add release notes discipline and compatibility migration examples. |
|
||||
| Markitect profile contract ingress | 3.7 | 4.5 | Profile loading, diagnostics, runtime envelopes, profile-derived config, local alias normalization | Add richer compatibility fixtures and schema drift diagnostics. |
|
||||
| Graph and event ingress | 3.9 | 4.5 | Graph loading, endpoint diagnostics, event model, JSONL log, export, repair checks, corrupt-record diagnostics, fake graph/event adapters | Add broader malformed/large graph fixtures and operator repair utilities. |
|
||||
| Graph and event ingress | 4.0 | 4.5 | Graph loading, endpoint diagnostics, event model, JSONL log, export, repair checks, corrupt-record diagnostics, fake and live-shaped graph/event adapters | Add broader malformed/large graph fixtures and operator repair utilities. |
|
||||
| Phase domain model | 3.5 | 4.5 | Phases, lifecycle states, actions, paths, retention rules, profile-derived transition rules | Add migration semantics for profile/rule changes over durable stores. |
|
||||
| Profile execution planning | 4.2 | 4.5 | Adapter plan, capabilities, policy gates, fallback behavior, config-driven local/external resolution, adapter pack manifests | Add compatibility gates for live adapter packs. |
|
||||
| Lifecycle planning and apply | 4.0 | 4.5 | Dry-run lifecycle plans, profile rules, review-gated local apply, service `lifecycle.apply`, apply audit queries | Add operator migration semantics and richer apply rollback/repair drills. |
|
||||
| Profile execution planning | 4.3 | 4.5 | Adapter plan, capabilities, policy gates, fallback behavior, config-driven local/external resolution, adapter pack manifests, live-shaped compatibility gates | Add compatibility gates for credentialed live adapter packs. |
|
||||
| Lifecycle planning and apply | 4.1 | 4.5 | Dry-run lifecycle plans, profile rules, review-gated local apply, service `lifecycle.apply`, apply audit/export queries | Add richer apply rollback and repair drills. |
|
||||
| Activation planning | 4.0 | 4.8 | Budgeted activation, selections, package request, graph neighborhoods, paths, ranking, metrics, multi-scenario evaluation fixtures | Wire semantic-index-assisted retrieval into runtime planning. |
|
||||
| Local persistence | 3.7 | 4.5 | File-backed graph store, JSONL event log, audit sink, atomic JSON writes, metadata migration diagnostics, export, repair diagnostics | Add executable migrations, compaction/retention utilities, and stronger corruption recovery. |
|
||||
| Policy, review, and audit | 3.9 | 5.0 | Operation points, review records, audit schema, queryable audit sinks, denials, redaction, fake external policy/audit adapters | Add live policy adapter boundary and enforceable audit retention policy. |
|
||||
| Observability and operations | 3.6 | 4.8 | Health report, config diagnostics, adapter status, fake telemetry audit sink, operational recipe | Add metrics/event export and deployable health/readiness binding. |
|
||||
| Markitect interop | 3.7 | 4.5 | Local validation, package request/response envelopes, fake compiler | Add optional live Markitect compiler adapter and contract compatibility suite. |
|
||||
| Kontextual/Infospace interop | 3.3 | 4.5 | Delegation envelope, fake runtime registry, activation quality report fixture, adapter compatibility manifests | Add live/fake delegation scenarios and broader Infospace restart reports. |
|
||||
| Testing and evaluation | 4.1 | 4.5 | 70 deterministic tests over runtime, CLI, adapters, policy, activation, lifecycle, service, fakes, and evaluation scenarios | Add larger regression corpus and threshold trend reports. |
|
||||
| Service readiness | 4.2 | 4.8 | Service contracts, full local runner parity, health, config, adapter conformance, fake pack | Add optional framework binding and deployable readiness endpoints. |
|
||||
| Developer experience | 4.1 | 4.5 | README, package map, CLI examples, persistence/policy/interop/service/lifecycle/fake-pack docs, operational recipe | Add troubleshooting matrix and embedded-service examples. |
|
||||
| Local persistence | 4.0 | 4.5 | File-backed graph store, JSONL event log, audit sink, atomic JSON writes, executable metadata migrations, migration audit, export, repair diagnostics | Add compaction/retention utilities and stronger corruption recovery. |
|
||||
| Policy, review, and audit | 4.2 | 5.0 | Operation points, review records, audit schema, queryable/exportable audit sinks, retention plans, denials, redaction, fake/live-shaped policy/audit adapters | Add live policy adapter boundary and enforceable audit retention pruning. |
|
||||
| Observability and operations | 4.0 | 4.8 | Health report, readiness report, config diagnostics, adapter status, service binding, fake/live-shaped telemetry audit sinks, operational recipe | Add metrics/event export to external telemetry and deployable service packaging. |
|
||||
| Markitect interop | 4.0 | 4.5 | Local validation, package request/response envelopes, fake and live-shaped compiler fixtures | Add optional credentialed Markitect compiler adapter and schema drift suite. |
|
||||
| Kontextual/Infospace interop | 3.7 | 4.5 | Delegation envelope, fake and live-shaped runtime registry, activation quality report fixture, adapter compatibility manifests | Add credentialed Kontextual adapter drill and broader Infospace restart reports. |
|
||||
| Testing and evaluation | 4.3 | 4.7 | Deterministic tests over runtime, CLI, adapters, policy, activation, lifecycle, service, fakes, live-shaped packs, API snapshots, and evaluation threshold reports | Add larger regression corpus and threshold trend reports. |
|
||||
| Service readiness | 4.5 | 4.8 | Service contracts, full local runner parity, framework-neutral service binding, WSGI adapter, health/readiness, config, adapter conformance | Add deployable packaging and operator readiness runbooks. |
|
||||
| Developer experience | 4.3 | 4.7 | README, package map, CLI examples, persistence/policy/interop/service/lifecycle/fake-pack docs, operational recipe, API compatibility docs | Add troubleshooting matrix and release note templates. |
|
||||
|
||||
## Assessment
|
||||
|
||||
The project has crossed the local integration-readiness threshold. The runtime
|
||||
envelopes, policy/review model, profile-derived configuration, lifecycle rules,
|
||||
local persistence diagnostics, queryable audit path, fake external pack
|
||||
manifests, and conformance helpers form a solid integration boundary.
|
||||
local persistence migrations, queryable/exportable audit path, fake and
|
||||
live-shaped external pack manifests, service binding, API snapshots, and
|
||||
conformance helpers form a solid integration boundary.
|
||||
|
||||
The biggest optimization opportunity is now the next operational layer:
|
||||
turning diagnostic-only durability into operator actions, adding optional
|
||||
deployable service bindings, and testing live or live-shaped adapters behind
|
||||
the same conformance suite as the fake pack.
|
||||
moving from live-shaped local fixtures to credentialed live adapter drills,
|
||||
packaging the service binding for deployment, and growing evaluation thresholds
|
||||
into trend reports.
|
||||
|
||||
## Completed Refinement Workplan
|
||||
|
||||
@@ -86,19 +86,31 @@ the same conformance suite as the fake pack.
|
||||
- adapter pack manifests and explicit missing-capability diagnostics;
|
||||
- an operational end-to-end recipe.
|
||||
|
||||
`PMEM-WP-0012` moved the score from 4.0 to 4.2 by adding:
|
||||
|
||||
- framework-neutral `ServiceBinding` and WSGI adapter tests without starting a
|
||||
listener;
|
||||
- executable local-store migration planning/apply behavior with audit traces;
|
||||
- live-shaped Markitect/Kontextual/telemetry adapter fixtures behind the same
|
||||
manifest and conformance contract;
|
||||
- audit retention plans and export batches;
|
||||
- evaluation threshold reports over the scenario corpus;
|
||||
- public API and service operation compatibility snapshots.
|
||||
|
||||
## Recommended Next Refinement
|
||||
|
||||
Create and execute `PMEM-WP-0012`: live-adapter and service-binding readiness.
|
||||
Create and execute `PMEM-WP-0013`: credentialed adapter drills and deployment
|
||||
packaging.
|
||||
|
||||
Highest-value tasks:
|
||||
|
||||
- Add an optional framework binding around `LocalServiceRunner` with health and
|
||||
readiness endpoints.
|
||||
- Add executable local-store migrations, not only diagnostics.
|
||||
- Add live-shaped Markitect/Kontextual adapter fixtures behind the manifest and
|
||||
conformance suite.
|
||||
- Add audit retention enforcement and telemetry export drills.
|
||||
- Grow the evaluation corpus into threshold reports that can catch regressions.
|
||||
- Add optional credentialed Markitect/Kontextual adapter smoke drills that are
|
||||
skipped unless credentials are present.
|
||||
- Package the service binding as a deployable local service with operator
|
||||
readiness checks.
|
||||
- Add audit retention pruning and telemetry export enforcement.
|
||||
- Grow evaluation reporting into historical threshold trends.
|
||||
- Add release note and migration-note templates for compatibility changes.
|
||||
|
||||
## Score Movement Gates
|
||||
|
||||
@@ -111,10 +123,10 @@ Achieved overall score **4.0** when:
|
||||
|
||||
Move overall score to **4.3+** when:
|
||||
|
||||
- Live optional Markitect or Kontextual adapter can be used behind the same
|
||||
conformance suite as the fake pack.
|
||||
- Operational docs include a deployable service binding or a clear embedding
|
||||
recipe.
|
||||
- Credentialed optional Markitect or Kontextual adapter smoke drills run behind
|
||||
the same conformance suite as the fake/live-shaped packs.
|
||||
- Operational docs include deployable service packaging and an operator
|
||||
readiness runbook.
|
||||
|
||||
Move overall score to **4.7+** only when:
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Operational Readiness Recipe
|
||||
|
||||
Updated: 2026-05-18
|
||||
Updated: 2026-05-19
|
||||
|
||||
This recipe exercises the local operational surface without requiring live
|
||||
Markitect, Kontextual, or telemetry services. It is the expected smoke path for
|
||||
@@ -60,6 +60,30 @@ Expected checks:
|
||||
audit sink retention mode.
|
||||
- `health["ok"]` is true.
|
||||
|
||||
## Service Binding Drill
|
||||
|
||||
`ServiceBinding` wraps `LocalServiceRunner` in an HTTP-shaped API without
|
||||
starting a listener:
|
||||
|
||||
```python
|
||||
from phase_memory import ServiceBinding
|
||||
|
||||
binding = ServiceBinding(runner)
|
||||
|
||||
health_response = binding.route("GET", "/health")
|
||||
ready_response = binding.route("GET", "/ready")
|
||||
contracts_response = binding.route("GET", "/contracts")
|
||||
package_response = binding.route(
|
||||
"POST",
|
||||
"/operations/package.compile",
|
||||
{"selection": activation["data"]["activation_plan"]["selection"]},
|
||||
)
|
||||
```
|
||||
|
||||
The WSGI adapter returned by `binding.as_wsgi_app()` is also callable in tests
|
||||
without opening a socket. Use this for deployment wrappers so the core service
|
||||
operation contract stays framework-neutral.
|
||||
|
||||
## Review-Gated Apply
|
||||
|
||||
Lifecycle actions that require review are denied until an approval marker or
|
||||
@@ -90,6 +114,12 @@ from phase_memory import RuntimeConfig, LocalServiceRunner
|
||||
config = RuntimeConfig.from_profile(profile, local_store_path=".phase-memory-local")
|
||||
runner = LocalServiceRunner(config=config)
|
||||
repair = runner.runtime.repair_diagnostics(source_ref=config.local_store_path)
|
||||
migration_plan = runner.runtime.plan_store_migration(source_ref=config.local_store_path)
|
||||
migration_apply = runner.runtime.apply_store_migration(
|
||||
migration_plan["data"]["migration_plan"],
|
||||
actor="operator",
|
||||
source_ref=config.local_store_path,
|
||||
)
|
||||
```
|
||||
|
||||
Repair diagnostics distinguish:
|
||||
@@ -100,6 +130,22 @@ Repair diagnostics distinguish:
|
||||
- `missing_edge_source` / `missing_edge_target` for graph reference damage.
|
||||
- `orphaned_path_event` when paths reference absent event-log records.
|
||||
|
||||
Migration apply is audited as `store.migration.apply` and updates metadata
|
||||
atomically when changes are needed.
|
||||
|
||||
## Audit Export And Retention Drill
|
||||
|
||||
Runtime audit behavior is inspectable beyond point queries:
|
||||
|
||||
```python
|
||||
export = runner.runtime.export_audit_events({"operation": "package.compile"})
|
||||
retention = runner.runtime.audit_retention_plan(retention_days=30)
|
||||
```
|
||||
|
||||
The export batch includes matching audit events and sink retention metadata.
|
||||
The retention plan identifies eligible operation ids but does not prune records;
|
||||
retention apply is a follow-on operational task.
|
||||
|
||||
## Adapter Pack Compatibility
|
||||
|
||||
Fake and future live adapter packs should publish a manifest with:
|
||||
@@ -108,13 +154,16 @@ Fake and future live adapter packs should publish a manifest with:
|
||||
- ownership boundaries for every adapter;
|
||||
- required conformance helpers.
|
||||
|
||||
Validate a pack before wiring it into the runtime:
|
||||
Validate fake or live-shaped packs before wiring them into the runtime:
|
||||
|
||||
```python
|
||||
from phase_memory import fake_external_adapter_pack, validate_adapter_pack_manifest
|
||||
from phase_memory import fake_external_adapter_pack, live_shaped_adapter_pack, validate_adapter_pack_manifest
|
||||
|
||||
diagnostics = validate_adapter_pack_manifest(fake_external_adapter_pack())
|
||||
assert diagnostics == ()
|
||||
|
||||
live_diagnostics = validate_adapter_pack_manifest(live_shaped_adapter_pack())
|
||||
assert live_diagnostics == ()
|
||||
```
|
||||
|
||||
Missing capabilities are reported as `missing_adapter_capability` diagnostics
|
||||
@@ -129,8 +178,12 @@ The stable embedding surface is:
|
||||
`service_contracts()["operations"]`.
|
||||
- `RuntimeConfig` and `resolve_runtime_adapters` for local/external adapter
|
||||
resolution.
|
||||
- `ServiceBinding` and `service_binding_from_config` for optional service
|
||||
wrappers.
|
||||
- Adapter conformance helpers in `phase_memory.service`.
|
||||
- External adapter pack manifests and validation helpers.
|
||||
- Public export and service operation snapshots in
|
||||
`tests/fixtures/public-api-snapshot.json`.
|
||||
|
||||
New public operations should be added to the service contract first, then to
|
||||
the local runner, runtime tests, and docs in the same change.
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
161
src/phase_memory/evaluation.py
Normal file
161
src/phase_memory/evaluation.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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":
|
||||
|
||||
164
src/phase_memory/service_binding.py
Normal file
164
src/phase_memory/service_binding.py
Normal 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"))
|
||||
102
tests/fixtures/public-api-snapshot.json
vendored
Normal file
102
tests/fixtures/public-api-snapshot.json
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"exports": [
|
||||
"ADAPTER_PACK_MANIFEST_SCHEMA",
|
||||
"ActivationPlan",
|
||||
"Diagnostic",
|
||||
"EVALUATION_REPORT_SCHEMA",
|
||||
"ExternalAdapterPack",
|
||||
"FakeExternalEventLog",
|
||||
"FakeExternalGraphStore",
|
||||
"FakeExternalPolicyGateway",
|
||||
"FakeExternalSemanticIndex",
|
||||
"FakeKontextualRuntimeRegistry",
|
||||
"FakeMarkitectPackageCompiler",
|
||||
"FakeTelemetryAuditSink",
|
||||
"LifecycleAction",
|
||||
"LifecycleActionKind",
|
||||
"LifecycleRuleConfig",
|
||||
"LifecycleState",
|
||||
"LiveShapedKontextualEventLog",
|
||||
"LiveShapedKontextualGraphStore",
|
||||
"LiveShapedKontextualRuntimeRegistry",
|
||||
"LiveShapedMarkitectPackageCompiler",
|
||||
"LiveShapedPermissionSemanticIndex",
|
||||
"LiveShapedPolicyGateway",
|
||||
"LiveShapedTelemetryAuditSink",
|
||||
"LocalMarkitectValidator",
|
||||
"LocalServiceRunner",
|
||||
"MARKITECT_PACKAGE_REQUEST_SCHEMA",
|
||||
"MARKITECT_PACKAGE_RESPONSE_SCHEMA",
|
||||
"MemoryEdge",
|
||||
"MemoryEvent",
|
||||
"MemoryGraph",
|
||||
"MemoryKind",
|
||||
"MemoryNode",
|
||||
"MemoryOperation",
|
||||
"MemoryPath",
|
||||
"MemoryPathState",
|
||||
"MemoryPhase",
|
||||
"OptionalMarkitectValidator",
|
||||
"POLICY_OPERATION_POINTS",
|
||||
"PhaseMemoryRuntime",
|
||||
"PhaseTransitionRule",
|
||||
"PolicyDecision",
|
||||
"ProfileExecutionPlan",
|
||||
"ProfileIntent",
|
||||
"READINESS_REPORT_SCHEMA",
|
||||
"RetentionRule",
|
||||
"ReviewDecision",
|
||||
"ReviewRecord",
|
||||
"RuntimeAdapterBundle",
|
||||
"RuntimeConfig",
|
||||
"SERVICE_BINDING_SCHEMA",
|
||||
"ServiceBinding",
|
||||
"ServiceResponse",
|
||||
"WordCountTokenEstimator",
|
||||
"abandon_path",
|
||||
"activation_quality_report",
|
||||
"adapter_pack_manifest",
|
||||
"branch_path",
|
||||
"compact_path",
|
||||
"create_path",
|
||||
"evaluation_threshold_report",
|
||||
"fake_external_adapter_pack",
|
||||
"fake_external_runtime_config",
|
||||
"graph_from_markitect",
|
||||
"health_report",
|
||||
"live_shaped_adapter_pack",
|
||||
"make_review_record",
|
||||
"merge_path",
|
||||
"package_request_from_selection",
|
||||
"package_response_envelope",
|
||||
"path_event",
|
||||
"plan_activation",
|
||||
"plan_compaction",
|
||||
"plan_lifecycle_from_profile",
|
||||
"plan_neighborhood_activation",
|
||||
"plan_phase_transition",
|
||||
"plan_phase_transitions_from_rules",
|
||||
"plan_profile_execution",
|
||||
"plan_refresh",
|
||||
"plan_retention",
|
||||
"plan_retention_from_rules",
|
||||
"profile_from_markitect",
|
||||
"resolve_runtime_adapters",
|
||||
"retrieve_graph_neighborhood",
|
||||
"runtime_from_config",
|
||||
"select_event_path",
|
||||
"service_binding_from_config",
|
||||
"service_contracts",
|
||||
"validate_adapter_pack_manifest"
|
||||
],
|
||||
"service_operations": [
|
||||
"audit.query",
|
||||
"graph.activation.plan",
|
||||
"graph.import",
|
||||
"graph.lifecycle.plan",
|
||||
"health.check",
|
||||
"lifecycle.apply",
|
||||
"package.compile",
|
||||
"profile.plan"
|
||||
]
|
||||
}
|
||||
72
tests/test_audit_operations.py
Normal file
72
tests/test_audit_operations.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from phase_memory.lifecycle import plan_compaction
|
||||
from phase_memory.models import MemoryNode
|
||||
from phase_memory.runtime import PhaseMemoryRuntime
|
||||
|
||||
|
||||
def test_audit_export_traces_policy_denial_package_compile_and_review_apply() -> None:
|
||||
runtime = PhaseMemoryRuntime()
|
||||
graph = {
|
||||
"schema_version": "markitect.memory.graph.v1",
|
||||
"id": "audit.graph",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node.public",
|
||||
"kind": "knowledge",
|
||||
"text": "Public node.",
|
||||
"policy": {"labels": ["public"], "trust_zone": "local"},
|
||||
},
|
||||
{
|
||||
"id": "node.secret",
|
||||
"kind": "knowledge",
|
||||
"text": "Secret node.",
|
||||
"policy": {"labels": ["restricted"], "secret": True, "trust_zone": "local"},
|
||||
},
|
||||
],
|
||||
"edges": [],
|
||||
"events": [],
|
||||
}
|
||||
|
||||
activation = runtime.plan_activation(
|
||||
graph,
|
||||
max_items=3,
|
||||
max_tokens=30,
|
||||
policy_context={"denied_labels": ["restricted"], "secrets_allowed": False, "trust_zone": "local"},
|
||||
)
|
||||
runtime.compile_package(activation["data"]["package_request"]["selection"])
|
||||
node = runtime.graph_store.save_node(MemoryNode("audit.review", "episode", "Review text"))
|
||||
compact = plan_compaction([node])
|
||||
runtime.apply_lifecycle_actions([compact])
|
||||
runtime.apply_lifecycle_actions([compact], approval_marker="review:audit")
|
||||
|
||||
export = runtime.export_audit_events()
|
||||
operations = [event["operation"] for event in export["batch"]["events"]]
|
||||
|
||||
assert export["valid"] is True
|
||||
assert "graph.activation.plan" in operations
|
||||
assert "package.compile" in operations
|
||||
assert operations.count("lifecycle.apply") == 2
|
||||
assert activation["data"]["policy_denials"][0]["id"] == "node.secret"
|
||||
|
||||
|
||||
def test_audit_retention_plan_identifies_eligible_records() -> None:
|
||||
runtime = PhaseMemoryRuntime()
|
||||
runtime.audit_sink.record(
|
||||
{
|
||||
"schema_version": "phase_memory.audit.event.v1",
|
||||
"operation_id": "op:old",
|
||||
"operation": "manual",
|
||||
"timestamp": "2026-01-01T00:00:00+00:00",
|
||||
"subject": {"kind": "audit_events", "id": "old"},
|
||||
"source": {"ref": "test"},
|
||||
"dry_run": True,
|
||||
"allowed": True,
|
||||
}
|
||||
)
|
||||
|
||||
plan = runtime.audit_retention_plan(retention_days=30, now=datetime(2026, 5, 19, tzinfo=timezone.utc))
|
||||
|
||||
assert plan["valid"] is True
|
||||
assert plan["plan"]["eligible_operation_ids"] == ["op:old"]
|
||||
assert plan["plan"]["eligible_count"] == 1
|
||||
@@ -4,6 +4,7 @@ from pathlib import Path
|
||||
|
||||
from phase_memory.adapters import InMemorySemanticIndex
|
||||
from phase_memory.contracts import graph_from_markitect
|
||||
from phase_memory.evaluation import EVALUATION_REPORT_SCHEMA, evaluation_threshold_report
|
||||
from phase_memory.models import ActivationPlan, MemoryPath
|
||||
from phase_memory.retrieval import activation_quality_report, select_event_path
|
||||
from phase_memory.runtime import PhaseMemoryRuntime
|
||||
@@ -85,6 +86,22 @@ def test_budget_path_and_semantic_hint_scenario_meets_quality_thresholds() -> No
|
||||
assert report["explanation_coverage"] == 1.0
|
||||
|
||||
|
||||
def test_evaluation_threshold_report_summarizes_all_scenarios() -> None:
|
||||
data = json.loads((FIXTURES / "evaluation-scenarios.json").read_text(encoding="utf-8"))
|
||||
|
||||
report = evaluation_threshold_report(data)
|
||||
|
||||
assert report["schema_version"] == EVALUATION_REPORT_SCHEMA
|
||||
assert report["valid"] is True
|
||||
assert report["metrics"]["scenario_count"] == 3
|
||||
assert report["metrics"]["policy_denial_count"] == 1
|
||||
assert report["metrics"]["lifecycle_action_count"] >= 3
|
||||
assert report["metrics"]["path_event_count"] == 1
|
||||
assert report["metrics"]["semantic_hit_count"] == 1
|
||||
assert report["metrics"]["budget_omission_count"] == 1
|
||||
assert report["diagnostics"] == []
|
||||
|
||||
|
||||
def _activation_plan(response):
|
||||
data = response["data"]["activation_plan"]
|
||||
return ActivationPlan(
|
||||
|
||||
@@ -7,6 +7,7 @@ from phase_memory.external_adapters import (
|
||||
adapter_pack_manifest,
|
||||
fake_external_adapter_pack,
|
||||
fake_external_runtime_config,
|
||||
live_shaped_adapter_pack,
|
||||
validate_adapter_pack_manifest,
|
||||
)
|
||||
from phase_memory.service import (
|
||||
@@ -64,6 +65,7 @@ def test_adapter_pack_manifest_reports_missing_capabilities() -> None:
|
||||
capabilities=tuple(capability for capability in pack.capabilities if capability != "telemetry.audit.fake"),
|
||||
ownership_boundaries=pack.ownership_boundaries,
|
||||
required_conformance=pack.required_conformance,
|
||||
capability_requirements=pack.capability_requirements,
|
||||
metadata=pack.metadata,
|
||||
)
|
||||
|
||||
@@ -100,3 +102,32 @@ def test_external_runtime_config_resolves_supplied_fake_pack() -> None:
|
||||
assert fetched["operation"] == "package.compile"
|
||||
assert report["ok"] is True
|
||||
assert report["adapters"]["package_compiler"] == "FakeMarkitectPackageCompiler"
|
||||
|
||||
|
||||
def test_live_shaped_adapter_pack_uses_same_manifest_and_conformance_contract() -> None:
|
||||
config = fake_external_runtime_config()
|
||||
pack = live_shaped_adapter_pack()
|
||||
manifest = adapter_pack_manifest(pack)
|
||||
bundle = resolve_runtime_adapters(config, external_adapters=pack.adapters)
|
||||
runtime = runtime_from_config(config, external_adapters=pack.adapters)
|
||||
|
||||
assert validate_adapter_pack_manifest(pack) == ()
|
||||
assert manifest["metadata"]["network_required"] is False
|
||||
assert manifest["adapters"]["package_compiler"]["required_capabilities"] == ["markitect.package.compile.live-shaped"]
|
||||
assert not [diagnostic for diagnostic in bundle.diagnostics if diagnostic.severity == "error"]
|
||||
|
||||
envelope = runtime.compile_package(
|
||||
{
|
||||
"schema_version": "markitect.memory.selection.v1",
|
||||
"id": "selection.live-shaped",
|
||||
"nodes": ["decision.boundary"],
|
||||
"events": [],
|
||||
}
|
||||
)
|
||||
registry_receipt = bundle.runtime_registry.publish_runtime_envelope(envelope)
|
||||
fetched = bundle.runtime_registry.fetch_runtime_envelope(registry_receipt["reference"])
|
||||
export = runtime.export_audit_events({"operation": "package.compile"})
|
||||
|
||||
assert envelope["data"]["package_response"]["package_ref"].startswith("markitect-live-shaped:")
|
||||
assert fetched["operation"] == "package.compile"
|
||||
assert export["batch"]["retention"]["mode"] == "live_shaped_telemetry"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from phase_memory.adapters import FileBackedMemoryGraphStore, JsonlAuditSink, JsonlMemoryEventLog
|
||||
from phase_memory.adapters import FileBackedMemoryGraphStore, JsonlAuditSink, JsonlMemoryEventLog, LOCAL_STORE_SCHEMA
|
||||
from phase_memory.lifecycle import plan_compaction, plan_retention
|
||||
from phase_memory.models import LifecycleAction, LifecycleActionKind, LifecycleState, MemoryEdge, MemoryEvent, MemoryNode
|
||||
from phase_memory.paths import abandon_path, branch_path, create_path, merge_path, path_event
|
||||
@@ -112,6 +112,55 @@ def test_file_backed_store_reports_migration_needs_and_uses_atomic_json_writes(t
|
||||
assert not list(tmp_path.rglob("*.tmp"))
|
||||
|
||||
|
||||
def test_local_store_migration_apply_updates_metadata_and_audits(tmp_path) -> None:
|
||||
store = FileBackedMemoryGraphStore(tmp_path)
|
||||
runtime = PhaseMemoryRuntime(graph_store=store, event_log=JsonlMemoryEventLog(tmp_path / "events.jsonl"))
|
||||
(tmp_path / "phase-memory.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": "phase_memory.local_store.v0",
|
||||
"planned_migrations": ["v0-to-v1"],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
planned = runtime.plan_store_migration(source_ref=str(tmp_path))
|
||||
applied = runtime.apply_store_migration(planned["data"]["migration_plan"], actor="pytest", source_ref=str(tmp_path))
|
||||
metadata = store.metadata()
|
||||
audit = runtime.query_audit({"operation": "store.migration.apply", "dry_run": False})
|
||||
|
||||
assert planned["valid"] is True
|
||||
assert [action["action"] for action in planned["data"]["migration_plan"]["actions"]] == [
|
||||
"set_schema_version",
|
||||
"complete_planned_migration",
|
||||
]
|
||||
assert applied["valid"] is True
|
||||
assert applied["data"]["migration_result"]["changed"] is True
|
||||
assert metadata["schema_version"] == LOCAL_STORE_SCHEMA
|
||||
assert metadata["completed_migrations"] == ["v0-to-v1"]
|
||||
assert metadata["last_migration"]["actor"] == "pytest"
|
||||
assert audit["count"] == 1
|
||||
|
||||
|
||||
def test_local_store_migration_noop_and_corrupt_metadata_paths(tmp_path) -> None:
|
||||
store = FileBackedMemoryGraphStore(tmp_path)
|
||||
runtime = PhaseMemoryRuntime(graph_store=store, event_log=JsonlMemoryEventLog(tmp_path / "events.jsonl"))
|
||||
|
||||
noop = runtime.apply_store_migration(source_ref=str(tmp_path))
|
||||
assert noop["valid"] is True
|
||||
assert noop["data"]["migration_result"]["changed"] is False
|
||||
|
||||
(tmp_path / "phase-memory.json").write_text("{not-json}\n", encoding="utf-8")
|
||||
planned = runtime.plan_store_migration(source_ref=str(tmp_path))
|
||||
failed = runtime.apply_store_migration(source_ref=str(tmp_path))
|
||||
|
||||
assert planned["valid"] is False
|
||||
assert planned["diagnostics"][0]["code"] == "corrupt_store_metadata"
|
||||
assert failed["valid"] is False
|
||||
assert failed["data"]["migration_result"]["applied"] is False
|
||||
|
||||
|
||||
def test_repair_diagnostics_distinguish_corrupt_store_records(tmp_path) -> None:
|
||||
store = FileBackedMemoryGraphStore(tmp_path)
|
||||
runtime = PhaseMemoryRuntime(graph_store=store, event_log=JsonlMemoryEventLog(tmp_path / "events.jsonl"))
|
||||
|
||||
21
tests/test_public_api.py
Normal file
21
tests/test_public_api.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import phase_memory
|
||||
from phase_memory.service import LocalServiceRunner, SERVICE_OPERATIONS, service_contracts
|
||||
|
||||
|
||||
FIXTURES = Path(__file__).parent / "fixtures"
|
||||
|
||||
|
||||
def test_public_api_snapshot_is_explicit() -> None:
|
||||
snapshot = json.loads((FIXTURES / "public-api-snapshot.json").read_text(encoding="utf-8"))
|
||||
|
||||
assert sorted(phase_memory.__all__) == snapshot["exports"]
|
||||
assert sorted(SERVICE_OPERATIONS) == snapshot["service_operations"]
|
||||
|
||||
|
||||
def test_service_contract_catalog_matches_local_runner_supported_operations() -> None:
|
||||
declared = set(service_contracts()["operations"])
|
||||
|
||||
assert set(LocalServiceRunner.supported_operations()) == declared
|
||||
58
tests/test_service_binding.py
Normal file
58
tests/test_service_binding.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import json
|
||||
from io import BytesIO
|
||||
|
||||
from phase_memory import LocalServiceRunner, ServiceBinding
|
||||
from phase_memory.service_binding import READINESS_REPORT_SCHEMA
|
||||
|
||||
|
||||
def test_service_binding_exposes_health_readiness_and_contracts_without_listener() -> None:
|
||||
binding = ServiceBinding(LocalServiceRunner())
|
||||
|
||||
health = binding.route("GET", "/health")
|
||||
ready = binding.route("GET", "/ready")
|
||||
contracts = binding.route("GET", "/contracts")
|
||||
|
||||
assert health.status == 200
|
||||
assert ready.status == 200
|
||||
assert ready.body["schema_version"] == READINESS_REPORT_SCHEMA
|
||||
assert ready.body["unsupported_operations"] == []
|
||||
assert "package.compile" in contracts.body["operations"]
|
||||
|
||||
|
||||
def test_service_binding_dispatches_operation_payloads() -> None:
|
||||
binding = ServiceBinding(LocalServiceRunner())
|
||||
selection = {
|
||||
"schema_version": "markitect.memory.selection.v1",
|
||||
"id": "binding.selection",
|
||||
"nodes": [],
|
||||
"events": [],
|
||||
}
|
||||
|
||||
response = binding.route("POST", "/operations/package.compile", {"selection": selection})
|
||||
|
||||
assert response.status == 200
|
||||
assert response.body["operation"] == "package.compile"
|
||||
assert response.body["data"]["package_response"]["package_ref"] == "package:binding.selection"
|
||||
|
||||
|
||||
def test_service_binding_wsgi_adapter_is_callable_without_starting_server() -> None:
|
||||
binding = ServiceBinding(LocalServiceRunner())
|
||||
app = binding.as_wsgi_app()
|
||||
statuses: list[str] = []
|
||||
headers: list[list[tuple[str, str]]] = []
|
||||
|
||||
payload = json.dumps({"operation": "health.check", "payload": {}}).encode("utf-8")
|
||||
result = app(
|
||||
{
|
||||
"REQUEST_METHOD": "POST",
|
||||
"PATH_INFO": "/operations",
|
||||
"CONTENT_LENGTH": str(len(payload)),
|
||||
"wsgi.input": BytesIO(payload),
|
||||
},
|
||||
lambda status, response_headers: (statuses.append(status), headers.append(response_headers)),
|
||||
)
|
||||
|
||||
body = json.loads(b"".join(result).decode("utf-8"))
|
||||
assert statuses == ["200 OK"]
|
||||
assert body["schema_version"] == "phase_memory.health.report.v1"
|
||||
assert ("content-type", "application/json") in headers[0]
|
||||
@@ -4,11 +4,11 @@ type: workplan
|
||||
title: "Live Adapter And Service Binding Readiness"
|
||||
domain: markitect
|
||||
repo: phase-memory
|
||||
status: ready
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: phase-memory
|
||||
created: "2026-05-18"
|
||||
updated: "2026-05-18"
|
||||
updated: "2026-05-19"
|
||||
state_hub_workstream_id: "427b91ad-9df1-4053-aeb0-54ee39b6bf62"
|
||||
---
|
||||
|
||||
@@ -38,7 +38,7 @@ evaluation fixtures, adapter pack manifests, and operational recipes.
|
||||
|
||||
```task
|
||||
id: PMEM-WP-0012-T01
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "1244aabb-b8a3-4053-8454-499e8772f5bf"
|
||||
```
|
||||
@@ -56,7 +56,7 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: PMEM-WP-0012-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "b8d3e7a0-c538-4d6c-b2f8-7c33b17c850a"
|
||||
```
|
||||
@@ -73,7 +73,7 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: PMEM-WP-0012-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "e385af31-13f2-4be0-8fcf-89586e2d3954"
|
||||
```
|
||||
@@ -91,7 +91,7 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: PMEM-WP-0012-T04
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "d203294a-bf5a-43d0-a88c-086a3406940d"
|
||||
```
|
||||
@@ -108,7 +108,7 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: PMEM-WP-0012-T05
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "305729e2-23ff-4043-9356-0df83f8e6d7b"
|
||||
```
|
||||
@@ -127,7 +127,7 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: PMEM-WP-0012-T06
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "78f9d0d8-dc9d-4f43-a32d-92e17b3c5122"
|
||||
```
|
||||
@@ -149,4 +149,26 @@ Acceptance:
|
||||
|
||||
## Closure Review
|
||||
|
||||
Pending implementation.
|
||||
Completed on 2026-05-19.
|
||||
|
||||
Implemented:
|
||||
|
||||
- Optional framework-neutral `ServiceBinding` with health, readiness,
|
||||
contracts, operation dispatch, and WSGI callable coverage without starting a
|
||||
listener.
|
||||
- Executable local-store migration planning and apply behavior with atomic
|
||||
metadata updates and audited `store.migration.apply` events.
|
||||
- Live-shaped Markitect, Kontextual, policy, telemetry, semantic-index, and
|
||||
runtime-registry adapter fixtures behind the same manifest and conformance
|
||||
suite used by fake packs.
|
||||
- Audit export batches and retention plans for recording, JSONL, and telemetry
|
||||
audit sinks.
|
||||
- Evaluation threshold reporting over the policy/lifecycle/path/semantic/budget
|
||||
scenario corpus.
|
||||
- Public API and service operation compatibility snapshots with documentation.
|
||||
- Scorecard update from 4.0 to 4.2 and PMEM-WP-0013 as the next ready
|
||||
refinement workplan.
|
||||
|
||||
Verification:
|
||||
|
||||
- Focused PMEM-WP-0012 tests passed: 38 tests.
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
---
|
||||
id: PMEM-WP-0013
|
||||
type: workplan
|
||||
title: "Credentialed Adapter Drills And Deployment Packaging"
|
||||
domain: markitect
|
||||
repo: phase-memory
|
||||
status: ready
|
||||
owner: codex
|
||||
topic_slug: phase-memory
|
||||
created: "2026-05-19"
|
||||
updated: "2026-05-19"
|
||||
state_hub_workstream_id: "3343c4bf-40f4-42c9-9713-2a441349f723"
|
||||
---
|
||||
|
||||
# PMEM-WP-0013: Credentialed Adapter Drills And Deployment Packaging
|
||||
|
||||
## Goal
|
||||
|
||||
Move from live-shaped local fixtures to optional credentialed operational
|
||||
drills, deployable service packaging, and compatibility release discipline.
|
||||
|
||||
## Current Evidence
|
||||
|
||||
`PMEM-WP-0012` added service binding, executable local-store migrations,
|
||||
live-shaped adapter packs, audit retention/export plans, evaluation threshold
|
||||
reports, and public API snapshots. The scorecard now rates the repo at
|
||||
**4.2 / 5**.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Require live credentials in default tests.
|
||||
- Store credentials, tokens, or endpoints in Git.
|
||||
- Make one hosted service topology mandatory.
|
||||
- Move Markitect or Kontextual ownership into phase-memory.
|
||||
|
||||
## T01 - Add credential-gated adapter smoke drills
|
||||
|
||||
```task
|
||||
id: PMEM-WP-0013-T01
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "e4940a9d-130e-47ea-ba16-7b090841855c"
|
||||
```
|
||||
|
||||
Add optional smoke tests for credentialed Markitect/Kontextual adapters that
|
||||
skip unless explicit environment variables are present.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Default test suite remains offline and deterministic.
|
||||
- Credentialed drills reuse adapter manifest validation and conformance helpers.
|
||||
- Skipped tests clearly list the required environment variables.
|
||||
|
||||
## T02 - Package the service binding for deployment
|
||||
|
||||
```task
|
||||
id: PMEM-WP-0013-T02
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "bf8d2159-761a-47f5-b7be-41ad52460b64"
|
||||
```
|
||||
|
||||
Add a deployable service entrypoint around the framework-neutral binding.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- The package can expose health, readiness, contracts, and operation dispatch
|
||||
over a local service process.
|
||||
- Deployment remains optional and dependency-light.
|
||||
- Tests cover entrypoint construction without opening a real listener.
|
||||
|
||||
## T03 - Add operator readiness runbook
|
||||
|
||||
```task
|
||||
id: PMEM-WP-0013-T03
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "7e39e894-8754-4977-abdd-00f3bf1a73d1"
|
||||
```
|
||||
|
||||
Document service startup, readiness checks, migration apply, audit export, and
|
||||
rollback expectations for operators.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Runbook covers happy path and failure triage.
|
||||
- It distinguishes local fixture mode, live-shaped mode, and credentialed live
|
||||
mode.
|
||||
- It links back to API compatibility and maturity gates.
|
||||
|
||||
## T04 - Enforce audit retention pruning plans
|
||||
|
||||
```task
|
||||
id: PMEM-WP-0013-T04
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "b23e3126-bbfa-44b1-b2a1-22cda968f5d8"
|
||||
```
|
||||
|
||||
Move from retention planning to explicit retention apply behavior.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Audit sinks can produce and apply deterministic pruning plans.
|
||||
- Apply behavior records an audit event.
|
||||
- Tests cover no-op, eligible-record, and unsupported-sink paths.
|
||||
|
||||
## T05 - Add evaluation trend artifacts
|
||||
|
||||
```task
|
||||
id: PMEM-WP-0013-T05
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "2e71fedd-aac6-42c9-822c-6305412ea064"
|
||||
```
|
||||
|
||||
Persist deterministic evaluation reports as trendable artifacts.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Reports include run metadata and threshold deltas.
|
||||
- Fixtures can be compared without live dependencies.
|
||||
- Regression diagnostics remain actionable.
|
||||
|
||||
## T06 - Add compatibility release notes template
|
||||
|
||||
```task
|
||||
id: PMEM-WP-0013-T06
|
||||
status: todo
|
||||
priority: low
|
||||
state_hub_task_id: "c1a8f699-9a0b-4983-8d35-e59cd124dd58"
|
||||
```
|
||||
|
||||
Add release-note and migration-note templates for public API changes.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Template calls out changed exports, changed service operations, migration
|
||||
needs, and operator action.
|
||||
- API snapshot updates require a release note reference.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- The project has concrete evidence toward the 4.3+ scorecard gate.
|
||||
- Credentialed adapter drills are available but never required by default.
|
||||
- Service binding can be packaged and exercised as an operator-facing service.
|
||||
|
||||
## Closure Review
|
||||
|
||||
Pending implementation.
|
||||
Reference in New Issue
Block a user