fix(mcp): forward valid_to and source params in kg_add/kg_invalidate (#1314)

`tool_kg_add` previously accepted only `valid_from` and `source_closet`,
silently dropping `valid_to`, `source_file`, and `source_drawer_id` at
the MCP boundary. Backfilling already-ended historical facts therefore
collapsed to "still current," and adapter provenance never reached
the SQLite layer even though `KnowledgeGraph.add_triple` already
supported every column.

`tool_kg_invalidate` returned the literal string `"today"` whenever the
caller omitted `ended`, hiding the actual stamped date from anyone trying
to verify what got persisted.

Changes:
- Extend `tool_kg_add` signature + MCP input_schema with `valid_to`,
  `source_file`, `source_drawer_id`; forward all of them to
  `_kg.add_triple` and to the WAL log.
- Resolve `ended` to `date.today().isoformat()` in `tool_kg_invalidate`
  before logging / returning, so the response always reports the actual
  date stored in `valid_to`.
- Add regression tests for valid_to round-trip, source_file /
  source_drawer_id provenance, and the resolved-ended-date contract.
- Leave TODO(#1283) markers so the open ISO-8601 validation PR can drop
  `validate_iso_date` over `valid_from` / `valid_to` / `ended` cleanly.

The underlying `KnowledgeGraph.add_triple` already accepted these
kwargs (RFC 002 §5.5) — only the MCP edge needed wiring up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Igor Lins e Silva
2026-05-02 22:54:32 -03:00
parent d07b730f08
commit e4e25ed186
2 changed files with 153 additions and 16 deletions
+90 -6
View File
@@ -476,9 +476,9 @@ class TestWriteTools:
assert result1["success"] is True
assert result2["success"] is True
assert (
result1["drawer_id"] != result2["drawer_id"]
), "Documents with shared header but different content must have distinct drawer IDs"
assert result1["drawer_id"] != result2["drawer_id"], (
"Documents with shared header but different content must have distinct drawer IDs"
)
def test_delete_drawer(self, monkeypatch, config, palace_path, seeded_collection, kg):
_patch_mcp_server(monkeypatch, config, kg)
@@ -650,6 +650,90 @@ class TestKGTools:
ended="2026-03-01",
)
assert result["success"] is True
# Regression #1314: response must echo the actual ended date,
# not silently drop it and return the literal string "today".
assert result["ended"] == "2026-03-01"
def test_kg_add_forwards_valid_to(self, monkeypatch, config, palace_path, kg):
"""Regression #1314 case 1: valid_to must round-trip through kg_add."""
_patch_mcp_server(monkeypatch, config, kg)
from mempalace.mcp_server import tool_kg_add
result = tool_kg_add(
subject="_test_temporal",
predicate="had_value",
object="probe",
valid_from="2026-01-01",
valid_to="2026-04-28",
)
assert result["success"] is True
facts = kg.query_entity("_test_temporal")
assert len(facts) == 1
assert facts[0]["valid_from"] == "2026-01-01"
assert facts[0]["valid_to"] == "2026-04-28"
# An already-ended fact must not be reported as still current.
assert facts[0]["current"] is False
def test_kg_add_forwards_source_provenance(self, monkeypatch, config, palace_path, kg):
"""Regression #1314 case 3: source_file / source_drawer_id reach storage."""
_patch_mcp_server(monkeypatch, config, kg)
from mempalace.mcp_server import tool_kg_add
result = tool_kg_add(
subject="operating-verb",
predicate="candidate",
object="husbandry",
valid_from="2026-04-28",
source_closet="closet-42",
source_file="docs/decisions.md",
source_drawer_id="drawer_abc123",
)
assert result["success"] is True
triple_id = result["triple_id"]
# Read raw row to verify all provenance columns persisted.
with kg._lock:
row = (
kg._conn()
.execute(
"SELECT source_closet, source_file, source_drawer_id FROM triples WHERE id = ?",
(triple_id,),
)
.fetchone()
)
assert row is not None
assert row["source_closet"] == "closet-42"
assert row["source_file"] == "docs/decisions.md"
assert row["source_drawer_id"] == "drawer_abc123"
def test_kg_invalidate_returns_actual_ended_date(
self, monkeypatch, config, palace_path, seeded_kg
):
"""Regression #1314 case 2: response reports the resolved date, not 'today'."""
from datetime import date as _date
_patch_mcp_server(monkeypatch, config, seeded_kg)
from mempalace.mcp_server import tool_kg_invalidate
# Caller-supplied date round-trips into the response.
explicit = tool_kg_invalidate(
subject="Max",
predicate="does",
object="swimming",
ended="2026-04-28",
)
assert explicit["ended"] == "2026-04-28"
# Caller-omitted date resolves to today's ISO date — never the
# literal string "today" the buggy implementation used to return.
implicit = tool_kg_invalidate(
subject="Max",
predicate="loves",
object="Chess",
)
assert implicit["ended"] != "today"
assert implicit["ended"] == _date.today().isoformat()
def test_kg_timeline(self, monkeypatch, config, palace_path, seeded_kg):
_patch_mcp_server(monkeypatch, config, seeded_kg)
@@ -960,9 +1044,9 @@ class TestCacheInvalidation:
all_calls = captured["get"] + captured["create"]
assert all_calls, "expected get_collection or create_collection to be called"
for kwargs in all_calls:
assert (
"embedding_function" in kwargs
), f"missing embedding_function= in chromadb call: {kwargs}"
assert "embedding_function" in kwargs, (
f"missing embedding_function= in chromadb call: {kwargs}"
)
assert kwargs["embedding_function"] is not None
# Same expectation on the create=False (cache-miss) reopen path.