Add Markitect bridge and activation quality

This commit is contained in:
2026-05-18 20:04:40 +02:00
parent 8089a7c8fa
commit 1efb7d4c13
19 changed files with 871 additions and 43 deletions

View File

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

View File

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

93
docs/markitect-interop.md Normal file
View File

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

View File

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

View File

@@ -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,
},
}

View File

@@ -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),
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
{
"schema_version": "markitect.memory.profile.v99",
"title": "Invalid Profile Without Id"
}

View File

@@ -0,0 +1,8 @@
{
"package_id": "package:activation-fixture",
"diagnostics": [],
"item_count": 2,
"metadata": {
"compiled_by": "markitect-fixture"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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