Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/attune/memory/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,14 @@ def _generate_id(self, finding: dict[str, Any]) -> str:
def add_finding(self, workflow: str, finding: dict[str, Any]) -> str:
"""Add a finding from any workflow, return node ID.

Also accepts curated cross-session memory (no workflow origin) via
the ``USER_CONTEXT``/``FEEDBACK``/``PROJECT_CONTEXT``/``REFERENCE``
node types - pass ``workflow=""`` for those (see
``attune.memory.nodes`` module docstring).

Args:
workflow: Name of the workflow adding this finding
workflow: Name of the workflow adding this finding, or "" for
a curated-memory node with no workflow origin.
finding: Dict with at least 'type' and 'name' keys

Returns:
Expand Down
17 changes: 15 additions & 2 deletions src/attune/memory/nodes.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
"""Memory Graph Node Types

Defines node types for the cross-workflow knowledge graph.
Each node represents an entity discovered by a workflow.
Defines node types for the cross-workflow knowledge graph. Most nodes
represent an entity discovered by a workflow (a bug, a fix, a test).
The "Curated memory" category (below) is the exception: those nodes
represent hand-curated cross-session memory - decisions, standing
feedback, project context - that has no workflow origin and no
resolution lifecycle. ``source_workflow`` stays empty and ``severity``
stays unset for curated-memory nodes; ``status`` is reinterpreted as
active/superseded/stale rather than open/investigating/resolved/wontfix
(same field, no schema change).

Copyright 2025 Smart AI Memory, LLC
Licensed under the Apache License, Version 2.0
Expand Down Expand Up @@ -47,6 +54,12 @@ class NodeType(Enum):
DEPENDENCY = "dependency"
LICENSE = "license"

# Curated memory (not workflow findings - see module docstring)
USER_CONTEXT = "user_context"
FEEDBACK = "feedback"
PROJECT_CONTEXT = "project_context"
REFERENCE = "reference"


@dataclass
class Node:
Expand Down
5 changes: 3 additions & 2 deletions tests/memory/test_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ def test_all_node_types_count(self):
"""Test total number of node types."""
# FILE, FUNCTION, CLASS, MODULE, BUG, VULNERABILITY, PERFORMANCE_ISSUE,
# CODE_SMELL, TECH_DEBT, PATTERN, FIX, REFACTOR, TEST, TEST_CASE,
# COVERAGE_GAP, DOC, API_ENDPOINT, DEPENDENCY, LICENSE
assert len(NodeType) == 19
# COVERAGE_GAP, DOC, API_ENDPOINT, DEPENDENCY, LICENSE,
# USER_CONTEXT, FEEDBACK, PROJECT_CONTEXT, REFERENCE
assert len(NodeType) == 23

def test_node_type_from_string(self):
"""Test creating NodeType from string."""
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/memory/test_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,38 @@ def test_handles_corrupt_json(self, temp_graph_path):
graph = MemoryGraph(path=temp_graph_path)
assert len(graph.nodes) == 0

def test_loads_curated_memory_node_via_real_add_finding(self, temp_graph_path):
"""Regression guard: before NodeType.FEEDBACK/USER_CONTEXT/
PROJECT_CONTEXT/REFERENCE existed, reloading a graph containing one
of these types raised
``ValueError: 'feedback' is not a valid NodeType`` inside
MemoryGraph._load() -> Node.from_dict() -> NodeType(data["type"]).
Exercises the real public API (add_finding, not manual Node
construction) end to end: add, save, reload in a fresh instance,
traverse via find_related."""
graph1 = MemoryGraph(path=temp_graph_path)
feedback_id = graph1.add_finding(
workflow="",
finding={
"type": "feedback",
"name": "Standing commitments to Patrick",
"status": "active",
},
)
project_id = graph1.add_finding(
workflow="",
finding={"type": "project_context", "name": "Memory subsystem motivation"},
)
graph1.add_edge(project_id, feedback_id, EdgeType.RELATED_TO)

# A fresh instance must reload without raising.
graph2 = MemoryGraph(path=temp_graph_path)

assert graph2.nodes[feedback_id].type == NodeType.FEEDBACK
assert graph2.nodes[project_id].type == NodeType.PROJECT_CONTEXT
related = graph2.find_related(project_id, edge_types=[EdgeType.RELATED_TO])
assert [n.id for n in related] == [feedback_id]


# =============================================================================
# NODE OPERATIONS
Expand Down
35 changes: 35 additions & 0 deletions tests/unit/memory/test_nodes_coverage_boost.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ def test_node_type_enum_has_dependency_types(self):
assert NodeType.DEPENDENCY.value == "dependency"
assert NodeType.LICENSE.value == "license"

def test_node_type_enum_has_curated_memory_types(self):
"""Test that NodeType enum includes curated cross-session memory
types (not workflow findings - see module docstring)."""
assert NodeType.USER_CONTEXT.value == "user_context"
assert NodeType.FEEDBACK.value == "feedback"
assert NodeType.PROJECT_CONTEXT.value == "project_context"
assert NodeType.REFERENCE.value == "reference"

def test_node_type_can_be_created_from_string_value(self):
"""Test that NodeType can be created from string value."""
node_type = NodeType("bug")
Expand Down Expand Up @@ -351,6 +359,33 @@ def test_roundtrip_serialization(self):
assert restored.updated_at == original.updated_at
assert restored.status == original.status

def test_curated_memory_node_roundtrip_serialization(self):
"""Regression guard: before the curated-memory NodeType members
existed, Node.from_dict(node.to_dict()) on a FEEDBACK/USER_CONTEXT/
PROJECT_CONTEXT/REFERENCE node raised
``ValueError: '<value>' is not a valid NodeType`` at reload time
(from_dict calls NodeType(data["type"]) against whatever the enum
actually contains). severity stays unset and status is
reinterpreted (active, not open/resolved) for these node types -
see the module docstring."""
original = Node(
id="feedback_my_commitments_to_patrick",
type=NodeType.FEEDBACK,
name="Standing commitments to Patrick",
description="Neutral curiosity, full attention, correction without ego.",
source_file="/memory/feedback_my_commitments_to_patrick.md",
severity="",
status="active",
tags=["relationship", "standing-commitment"],
)

restored = Node.from_dict(original.to_dict())

assert restored.type == NodeType.FEEDBACK
assert restored.severity == ""
assert restored.status == "active"
assert restored.source_workflow == ""


@pytest.mark.unit
class TestBugNode:
Expand Down
Loading