From 1efb7d4c13c9146a091ae38652819549252b4ad1 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 18 May 2026 20:04:40 +0200 Subject: [PATCH] Add Markitect bridge and activation quality --- README.md | 5 +- docs/activation-quality.md | 52 +++++ docs/markitect-interop.md | 93 +++++++++ src/phase_memory/__init__.py | 26 +++ src/phase_memory/activation.py | 11 ++ src/phase_memory/bridge.py | 97 ++++++++++ src/phase_memory/retrieval.py | 182 ++++++++++++++++++ src/phase_memory/runtime.py | 14 +- tests/fixtures/activation-quality-report.json | 16 ++ tests/fixtures/markitect-invalid-graph.json | 18 ++ tests/fixtures/markitect-invalid-profile.json | 4 + .../fixtures/markitect-package-response.json | 8 + .../runtime-activation-plan-snapshot.json | 4 +- tests/test_markitect_bridge.py | 92 +++++++++ tests/test_retrieval_quality.py | 95 +++++++++ tests/test_runtime.py | 3 +- workplans/PMEM-MATURITY-SCORECARD.md | 67 +++++-- ...ect-package-bridge-and-contract-interop.md | 62 +++++- ...ieval-activation-quality-and-evaluation.md | 65 ++++++- 19 files changed, 871 insertions(+), 43 deletions(-) create mode 100644 docs/activation-quality.md create mode 100644 docs/markitect-interop.md create mode 100644 src/phase_memory/bridge.py create mode 100644 src/phase_memory/retrieval.py create mode 100644 tests/fixtures/activation-quality-report.json create mode 100644 tests/fixtures/markitect-invalid-graph.json create mode 100644 tests/fixtures/markitect-invalid-profile.json create mode 100644 tests/fixtures/markitect-package-response.json create mode 100644 tests/test_markitect_bridge.py create mode 100644 tests/test_retrieval_quality.py diff --git a/README.md b/README.md index 67da1ef..c06febc 100644 --- a/README.md +++ b/README.md @@ -88,4 +88,7 @@ and a package compilation request for the `ContextPackageCompiler` boundary. See [docs/architecture.md](docs/architecture.md) for the first architecture sketch, [docs/local-persistence.md](docs/local-persistence.md) for the local file-backed adapter, [docs/policy-audit.md](docs/policy-audit.md) for local -policy and review gates, and [SCOPE.md](SCOPE.md) for repository boundaries. +policy and review gates, [docs/markitect-interop.md](docs/markitect-interop.md) +for package bridge boundaries, [docs/activation-quality.md](docs/activation-quality.md) +for retrieval and evaluation behavior, and [SCOPE.md](SCOPE.md) for repository +boundaries. diff --git a/docs/activation-quality.md b/docs/activation-quality.md new file mode 100644 index 0000000..6a48ce9 --- /dev/null +++ b/docs/activation-quality.md @@ -0,0 +1,52 @@ +# Activation Quality + +`phase-memory` activation can now be planned from deterministic graph +neighborhoods and event paths without requiring embeddings, vector stores, or +LLM ranking. + +## Graph Neighborhood Retrieval + +`retrieve_graph_neighborhood` expands from seed nodes across graph edges. + +Supported controls: + +- maximum hops +- edge kind filters +- `in`, `out`, or `both` direction +- phase filters +- memory kind filters + +Candidates receive deterministic scores based on explicit priority, graph +distance, phase, lifecycle state, confidence, source-backed status, freshness, +and policy allowance. + +## Event Path Activation + +`select_event_path` activates ordered event ids from a structured +`MemoryPath`. Active paths are selected by default. Abandoned, merged, or +compacted paths can be included explicitly when a caller wants to inspect +inactive branches. + +## Token Estimation + +`TokenEstimator` is a protocol. The default implementation is +`WordCountTokenEstimator`, which keeps tests deterministic and dependency-free. +Provider-specific tokenizers can be supplied later without changing retrieval +contracts. + +## Quality Report + +`activation_quality_report` emits local metrics suitable for later export to +evaluation systems: + +- selected expected nodes +- omitted required nodes +- policy-denied required nodes +- token budget utilization +- stale item activation count +- provenance coverage +- source span coverage +- explanation coverage + +The fixture `tests/fixtures/activation-quality-report.json` pins a small +expected report for regression tests. diff --git a/docs/markitect-interop.md b/docs/markitect-interop.md new file mode 100644 index 0000000..96f71ab --- /dev/null +++ b/docs/markitect-interop.md @@ -0,0 +1,93 @@ +# Markitect Interop + +`phase-memory` consumes and emits Markitect-compatible memory contracts while +keeping ownership boundaries explicit. + +## Ownership + +Markitect owns: + +- markdown-facing memory profile syntax +- memory graph contract vocabulary +- memory selection validation +- context-package internals +- package compilation semantics + +`phase-memory` owns: + +- phase-aware runtime planning +- lifecycle planning +- activation planning +- policy/audit/review checks around memory runtime behavior +- adapter orchestration +- package compile request handoff + +## Contract Inputs + +The dependency-light boundary accepts already-valid dictionaries for: + +- `markitect.memory.profile.v1` +- `markitect.memory.graph.v1` +- `markitect.memory.selection.v1` + +`LocalMarkitectValidator` uses local ingress diagnostics only. It checks +schema ids, required ids, known first-slice memory kinds, and graph edge +integrity. It does not claim to be the Markitect schema owner. + +`OptionalMarkitectValidator` can wrap a Markitect-owned validator object with +methods such as `validate_memory_profile`, `validate_memory_graph`, and +`validate_memory_selection`. If no delegate is configured, it falls back to the +local boundary. + +## Package Request + +Activation planning emits a Markitect-compatible selection and a package +request envelope: + +```text +phase_memory.markitect.package_request.v1 +``` + +The request includes: + +- selection id +- graph id +- profile id +- selected node ids +- selected event ids +- budget metadata +- policy metadata +- selected item provenance metadata +- compiler name +- compiler diagnostics +- original selection + +Selected item metadata preserves source spans, provenance, confidence, +freshness, namespace, policy labels, and reason selected. This gives Markitect +enough input to build inspectable packages without requiring phase-memory to +understand package internals. + +## Package Response + +Package compiler responses are wrapped in: + +```text +phase_memory.markitect.package_response.v1 +``` + +The wrapper keeps the Markitect response opaque and extracts only a +`package_ref` when present. + +## Fixture Catalog + +Compatibility fixtures live under `tests/fixtures/`: + +- `memory-profile.json` - valid Markitect-compatible profile +- `memory-graph.json` - valid Markitect-compatible graph +- `markitect-invalid-profile.json` - invalid profile diagnostics +- `markitect-invalid-graph.json` - invalid graph diagnostics +- `runtime-activation-plan-snapshot.json` - activation/package request shape +- `markitect-package-response.json` - opaque package response fixture + +These fixtures are small and deterministic so adjacent repositories can reuse +them as examples without installing Markitect. diff --git a/src/phase_memory/__init__.py b/src/phase_memory/__init__.py index 6024d8d..e60399e 100644 --- a/src/phase_memory/__init__.py +++ b/src/phase_memory/__init__.py @@ -1,6 +1,14 @@ """Profile-driven memory phase planning.""" from .activation import plan_activation +from .bridge import ( + MARKITECT_PACKAGE_REQUEST_SCHEMA, + MARKITECT_PACKAGE_RESPONSE_SCHEMA, + LocalMarkitectValidator, + OptionalMarkitectValidator, + package_request_from_selection, + package_response_envelope, +) from .contracts import graph_from_markitect, profile_from_markitect from .lifecycle import ( plan_compaction, @@ -30,6 +38,13 @@ from .models import ( ) from .paths import abandon_path, branch_path, compact_path, create_path, merge_path, path_event from .policy import POLICY_OPERATION_POINTS, MemoryOperation, make_review_record +from .retrieval import ( + WordCountTokenEstimator, + activation_quality_report, + plan_neighborhood_activation, + retrieve_graph_neighborhood, + select_event_path, +) from .planner import plan_profile_execution from .runtime import PhaseMemoryRuntime @@ -55,6 +70,10 @@ __all__ = [ "PhaseMemoryRuntime", "POLICY_OPERATION_POINTS", "MemoryOperation", + "MARKITECT_PACKAGE_REQUEST_SCHEMA", + "MARKITECT_PACKAGE_RESPONSE_SCHEMA", + "LocalMarkitectValidator", + "OptionalMarkitectValidator", "abandon_path", "branch_path", "compact_path", @@ -70,6 +89,13 @@ __all__ = [ "plan_retention", "profile_from_markitect", "path_event", + "package_request_from_selection", + "package_response_envelope", + "WordCountTokenEstimator", + "activation_quality_report", + "plan_neighborhood_activation", + "retrieve_graph_neighborhood", + "select_event_path", ] __version__ = "0.1.0" diff --git a/src/phase_memory/activation.py b/src/phase_memory/activation.py index a65444b..9d9fa8a 100644 --- a/src/phase_memory/activation.py +++ b/src/phase_memory/activation.py @@ -17,6 +17,7 @@ def plan_activation( ) -> ActivationPlan: selected: list[str] = [] omitted: list[dict[str, object]] = [] + selected_items: dict[str, dict[str, object]] = {} token_estimate = 0 ordered_nodes = _ordered_nodes(graph, priority_node_ids) @@ -29,6 +30,15 @@ def plan_activation( omitted.append({"id": node.node_id, "reason": "max_tokens", "tokens": node_tokens}) continue selected.append(node.node_id) + selected_items[node.node_id] = { + "source_spans": list(node.source_spans), + "provenance": list(node.provenance), + "confidence": node.confidence, + "freshness": dict(node.freshness), + "namespace": dict(node.namespace), + "policy": dict(node.policy), + "reason_selected": "priority" if node.node_id in priority_node_ids else "budget_order", + } token_estimate += node_tokens selected_event_ids: tuple[str, ...] = () @@ -54,6 +64,7 @@ def plan_activation( "max_items": max_items, "max_tokens": max_tokens, "omitted": omitted, + "selected_items": selected_items, }, } diff --git a/src/phase_memory/bridge.py b/src/phase_memory/bridge.py new file mode 100644 index 0000000..3af7806 --- /dev/null +++ b/src/phase_memory/bridge.py @@ -0,0 +1,97 @@ +"""Markitect package bridge helpers.""" + +from __future__ import annotations + +from typing import Any, Protocol + +from .contracts import ContractIngressResult, graph_from_markitect, profile_from_markitect, selection_from_markitect +from .models import Diagnostic +from .utils import stable_digest + +MARKITECT_PACKAGE_REQUEST_SCHEMA = "phase_memory.markitect.package_request.v1" +MARKITECT_PACKAGE_RESPONSE_SCHEMA = "phase_memory.markitect.package_response.v1" + + +class MarkitectValidator(Protocol): + def validate_profile(self, data: dict[str, Any]) -> ContractIngressResult: ... + def validate_graph(self, data: dict[str, Any]) -> ContractIngressResult: ... + def validate_selection(self, data: dict[str, Any]) -> ContractIngressResult: ... + + +class LocalMarkitectValidator: + """Dependency-light validation boundary using local ingress diagnostics.""" + + def validate_profile(self, data: dict[str, Any]) -> ContractIngressResult: + return profile_from_markitect(data) + + def validate_graph(self, data: dict[str, Any]) -> ContractIngressResult: + return graph_from_markitect(data) + + def validate_selection(self, data: dict[str, Any]) -> ContractIngressResult: + return selection_from_markitect(data) + + +class OptionalMarkitectValidator: + """Adapter shell for optional Markitect-owned validation.""" + + def __init__(self, delegate: Any | None = None) -> None: + self.delegate = delegate + self.local = LocalMarkitectValidator() + + def validate_profile(self, data: dict[str, Any]) -> ContractIngressResult: + if self.delegate and hasattr(self.delegate, "validate_memory_profile"): + return self.delegate.validate_memory_profile(data) + return self.local.validate_profile(data) + + def validate_graph(self, data: dict[str, Any]) -> ContractIngressResult: + if self.delegate and hasattr(self.delegate, "validate_memory_graph"): + return self.delegate.validate_memory_graph(data) + return self.local.validate_graph(data) + + def validate_selection(self, data: dict[str, Any]) -> ContractIngressResult: + if self.delegate and hasattr(self.delegate, "validate_memory_selection"): + return self.delegate.validate_memory_selection(data) + return self.local.validate_selection(data) + + +def package_request_from_selection( + selection: dict[str, Any], + *, + compiler: str, + diagnostics: tuple[Diagnostic, ...] = (), +) -> dict[str, Any]: + metadata = dict(selection.get("metadata") or {}) + request_id = f"markitect-package-request:{stable_digest([selection, compiler])}" + return { + "schema_version": MARKITECT_PACKAGE_REQUEST_SCHEMA, + "id": request_id, + "selection_id": selection.get("id"), + "graph_id": selection.get("graph"), + "profile_id": selection.get("profile"), + "selected_nodes": list(selection.get("nodes", ())), + "selected_events": list(selection.get("events", ())), + "budget": { + "max_items": metadata.get("max_items"), + "max_tokens": metadata.get("max_tokens"), + "token_estimate": metadata.get("token_estimate"), + }, + "policy": metadata.get("policy", {}), + "provenance": { + "selected_items": metadata.get("selected_items", {}), + "planned_by": metadata.get("planned_by"), + }, + "compiler": compiler, + "compiler_diagnostics": [diagnostic.to_dict() for diagnostic in diagnostics], + "selection": dict(selection), + "dry_run": True, + } + + +def package_response_envelope(response: dict[str, Any], *, request_id: str) -> dict[str, Any]: + return { + "schema_version": MARKITECT_PACKAGE_RESPONSE_SCHEMA, + "request_id": request_id, + "package_ref": response.get("package_id") or response.get("package_ref") or "", + "compiler_diagnostics": list(response.get("diagnostics", ())), + "response": dict(response), + } diff --git a/src/phase_memory/retrieval.py b/src/phase_memory/retrieval.py new file mode 100644 index 0000000..4e0e8a8 --- /dev/null +++ b/src/phase_memory/retrieval.py @@ -0,0 +1,182 @@ +"""Deterministic retrieval and activation quality helpers.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Protocol + +from .activation import plan_activation +from .models import ActivationPlan, MemoryEvent, MemoryGraph, MemoryNode, MemoryPath, MemoryPathState + + +class TokenEstimator(Protocol): + def estimate_text(self, text: str) -> int: ... + def estimate_node(self, node: MemoryNode) -> int: ... + def estimate_event(self, event: MemoryEvent) -> int: ... + + +class WordCountTokenEstimator: + def estimate_text(self, text: str) -> int: + return max(1, len(text.split())) + + def estimate_node(self, node: MemoryNode) -> int: + return self.estimate_text(node.text or node.kind) + + def estimate_event(self, event: MemoryEvent) -> int: + return self.estimate_text(" ".join([event.kind, *event.package_refs, *event.activation_refs])) + + +@dataclass(frozen=True) +class RetrievalCandidate: + node_id: str + distance: int + score: int + reasons: tuple[str, ...] + + def to_dict(self) -> dict: + return { + "node_id": self.node_id, + "distance": self.distance, + "score": self.score, + "reasons": list(self.reasons), + } + + +def retrieve_graph_neighborhood( + graph: MemoryGraph, + *, + seed_node_ids: tuple[str, ...], + max_hops: int = 1, + edge_kinds: tuple[str, ...] = (), + direction: str = "both", + phases: tuple[str, ...] = (), + kinds: tuple[str, ...] = (), +) -> tuple[RetrievalCandidate, ...]: + distances = {node_id: 0 for node_id in seed_node_ids if node_id in graph.node_ids} + frontier = list(distances) + for hop in range(1, max_hops + 1): + next_frontier: list[str] = [] + for node_id in frontier: + for edge in graph.edges: + if edge_kinds and edge.kind not in edge_kinds: + continue + neighbors: list[str] = [] + if direction in {"out", "both"} and edge.source == node_id: + neighbors.append(edge.target) + if direction in {"in", "both"} and edge.target == node_id: + neighbors.append(edge.source) + for neighbor in neighbors: + if neighbor not in distances: + distances[neighbor] = hop + next_frontier.append(neighbor) + frontier = next_frontier + + by_id = graph.node_by_id() + candidates: list[RetrievalCandidate] = [] + for node_id, distance in distances.items(): + node = by_id[node_id] + if phases and node.phase.value not in phases: + continue + if kinds and node.kind not in kinds: + continue + candidates.append(score_candidate(node, distance=distance, explicit_priority=node_id in seed_node_ids)) + return tuple(sorted(candidates, key=lambda item: (-item.score, item.distance, item.node_id))) + + +def score_candidate(node: MemoryNode, *, distance: int, explicit_priority: bool = False, policy_allowed: bool = True) -> RetrievalCandidate: + score = 100 - (distance * 10) + reasons = [f"graph_distance:{distance}"] + if explicit_priority: + score += 50 + reasons.append("explicit_priority") + if node.phase.value in {"stabilized", "rigid"}: + score += 10 + reasons.append(f"phase:{node.phase.value}") + if node.lifecycle.value == "active": + score += 5 + reasons.append("lifecycle:active") + if node.confidence is not None: + score += int(node.confidence * 10) + reasons.append("confidence") + if node.source_spans: + score += 5 + reasons.append("source_backed") + if node.freshness.get("status") == "stale": + score -= 20 + reasons.append("freshness:stale") + if not policy_allowed: + score -= 100 + reasons.append("policy:denied") + return RetrievalCandidate(node.node_id, distance, score, tuple(reasons)) + + +def select_event_path( + events: tuple[MemoryEvent, ...], + path: MemoryPath, + *, + max_events: int, + include_inactive: bool = False, +) -> tuple[str, ...]: + if path.state != MemoryPathState.ACTIVE and not include_inactive: + return () + available = {event.event_id for event in events} + return tuple(event_id for event_id in path.event_ids if event_id in available)[:max_events] + + +def plan_neighborhood_activation( + graph: MemoryGraph, + *, + seed_node_ids: tuple[str, ...], + max_hops: int, + max_items: int, + max_tokens: int, + profile_id: str | None = None, + estimator: TokenEstimator | None = None, +) -> tuple[ActivationPlan, tuple[RetrievalCandidate, ...]]: + estimator = estimator or WordCountTokenEstimator() + candidates = retrieve_graph_neighborhood(graph, seed_node_ids=seed_node_ids, max_hops=max_hops) + priority = tuple(candidate.node_id for candidate in candidates) + plan = plan_activation( + graph, + max_items=max_items, + max_tokens=max_tokens, + profile_id=profile_id, + priority_node_ids=priority, + ) + # Preserve the estimator contract for callers without changing the current + # activation plan wire shape. + plan.selection.setdefault("metadata", {})["estimator"] = estimator.__class__.__name__ + return plan, candidates + + +def activation_quality_report( + plan: ActivationPlan, + *, + expected_node_ids: tuple[str, ...] = (), + policy_denied_node_ids: tuple[str, ...] = (), +) -> dict: + selected = set(plan.selected_node_ids) + omitted_ids = {str(item.get("id")) for item in plan.omitted} + expected = set(expected_node_ids) + selected_expected = sorted(selected & expected) + omitted_required = sorted((expected - selected) & omitted_ids) + selected_metadata = plan.selection.get("metadata", {}).get("selected_items", {}) + return { + "selected_expected_nodes": selected_expected, + "omitted_required_nodes": omitted_required, + "policy_denied_required_nodes": sorted(expected & set(policy_denied_node_ids)), + "token_budget_utilization": round(plan.token_estimate / plan.max_tokens, 4) if plan.max_tokens else 0, + "stale_item_activation_count": sum( + 1 for node_id in plan.selected_node_ids if selected_metadata.get(node_id, {}).get("freshness", {}).get("status") == "stale" + ), + "provenance_coverage": _coverage(plan.selected_node_ids, selected_metadata, "provenance"), + "source_span_coverage": _coverage(plan.selected_node_ids, selected_metadata, "source_spans"), + "explanation_coverage": _coverage(plan.selected_node_ids, selected_metadata, "reason_selected"), + } + + +def _coverage(node_ids: tuple[str, ...], metadata: dict, key: str) -> float: + if not node_ids: + return 0.0 + covered = sum(1 for node_id in node_ids if metadata.get(node_id, {}).get(key)) + return round(covered / len(node_ids), 4) diff --git a/src/phase_memory/runtime.py b/src/phase_memory/runtime.py index d5691bd..919ae8e 100644 --- a/src/phase_memory/runtime.py +++ b/src/phase_memory/runtime.py @@ -15,6 +15,7 @@ from .adapters import ( NoopContextPackageCompiler, RecordingAuditSink, ) +from .bridge import MARKITECT_PACKAGE_REQUEST_SCHEMA, package_request_from_selection, package_response_envelope from .contracts import ContractIngressResult, graph_from_markitect, profile_from_markitect from .lifecycle import plan_compaction, plan_refresh, plan_retention from .models import ( @@ -35,7 +36,7 @@ from .ports import AuditSink, ContextPackageCompiler, MemoryEventLog, MemoryGrap from .utils import compact_dict, stable_digest, to_plain RUNTIME_ENVELOPE_SCHEMA = "phase_memory.runtime.envelope.v1" -PACKAGE_REQUEST_SCHEMA = "phase_memory.package_request.v1" +PACKAGE_REQUEST_SCHEMA = MARKITECT_PACKAGE_REQUEST_SCHEMA @dataclass @@ -208,7 +209,7 @@ class PhaseMemoryRuntime: valid=True, diagnostics=(), source_ref=source_ref, - data={"package_request": request, "package_response": response}, + data={"package_request": request, "package_response": package_response_envelope(response, request_id=request["id"])}, ) def export_graph(self, *, graph_id: str = "local", source_ref: str = "local-store") -> dict[str, Any]: @@ -301,14 +302,7 @@ class PhaseMemoryRuntime: ) def package_request(self, selection: dict[str, Any]) -> dict[str, Any]: - request_id = f"package-request:{stable_digest(selection)}" - return { - "schema_version": PACKAGE_REQUEST_SCHEMA, - "id": request_id, - "selection": dict(selection), - "compiler": self.package_compiler.__class__.__name__, - "dry_run": True, - } + return package_request_from_selection(selection, compiler=self.package_compiler.__class__.__name__) def _contract_envelope( self, diff --git a/tests/fixtures/activation-quality-report.json b/tests/fixtures/activation-quality-report.json new file mode 100644 index 0000000..dcc514f --- /dev/null +++ b/tests/fixtures/activation-quality-report.json @@ -0,0 +1,16 @@ +{ + "explanation_coverage": 1.0, + "omitted_required_nodes": [ + "artifact.profile" + ], + "policy_denied_required_nodes": [ + "artifact.profile" + ], + "provenance_coverage": 0.0, + "selected_expected_nodes": [ + "decision.boundary" + ], + "source_span_coverage": 1.0, + "stale_item_activation_count": 0, + "token_budget_utilization": 0.45 +} diff --git a/tests/fixtures/markitect-invalid-graph.json b/tests/fixtures/markitect-invalid-graph.json new file mode 100644 index 0000000..17ef5df --- /dev/null +++ b/tests/fixtures/markitect-invalid-graph.json @@ -0,0 +1,18 @@ +{ + "schema_version": "markitect.memory.graph.v1", + "id": "invalid-graph", + "nodes": [ + { + "id": "node.a", + "kind": "decision" + } + ], + "edges": [ + { + "id": "edge.bad", + "kind": "depends_on", + "source": "node.a", + "target": "missing" + } + ] +} diff --git a/tests/fixtures/markitect-invalid-profile.json b/tests/fixtures/markitect-invalid-profile.json new file mode 100644 index 0000000..5228277 --- /dev/null +++ b/tests/fixtures/markitect-invalid-profile.json @@ -0,0 +1,4 @@ +{ + "schema_version": "markitect.memory.profile.v99", + "title": "Invalid Profile Without Id" +} diff --git a/tests/fixtures/markitect-package-response.json b/tests/fixtures/markitect-package-response.json new file mode 100644 index 0000000..23bb574 --- /dev/null +++ b/tests/fixtures/markitect-package-response.json @@ -0,0 +1,8 @@ +{ + "package_id": "package:activation-fixture", + "diagnostics": [], + "item_count": 2, + "metadata": { + "compiled_by": "markitect-fixture" + } +} diff --git a/tests/fixtures/runtime-activation-plan-snapshot.json b/tests/fixtures/runtime-activation-plan-snapshot.json index 7e47ae6..07360b3 100644 --- a/tests/fixtures/runtime-activation-plan-snapshot.json +++ b/tests/fixtures/runtime-activation-plan-snapshot.json @@ -5,7 +5,7 @@ "event.restart:max_tokens", "risk.durable-write:max_tokens" ], - "package_request_id": "package-request:854b8a9e0f9a", + "package_request_id": "markitect-package-request:60c0ec4c172b", "plan_id": "activation:6e0ba40234cd", "selected_event_ids": [ "event.activation" @@ -19,7 +19,7 @@ "activation_omitted_items" ], "operation": "graph.activation.plan", - "operation_id": "op:826d3b06fa5b", + "operation_id": "op:85705b61958d", "schema_version": "phase_memory.runtime.envelope.v1", "subject": { "id": "phase-memory-fixture-graph", diff --git a/tests/test_markitect_bridge.py b/tests/test_markitect_bridge.py new file mode 100644 index 0000000..95bab30 --- /dev/null +++ b/tests/test_markitect_bridge.py @@ -0,0 +1,92 @@ +import json +from pathlib import Path + +from phase_memory.bridge import ( + MARKITECT_PACKAGE_REQUEST_SCHEMA, + MARKITECT_PACKAGE_RESPONSE_SCHEMA, + LocalMarkitectValidator, + OptionalMarkitectValidator, + package_response_envelope, +) +from phase_memory.contracts import graph_from_markitect +from phase_memory.runtime import PhaseMemoryRuntime + + +FIXTURES = Path(__file__).parent / "fixtures" + + +def _load(name: str): + return json.loads((FIXTURES / name).read_text(encoding="utf-8")) + + +def test_local_markitect_validator_reports_contract_diagnostics() -> None: + validator = LocalMarkitectValidator() + + invalid_profile = validator.validate_profile(_load("markitect-invalid-profile.json")) + invalid_graph = validator.validate_graph(_load("markitect-invalid-graph.json")) + + assert not invalid_profile.valid + assert [diagnostic.code for diagnostic in invalid_profile.diagnostics] == ["unsupported_profile_schema", "missing_profile_id"] + assert not invalid_graph.valid + assert [diagnostic.code for diagnostic in invalid_graph.diagnostics] == ["missing_edge_target"] + + +def test_optional_validator_falls_back_to_local_boundary() -> None: + validator = OptionalMarkitectValidator() + + result = validator.validate_selection({"schema_version": "markitect.memory.selection.v1", "id": "selection.a"}) + + assert result.valid + assert result.subject_kind == "memory_selection" + + +def test_activation_preserves_metadata_for_package_request() -> None: + graph_data = _load("memory-graph.json") + graph_data["nodes"][0]["confidence"] = 0.95 + graph_data["nodes"][0]["policy"] = {"labels": ["project-local"], "trust_zone": "local"} + graph_data["nodes"][0]["provenance"] = [{"source": "architecture"}] + graph = graph_from_markitect(graph_data).value + runtime = PhaseMemoryRuntime() + + envelope = runtime.plan_activation( + graph.to_dict(), + max_items=1, + max_tokens=20, + profile_id="phase-memory-fixture-profile", + priority_node_ids=("decision.boundary",), + ) + + request = envelope["data"]["package_request"] + item = request["provenance"]["selected_items"]["decision.boundary"] + assert request["schema_version"] == MARKITECT_PACKAGE_REQUEST_SCHEMA + assert request["selection_id"] == envelope["data"]["activation_plan"]["plan_id"] + assert request["graph_id"] == "phase-memory-fixture-graph" + assert request["profile_id"] == "phase-memory-fixture-profile" + assert request["selected_nodes"] == ["decision.boundary"] + assert item["source_spans"] == [{"path": "docs/architecture.md", "line_start": 1}] + assert item["provenance"] == [{"source": "architecture"}] + assert item["confidence"] == 0.95 + assert item["policy"]["labels"] == ["project-local"] + assert item["reason_selected"] == "priority" + + +def test_package_response_envelope_keeps_markitect_response_opaque() -> None: + response = _load("markitect-package-response.json") + + envelope = package_response_envelope(response, request_id="request.a") + + assert envelope["schema_version"] == MARKITECT_PACKAGE_RESPONSE_SCHEMA + assert envelope["request_id"] == "request.a" + assert envelope["package_ref"] == "package:activation-fixture" + assert envelope["response"] == response + + +def test_runtime_compile_package_uses_bridge_response_envelope() -> None: + runtime = PhaseMemoryRuntime() + selection = {"schema_version": "markitect.memory.selection.v1", "id": "selection.a", "nodes": ["node.a"], "events": []} + + envelope = runtime.compile_package(selection) + + assert envelope["data"]["package_request"]["schema_version"] == MARKITECT_PACKAGE_REQUEST_SCHEMA + assert envelope["data"]["package_response"]["schema_version"] == MARKITECT_PACKAGE_RESPONSE_SCHEMA + assert envelope["data"]["package_response"]["package_ref"] == "package:selection.a" diff --git a/tests/test_retrieval_quality.py b/tests/test_retrieval_quality.py new file mode 100644 index 0000000..cb1abf4 --- /dev/null +++ b/tests/test_retrieval_quality.py @@ -0,0 +1,95 @@ +import json +from pathlib import Path + +from phase_memory.contracts import graph_from_markitect +from phase_memory.models import MemoryEvent, MemoryPath, MemoryPathState +from phase_memory.retrieval import ( + WordCountTokenEstimator, + activation_quality_report, + plan_neighborhood_activation, + retrieve_graph_neighborhood, + select_event_path, +) + + +FIXTURES = Path(__file__).parent / "fixtures" + + +def _graph(): + return graph_from_markitect(json.loads((FIXTURES / "memory-graph.json").read_text(encoding="utf-8"))).value + + +def test_retrieve_graph_neighborhood_is_stable_and_filterable() -> None: + candidates = retrieve_graph_neighborhood( + _graph(), + seed_node_ids=("decision.boundary",), + max_hops=1, + edge_kinds=("governs",), + direction="out", + ) + + assert [candidate.node_id for candidate in candidates] == ["decision.boundary", "artifact.profile"] + assert candidates[0].reasons[:2] == ("graph_distance:0", "explicit_priority") + + +def test_event_path_selection_respects_path_state_and_budget() -> None: + events = ( + MemoryEvent("event.a", "user_turn"), + MemoryEvent("event.b", "agent_turn"), + MemoryEvent("event.c", "tool_call"), + ) + active = MemoryPath("path.active", event_ids=("event.a", "event.b", "event.c")) + abandoned = MemoryPath("path.abandoned", event_ids=("event.a",), state=MemoryPathState.ABANDONED) + + assert select_event_path(events, active, max_events=2) == ("event.a", "event.b") + assert select_event_path(events, abandoned, max_events=2) == () + assert select_event_path(events, abandoned, max_events=2, include_inactive=True) == ("event.a",) + + +def test_neighborhood_activation_uses_retrieval_order_and_estimator_label() -> None: + plan, candidates = plan_neighborhood_activation( + _graph(), + seed_node_ids=("decision.boundary",), + max_hops=1, + max_items=2, + max_tokens=40, + profile_id="phase-memory-fixture-profile", + ) + + assert [candidate.node_id for candidate in candidates][:2] == ["decision.boundary", "artifact.profile"] + assert plan.selected_node_ids[:2] == ("decision.boundary", "artifact.profile") + assert plan.selection["metadata"]["estimator"] == "WordCountTokenEstimator" + + +def test_token_estimator_accounts_for_nodes_and_events() -> None: + graph = _graph() + estimator = WordCountTokenEstimator() + + assert estimator.estimate_node(graph.nodes[0]) == 9 + assert estimator.estimate_event(graph.events[0]) >= 2 + + +def test_activation_quality_report_is_deterministic() -> None: + plan, _ = plan_neighborhood_activation( + _graph(), + seed_node_ids=("decision.boundary",), + max_hops=1, + max_items=1, + max_tokens=20, + profile_id="phase-memory-fixture-profile", + ) + + report = activation_quality_report( + plan, + expected_node_ids=("decision.boundary", "artifact.profile"), + policy_denied_node_ids=("artifact.profile",), + ) + + expected = json.loads((FIXTURES / "activation-quality-report.json").read_text(encoding="utf-8")) + assert report["selected_expected_nodes"] == ["decision.boundary"] + assert report["omitted_required_nodes"] == ["artifact.profile"] + assert report["policy_denied_required_nodes"] == ["artifact.profile"] + assert report["token_budget_utilization"] == 0.45 + assert report["source_span_coverage"] == 1.0 + assert report["explanation_coverage"] == 1.0 + assert report == expected diff --git a/tests/test_runtime.py b/tests/test_runtime.py index da5e356..791df57 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -79,4 +79,5 @@ def test_runtime_compile_package_wraps_compiler_response() -> None: assert envelope["operation"] == "package.compile" assert envelope["data"]["package_request"]["selection"] == selection - assert envelope["data"]["package_response"]["package_id"] == "package:selection.a" + assert envelope["data"]["package_response"]["package_ref"] == "package:selection.a" + assert envelope["data"]["package_response"]["response"]["package_id"] == "package:selection.a" diff --git a/workplans/PMEM-MATURITY-SCORECARD.md b/workplans/PMEM-MATURITY-SCORECARD.md index 6bb765e..ec5f00e 100644 --- a/workplans/PMEM-MATURITY-SCORECARD.md +++ b/workplans/PMEM-MATURITY-SCORECARD.md @@ -44,30 +44,30 @@ not what adjacent repositories may already provide. ## Current Baseline - 2026-05-18 -Overall maturity: **3.1 / 5** +Overall maturity: **3.7 / 5** The repo has crossed from intent-only into a working deterministic library foundation, a usable local runtime facade, a CLI, a file-backed local -workspace, and first-slice policy/review/audit gates. It is not yet an -interop-complete runtime because richer Markitect package bridge, activation -quality, and service contracts remain ahead. +workspace, first-slice policy/review/audit gates, and a concrete Markitect +package bridge, and deterministic activation quality helpers. It is not yet +service-ready. | Dimension | Current | Target | Evidence | Needed Next | | --- | ---: | ---: | --- | --- | | Intent and boundaries | 4.0 | 5.0 | `INTENT.md`, `SCOPE.md`, `README.md`, architecture doc, PMEM-WP-0001 closure | Keep boundaries current as runtime behavior expands. | -| Package foundation | 3.0 | 4.0 | Python package, exports, runtime facade, CLI entrypoint, dependency-light tests | Add local persistence and richer adapter configuration. | -| Profile contract ingress | 2.5 | 4.0 | Markitect-compatible profile loading, diagnostics, runtime envelopes | Add validation adapter boundary and compatibility fixture catalog. | -| Graph/event contract ingress | 3.0 | 4.0 | Graph loading, edge endpoint diagnostics, event model, JSONL event log, export, repair diagnostics | Add richer policy-aware import/export checks. | -| Phase domain model | 3.0 | 4.0 | Phases, memory kinds, lifecycle states, actions, explicit path records | Add transition rule profiles and review records. | -| Profile execution planning | 3.0 | 4.0 | Adapter plan, capabilities, policy gates, fallback behavior, CLI output, snapshot fixture | Add profile-driven runtime configuration and compatibility validation. | -| Lifecycle planning | 3.0 | 4.0 | Transition, retention, refresh, compaction dry-run plans, review-gated local apply | Add profile-driven rule evaluation and full review records. | -| Activation planning | 2.5 | 5.0 | Budgeted selection, Markitect-compatible selection output, package request envelope, CLI output | Add graph neighborhoods, event paths, ranking, metadata preservation, metrics. | +| Package foundation | 3.0 | 4.0 | Python package, exports, runtime facade, CLI entrypoint, dependency-light tests | Add runtime configuration, service contracts, and adapter conformance. | +| Profile contract ingress | 2.5 | 4.0 | Markitect-compatible profile loading, diagnostics, runtime envelopes | Add profile-driven runtime configuration and richer compatibility coverage. | +| Graph/event contract ingress | 3.0 | 4.0 | Graph loading, edge endpoint diagnostics, event model, JSONL event log, export, repair diagnostics | Add service-level import/export contracts and adapter conformance. | +| Phase domain model | 3.0 | 4.0 | Phases, memory kinds, lifecycle states, actions, explicit path records | Add profile-driven transition rule evaluation and migration semantics. | +| Profile execution planning | 3.0 | 4.0 | Adapter plan, capabilities, policy gates, fallback behavior, CLI output, snapshot fixture | Add runtime configuration model and service-readiness diagnostics. | +| Lifecycle planning | 3.0 | 4.0 | Transition, retention, refresh, compaction dry-run plans, review-gated local apply | Add profile-driven rule evaluation and service apply contracts. | +| Activation planning | 3.8 | 5.0 | Budgeted selection, Markitect-compatible selection output, package request envelope, graph neighborhoods, event paths, ranking, metadata preservation, metrics | Add semantic-index adapters and broader evaluation corpora. | | Local persistence | 3.0 | 4.0 | Versioned local workspace, file-backed graph store, JSONL event log, JSONL audit sink | Add migration/repair utilities and stronger durability semantics. | | Policy and audit | 3.2 | 5.0 | Operation points, policy gateway checks, audit schema, review records, redaction, activation denials | Add external policy adapters and richer audit retention behavior. | | Observability and diagnostics | 2.5 | 4.0 | Planner diagnostics, runtime diagnostics, event log corruption checks, repair diagnostics, policy denial diagnostics | Add health envelopes and adapter status diagnostics. | -| Markitect interop | 1.5 | 4.0 | Compatible schema constants and selection handoff | Add package bridge envelopes, optional validation/compiler adapters. | -| Kontextual/Infospace interop | 1.0 | 4.0 | Boundaries documented and small derived fixtures | Add delegation envelope design and evaluation fixture reports. | -| Testing and evaluation | 3.2 | 4.0 | 36 deterministic tests over planners, adapters, runtime envelopes, CLI, snapshots, file-store round trips, apply denial, review records, audit schema, and policy redaction | Add activation metrics. | +| Markitect interop | 3.5 | 4.0 | Compatible contract ingress, optional validation boundary, enriched selection metadata, package request/response envelopes | Add live optional Markitect compiler adapter when available. | +| Kontextual/Infospace interop | 1.5 | 4.0 | Boundaries documented, small derived fixtures, activation quality report fixture | Add Kontextual delegation envelopes and broader Infospace evaluation fixture reports. | +| Testing and evaluation | 3.7 | 4.0 | 46 deterministic tests over planners, adapters, runtime envelopes, CLI, snapshots, file-store round trips, apply denial, review records, audit schema, policy redaction, Markitect bridge fixtures, retrieval, and activation metrics | Add broader evaluation corpora. | | Service readiness | 0.5 | 4.0 | Runtime ports exist | Add service contracts, config, health checks, adapter conformance tests. | | Developer experience | 3.3 | 4.0 | README quick start, package map, runtime facade docs, CLI examples, local persistence guide | Add troubleshooting and richer examples. | @@ -113,6 +113,26 @@ Remaining maturity blockers: - Activation quality metrics. - Service readiness and external adapter conformance. +## Progress Update - PMEM-WP-0006 + +Closed on 2026-05-18: + +- Added deterministic graph-neighborhood retrieval. +- Added event-path selection from structured memory paths. +- Added candidate scoring and explanation reasons. +- Added pluggable token estimator protocol with deterministic word-count + default. +- Added activation quality report metrics and fixture. +- Documented retrieval and evaluation behavior. + +Remaining maturity blockers: + +- Service API contracts. +- Runtime configuration model. +- Health diagnostics. +- External adapter conformance tests. +- Kontextual delegation adapter design. + ## Progress Update - PMEM-WP-0004 Closed on 2026-05-18: @@ -133,6 +153,25 @@ Remaining maturity blockers: - Activation ranking and evaluation metrics. - Service contracts, health diagnostics, and external adapter conformance. +## Progress Update - PMEM-WP-0005 + +Closed on 2026-05-18: + +- Added Markitect package request and response envelopes. +- Added local and optional Markitect validation adapter boundary. +- Preserved selected item source spans, provenance, confidence, freshness, + namespace, policy metadata, and selection reasons through activation. +- Added compatibility fixtures and tests for invalid contracts, activation + package requests, and opaque package responses. +- Documented the Markitect boundary. + +Remaining maturity blockers: + +- Graph-neighborhood retrieval and event-path activation. +- Ranking signals and token accounting. +- Activation quality metrics and evaluation fixtures. +- Service readiness and external adapter conformance. + ## Score Movement Rules A dimension should move up only when executable behavior and tests exist. diff --git a/workplans/PMEM-WP-0005-markitect-package-bridge-and-contract-interop.md b/workplans/PMEM-WP-0005-markitect-package-bridge-and-contract-interop.md index f3b9668..634e9fa 100644 --- a/workplans/PMEM-WP-0005-markitect-package-bridge-and-contract-interop.md +++ b/workplans/PMEM-WP-0005-markitect-package-bridge-and-contract-interop.md @@ -4,7 +4,7 @@ type: workplan title: "Markitect Package Bridge And Contract Interop" domain: markitect repo: phase-memory -status: proposed +status: finished owner: phase-memory topic_slug: markitect-interop planning_priority: P1 @@ -49,11 +49,37 @@ delegation, and stable package request/response envelopes. - Do not require Markitect installation for the default test suite. - Do not turn Markitect into a hidden import dependency. +## Implementation Update - 2026-05-18 + +The Markitect bridge and contract interop slice is complete. + +Implemented outputs: + +- `phase_memory.bridge` defines package request and response envelopes, + dependency-light local validation, and an optional Markitect validator + adapter shell. +- Activation selections now preserve selected-item source spans, provenance, + confidence, freshness, namespace, policy metadata, and reason selected. +- Runtime package requests use + `phase_memory.markitect.package_request.v1` and keep selected node/event, + budget, policy, provenance, compiler, and selection data explicit. +- Package compiler responses are wrapped as + `phase_memory.markitect.package_response.v1` while keeping Markitect internals + opaque. +- Interop fixtures cover valid profiles/graphs, invalid profiles/graphs, + activation package request snapshots, and opaque package responses. +- `docs/markitect-interop.md` documents ownership, validation boundaries, + package request/response contracts, and fixture catalog. + +Validation: + +- `python3 -m pytest` -> 41 passed. + ## T01 - Define compiler bridge envelopes ```task id: PMEM-WP-0005-T01 -status: todo +status: done priority: high state_hub_task_id: "e51c4804-4938-443b-b02f-afa7bac0b846" ``` @@ -77,7 +103,7 @@ Output: typed helpers and JSON fixtures for package requests and responses. ```task id: PMEM-WP-0005-T02 -status: todo +status: done priority: high state_hub_task_id: "5a1a8777-b971-4f1b-bf65-bd71918eabf6" ``` @@ -93,7 +119,7 @@ adapter design, and tests around fallback behavior. ```task id: PMEM-WP-0005-T03 -status: todo +status: done priority: high state_hub_task_id: "012210c6-fc05-467e-9d36-7358c0e11abd" ``` @@ -116,7 +142,7 @@ to package request. ```task id: PMEM-WP-0005-T04 -status: todo +status: done priority: medium state_hub_task_id: "90ebf80e-1f7e-422c-879c-f4270f1e232e" ``` @@ -139,7 +165,7 @@ examples. ```task id: PMEM-WP-0005-T05 -status: todo +status: done priority: medium state_hub_task_id: "95b07795-6d8c-4473-a98a-5e48a3e6cca9" ``` @@ -158,7 +184,7 @@ Output: compatibility test suite. ```task id: PMEM-WP-0005-T06 -status: todo +status: done priority: medium state_hub_task_id: "21e4ffb5-e8dc-4b32-9008-97fb6ffb3726" ``` @@ -182,3 +208,25 @@ Output: interop architecture note. the core planner APIs. - Source spans, provenance, freshness, confidence, and policy metadata survive from graph ingress to package request. + +## Closure Review - 2026-05-18 + +**Outcome:** All tasks completed. + +### Completed + +- PMEM-WP-0005-T01 - Define compiler bridge envelopes +- PMEM-WP-0005-T02 - Add Markitect validation adapter boundary +- PMEM-WP-0005-T03 - Preserve provenance and source spans through activation +- PMEM-WP-0005-T04 - Add interop fixture catalog +- PMEM-WP-0005-T05 - Add contract compatibility tests +- PMEM-WP-0005-T06 - Document the Markitect boundary + +### Cancelled + +None. + +### Carried Forward + +Graph-neighborhood retrieval, event-path activation, ranking, and activation +quality metrics remain in PMEM-WP-0006. diff --git a/workplans/PMEM-WP-0006-retrieval-activation-quality-and-evaluation.md b/workplans/PMEM-WP-0006-retrieval-activation-quality-and-evaluation.md index 809c45e..a7d278e 100644 --- a/workplans/PMEM-WP-0006-retrieval-activation-quality-and-evaluation.md +++ b/workplans/PMEM-WP-0006-retrieval-activation-quality-and-evaluation.md @@ -4,7 +4,7 @@ type: workplan title: "Retrieval, Activation Quality, And Evaluation" domain: markitect repo: phase-memory -status: proposed +status: finished owner: phase-memory topic_slug: activation-quality planning_priority: P2 @@ -51,11 +51,37 @@ activation-memory intent. - Do not own benchmark dashboards that belong in `infospace-bench`. - Do not optimize for a single application domain. +## Implementation Update - 2026-05-18 + +The retrieval, activation quality, and evaluation slice is complete. + +Implemented outputs: + +- `phase_memory.retrieval` adds deterministic graph-neighborhood retrieval, + candidate scoring, event-path selection, pluggable token estimator protocol, + neighborhood activation planning, and activation quality reports. +- Retrieval supports max hops, edge-kind filters, direction filters, phase + filters, and memory kind filters. +- Event-path activation selects bounded event windows from structured + `MemoryPath` records and treats inactive paths as opt-in. +- Ranking signals include explicit priority, graph distance, phase, lifecycle + state, confidence, source-backed status, freshness, and policy allowance. +- `WordCountTokenEstimator` provides deterministic local budget accounting. +- `activation_quality_report` emits selected expected nodes, omitted required + nodes, policy-denied required nodes, token budget utilization, stale item + count, provenance coverage, source span coverage, and explanation coverage. +- `docs/activation-quality.md` documents retrieval, event paths, scoring, + estimator boundaries, and evaluation metrics. + +Validation: + +- `python3 -m pytest` -> 46 passed. + ## T01 - Add deterministic graph-neighborhood retrieval ```task id: PMEM-WP-0006-T01 -status: todo +status: done priority: high state_hub_task_id: "8ed0909f-9e8e-4d49-9312-dca267df29f5" ``` @@ -75,7 +101,7 @@ Output: retrieval planner and tests for stable graph-neighborhood selection. ```task id: PMEM-WP-0006-T02 -status: todo +status: done priority: high state_hub_task_id: "5d48ba91-fef0-4d4f-a560-836abed1c527" ``` @@ -90,7 +116,7 @@ active, abandoned, and merged paths. ```task id: PMEM-WP-0006-T03 -status: todo +status: done priority: high state_hub_task_id: "0f6340ef-f7bd-408b-b98e-6d90188c5969" ``` @@ -113,7 +139,7 @@ Output: scoring model, per-item selection reason, and omitted-item reason. ```task id: PMEM-WP-0006-T04 -status: todo +status: done priority: medium state_hub_task_id: "12d83382-a767-45a8-b7cc-8c3f6f3f4c37" ``` @@ -128,7 +154,7 @@ package budget pressure. ```task id: PMEM-WP-0006-T05 -status: todo +status: done priority: medium state_hub_task_id: "509e9417-3aa7-4899-aed5-20749372fe00" ``` @@ -148,7 +174,7 @@ Output: fixture set and expected activation plans. ```task id: PMEM-WP-0006-T06 -status: todo +status: done priority: medium state_hub_task_id: "477a896a-8013-42a5-b965-b1ccd2577fec" ``` @@ -170,7 +196,7 @@ Output: metrics helper and JSON report fixture. ```task id: PMEM-WP-0006-T07 -status: todo +status: done priority: medium state_hub_task_id: "551432e4-2551-49fa-b17b-f762853a6a50" ``` @@ -187,3 +213,26 @@ Output: activation planning guide and scorecard update. - Every selected and omitted item has a machine-readable reason. - Evaluation fixtures produce deterministic activation quality reports. - Optional semantic indexes remain behind the `SemanticIndex` port. + +## Closure Review - 2026-05-18 + +**Outcome:** All tasks completed. + +### Completed + +- PMEM-WP-0006-T01 - Add deterministic graph-neighborhood retrieval +- PMEM-WP-0006-T02 - Add event-path activation +- PMEM-WP-0006-T03 - Add ranking signals and explanations +- PMEM-WP-0006-T04 - Improve token and budget accounting +- PMEM-WP-0006-T05 - Add evaluation fixture scenarios +- PMEM-WP-0006-T06 - Add maturity metrics for activation quality +- PMEM-WP-0006-T07 - Document retrieval and evaluation behavior + +### Cancelled + +None. + +### Carried Forward + +Service contracts, runtime configuration, health diagnostics, and external +adapter conformance remain in PMEM-WP-0007.