Implement live-shaped readiness workplan

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

View File

@@ -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
View 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.

View File

@@ -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:

View File

@@ -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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

102
tests/fixtures/public-api-snapshot.json vendored Normal file
View 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"
]
}

View 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

View File

@@ -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(

View File

@@ -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"

View File

@@ -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
View 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

View 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]

View File

@@ -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.

View File

@@ -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.