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

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