generated from coulomb/repo-seed
Implement phase-memory foundation
This commit is contained in:
56
tests/fixtures/memory-graph.json
vendored
Normal file
56
tests/fixtures/memory-graph.json
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"schema_version": "markitect.memory.graph.v1",
|
||||
"id": "phase-memory-fixture-graph",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "decision.boundary",
|
||||
"kind": "decision",
|
||||
"text": "Markitect owns syntax contracts; phase-memory owns runtime phase planning.",
|
||||
"phase": "stabilized",
|
||||
"source_spans": [{"path": "docs/architecture.md", "line_start": 1}],
|
||||
"metadata": {"title": "Boundary decision"}
|
||||
},
|
||||
{
|
||||
"id": "event.restart",
|
||||
"kind": "episode",
|
||||
"text": "Restart package should include boundary decision and active graph neighborhood.",
|
||||
"phase": "fluid",
|
||||
"freshness": {"updated_at": "2026-05-01T00:00:00+00:00", "source_digest": "old"}
|
||||
},
|
||||
{
|
||||
"id": "artifact.profile",
|
||||
"kind": "artifact",
|
||||
"text": "Memory profile declares budgets, stores, retention, activation, policy, and fallback behavior.",
|
||||
"phase": "stabilized",
|
||||
"freshness": {"updated_at": "2026-05-18T00:00:00+00:00", "source_digest": "fresh"}
|
||||
},
|
||||
{
|
||||
"id": "risk.durable-write",
|
||||
"kind": "risk",
|
||||
"text": "Durable writes must stay review gated until the runtime plan is explicit.",
|
||||
"phase": "fluid"
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge.boundary-profile",
|
||||
"kind": "governs",
|
||||
"source": "decision.boundary",
|
||||
"target": "artifact.profile"
|
||||
},
|
||||
{
|
||||
"id": "edge.risk-boundary",
|
||||
"kind": "depends_on",
|
||||
"source": "risk.durable-write",
|
||||
"target": "decision.boundary"
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"id": "event.activation",
|
||||
"kind": "activated",
|
||||
"timestamp": "2026-05-18T00:00:00+00:00",
|
||||
"activation_refs": ["activation.fixture"]
|
||||
}
|
||||
]
|
||||
}
|
||||
45
tests/fixtures/memory-profile.json
vendored
Normal file
45
tests/fixtures/memory-profile.json
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"schema_version": "markitect.memory.profile.v1",
|
||||
"id": "phase-memory-fixture-profile",
|
||||
"title": "Phase Memory Fixture Profile",
|
||||
"intent": "Plan reasoning, conversation, knowledge, and activation memory behavior.",
|
||||
"memory_kinds": ["reasoning", "conversation", "knowledge", "package"],
|
||||
"stores": {
|
||||
"reasoning": "local-graph-store",
|
||||
"conversation": "local-event-log",
|
||||
"knowledge": "local-graph-store",
|
||||
"package": "markitect-context-package"
|
||||
},
|
||||
"limits": {
|
||||
"reasoning": {"max_nodes": 40},
|
||||
"conversation": {"max_nodes": 20},
|
||||
"package": {"max_items": 3}
|
||||
},
|
||||
"retention": {
|
||||
"conversation": {"stale_after_days": 7, "delete_after_days": 30}
|
||||
},
|
||||
"refresh": {
|
||||
"trigger": "source-artifact-or-profile-digest-change"
|
||||
},
|
||||
"compaction": {
|
||||
"strategy": "summarize-trace-after-review"
|
||||
},
|
||||
"activation": {
|
||||
"max_items": 3,
|
||||
"max_tokens": 30
|
||||
},
|
||||
"policy": {
|
||||
"required_labels": ["project-local"],
|
||||
"durable_writes": "review-gated",
|
||||
"secrets_allowed": false
|
||||
},
|
||||
"observability": {
|
||||
"emit_events": true
|
||||
},
|
||||
"failure": {
|
||||
"missing_runtime_store": "degrade-to-dry-run"
|
||||
},
|
||||
"metadata": {
|
||||
"derived_from": ["MKTT-WP-0016", "IB-WP-0017"]
|
||||
}
|
||||
}
|
||||
36
tests/test_activation.py
Normal file
36
tests/test_activation.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from phase_memory.activation import activation_action, plan_activation
|
||||
from phase_memory.contracts import graph_from_markitect
|
||||
from phase_memory.models import LifecycleActionKind
|
||||
|
||||
|
||||
def test_activation_planner_emits_markitect_selection_under_budget() -> None:
|
||||
graph_data = json.loads((Path(__file__).parent / "fixtures" / "memory-graph.json").read_text(encoding="utf-8"))
|
||||
graph = graph_from_markitect(graph_data).value
|
||||
|
||||
plan = plan_activation(
|
||||
graph,
|
||||
max_items=2,
|
||||
max_tokens=18,
|
||||
profile_id="phase-memory-fixture-profile",
|
||||
priority_node_ids=("decision.boundary",),
|
||||
)
|
||||
|
||||
assert plan.selected_node_ids[0] == "decision.boundary"
|
||||
assert len(plan.selected_node_ids) <= 2
|
||||
assert plan.token_estimate <= 18
|
||||
assert plan.selection["schema_version"] == "markitect.memory.selection.v1"
|
||||
assert plan.selection["profile"] == "phase-memory-fixture-profile"
|
||||
assert plan.omitted
|
||||
|
||||
|
||||
def test_activation_action_wraps_selection_for_context_package_boundary() -> None:
|
||||
graph = graph_from_markitect(json.loads((Path(__file__).parent / "fixtures" / "memory-graph.json").read_text(encoding="utf-8"))).value
|
||||
plan = plan_activation(graph, max_items=1, max_tokens=20)
|
||||
|
||||
action = activation_action(plan)
|
||||
|
||||
assert action.action == LifecycleActionKind.ACTIVATE
|
||||
assert action.metadata["selection"]["id"] == plan.plan_id
|
||||
44
tests/test_adapters.py
Normal file
44
tests/test_adapters.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from phase_memory.adapters import (
|
||||
AllowAllPolicyGateway,
|
||||
InMemoryMemoryEventLog,
|
||||
InMemoryMemoryGraphStore,
|
||||
NoopContextPackageCompiler,
|
||||
RecordingAuditSink,
|
||||
)
|
||||
from phase_memory.models import MemoryEdge, MemoryEvent, MemoryNode, ProfileIntent
|
||||
from phase_memory.ports import graph_from_store
|
||||
|
||||
|
||||
def test_in_memory_store_and_event_log_are_deterministic() -> None:
|
||||
store = InMemoryMemoryGraphStore()
|
||||
profile = ProfileIntent(profile_id="profile")
|
||||
node = MemoryNode("node.a", "decision", "Boundary decision")
|
||||
edge = MemoryEdge("edge.a", "governs", "node.a", "node.a")
|
||||
|
||||
store.save_profile(profile)
|
||||
store.save_node(node)
|
||||
store.save_edge(edge)
|
||||
|
||||
assert store.get_profile("profile") == profile
|
||||
assert store.list_nodes() == [node]
|
||||
assert store.list_edges(source="node.a") == [edge]
|
||||
assert graph_from_store(store).nodes == (node,)
|
||||
|
||||
log = InMemoryMemoryEventLog()
|
||||
event = MemoryEvent("event.a", "recorded")
|
||||
assert log.append(event) == event
|
||||
assert log.list_events(kind="recorded") == [event]
|
||||
|
||||
|
||||
def test_local_policy_audit_and_compiler_adapters() -> None:
|
||||
policy = AllowAllPolicyGateway()
|
||||
audit = RecordingAuditSink()
|
||||
compiler = NoopContextPackageCompiler()
|
||||
|
||||
decision = policy.authorize(action="read", resource="memory-node:node.a")
|
||||
receipt = audit.record({"operation": "read", "allowed": decision.allowed})
|
||||
package = compiler.compile_selection({"id": "selection.a", "nodes": ["node.a"], "events": []})
|
||||
|
||||
assert decision.allowed
|
||||
assert receipt["recorded"] is True
|
||||
assert package["package_id"] == "package:selection.a"
|
||||
44
tests/test_contracts.py
Normal file
44
tests/test_contracts.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from phase_memory.contracts import graph_from_markitect, profile_from_markitect
|
||||
|
||||
|
||||
FIXTURES = Path(__file__).parent / "fixtures"
|
||||
|
||||
|
||||
def test_profile_ingress_preserves_markitect_profile_intent() -> None:
|
||||
data = json.loads((FIXTURES / "memory-profile.json").read_text(encoding="utf-8"))
|
||||
|
||||
result = profile_from_markitect(data)
|
||||
|
||||
assert result.valid, [diagnostic.to_dict() for diagnostic in result.diagnostics]
|
||||
assert result.value.profile_id == "phase-memory-fixture-profile"
|
||||
assert result.value.memory_kinds == ("reasoning", "conversation", "knowledge", "package")
|
||||
assert result.value.policy["durable_writes"] == "review-gated"
|
||||
|
||||
|
||||
def test_graph_ingress_checks_edge_integrity_without_owning_vocabulary() -> None:
|
||||
data = json.loads((FIXTURES / "memory-graph.json").read_text(encoding="utf-8"))
|
||||
|
||||
result = graph_from_markitect(data)
|
||||
|
||||
assert result.valid, [diagnostic.to_dict() for diagnostic in result.diagnostics]
|
||||
assert result.value.graph_id == "phase-memory-fixture-graph"
|
||||
assert len(result.value.nodes) == 4
|
||||
assert result.value.node_ids == {
|
||||
"decision.boundary",
|
||||
"event.restart",
|
||||
"artifact.profile",
|
||||
"risk.durable-write",
|
||||
}
|
||||
|
||||
|
||||
def test_graph_ingress_rejects_missing_edge_endpoint() -> None:
|
||||
data = json.loads((FIXTURES / "memory-graph.json").read_text(encoding="utf-8"))
|
||||
data["edges"][0]["target"] = "missing"
|
||||
|
||||
result = graph_from_markitect(data)
|
||||
|
||||
assert not result.valid
|
||||
assert any(diagnostic.code == "missing_edge_target" for diagnostic in result.diagnostics)
|
||||
49
tests/test_lifecycle.py
Normal file
49
tests/test_lifecycle.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from phase_memory.lifecycle import plan_compaction, plan_phase_transition, plan_refresh, plan_retention
|
||||
from phase_memory.models import LifecycleActionKind, LifecycleState, MemoryNode, MemoryPhase
|
||||
|
||||
|
||||
def test_phase_transition_to_stabilized_requires_review() -> None:
|
||||
node = MemoryNode("event.restart", "episode", "Useful restart trace", phase=MemoryPhase.FLUID)
|
||||
|
||||
action = plan_phase_transition(node, MemoryPhase.STABILIZED)
|
||||
|
||||
assert action.action == LifecycleActionKind.TRANSITION_PHASE
|
||||
assert action.requires_review
|
||||
assert action.to_state == LifecycleState.REVIEW_NEEDED
|
||||
assert action.metadata["to_phase"] == "stabilized"
|
||||
|
||||
|
||||
def test_retention_plans_stale_and_delete_requested_without_physical_delete() -> None:
|
||||
now = datetime(2026, 5, 18, tzinfo=timezone.utc)
|
||||
stale = MemoryNode(
|
||||
"stale",
|
||||
"episode",
|
||||
freshness={"updated_at": "2026-05-01T00:00:00+00:00"},
|
||||
)
|
||||
old = MemoryNode(
|
||||
"old",
|
||||
"episode",
|
||||
freshness={"updated_at": "2026-04-01T00:00:00+00:00"},
|
||||
)
|
||||
|
||||
actions = plan_retention([stale, old], stale_after_days=7, delete_after_days=30, now=now)
|
||||
|
||||
by_target = {action.target_id: action for action in actions}
|
||||
assert by_target["stale"].action == LifecycleActionKind.MARK_STALE
|
||||
assert by_target["old"].action == LifecycleActionKind.REQUEST_DELETE
|
||||
assert by_target["old"].metadata["physical_delete"] is False
|
||||
|
||||
|
||||
def test_compaction_and_refresh_are_reviewable_plans() -> None:
|
||||
node = MemoryNode("artifact.profile", "artifact", "Profile text", freshness={"source_digest": "old"})
|
||||
|
||||
compact = plan_compaction([node])
|
||||
refresh = plan_refresh([node], source_digest_by_node_id={"artifact.profile": "new"})[0]
|
||||
|
||||
assert compact.action == LifecycleActionKind.COMPACT
|
||||
assert compact.requires_review
|
||||
assert refresh.action == LifecycleActionKind.REFRESH
|
||||
assert refresh.requires_review
|
||||
assert refresh.metadata["proposed_digest"] == "new"
|
||||
32
tests/test_planner.py
Normal file
32
tests/test_planner.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from phase_memory.contracts import profile_from_markitect
|
||||
from phase_memory.planner import plan_profile_execution
|
||||
|
||||
|
||||
def test_profile_execution_plan_reports_capabilities_and_gates() -> None:
|
||||
profile_data = json.loads((Path(__file__).parent / "fixtures" / "memory-profile.json").read_text(encoding="utf-8"))
|
||||
profile = profile_from_markitect(profile_data).value
|
||||
|
||||
plan = plan_profile_execution(profile)
|
||||
|
||||
assert plan.ready
|
||||
assert "activation.plan" in plan.capabilities
|
||||
assert "retention.plan" in plan.capabilities
|
||||
assert "durable_writes:review-gated" in plan.policy_gates
|
||||
assert "secrets:denied" in plan.policy_gates
|
||||
assert "phase_memory.profile.planned" in plan.observability_events
|
||||
|
||||
|
||||
def test_profile_execution_plan_degrades_when_adapter_is_missing() -> None:
|
||||
profile_data = json.loads((Path(__file__).parent / "fixtures" / "memory-profile.json").read_text(encoding="utf-8"))
|
||||
profile_data["stores"]["knowledge"] = "external-graph-store"
|
||||
profile = profile_from_markitect(profile_data).value
|
||||
|
||||
plan = plan_profile_execution(profile, available_adapters={"local-event-log"})
|
||||
|
||||
assert not plan.ready
|
||||
assert "external-graph-store" in plan.missing_adapters
|
||||
assert plan.fallback_behavior["missing_runtime_store"] == "degrade-to-dry-run"
|
||||
assert any(diagnostic.code == "missing_adapter" for diagnostic in plan.diagnostics)
|
||||
6
tests/test_smoke.py
Normal file
6
tests/test_smoke.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import phase_memory
|
||||
|
||||
|
||||
def test_package_imports() -> None:
|
||||
assert phase_memory.__version__ == "0.1.0"
|
||||
assert phase_memory.MemoryPhase.FLUID.value == "fluid"
|
||||
Reference in New Issue
Block a user