feat: implement memory and evidence administration with guided repairs
Publish documentation / publish (push) Successful in 1m27s

Add PostgreSQL-backed memory, editable evidence with source review and activation, and human-approved archive repairs across the harness, API, and UI. Include migrations, deployment support, regression coverage, and validation documentation.

Refresh permissions from validated session roles so existing administrator logins can access newly deployed archive management features.
This commit is contained in:
Codex
2026-09-10 10:31:34 +02:00
parent 8fe526dd6e
commit 82e2c91f42
168 changed files with 11914 additions and 1772 deletions
File diff suppressed because it is too large Load Diff
+19 -6
View File
@@ -1,5 +1,6 @@
import json
import uuid
from contextlib import nullcontext
from datetime import UTC, datetime
from types import SimpleNamespace
@@ -7,7 +8,9 @@ from typer.testing import CliRunner
from tht.cli import app
from tht.decisions import DecisionInput
from tht.memory import MemoryRecord, recall_memories, save_registry
from tht.memory import MemoryRecord, recall_memories
from tht.memory.models import Card
from tht.memory.service import MemoryService
from tht.phase import current_phase
from tht.session.filesystem_repository import FilesystemSessionRepository
from tht.session.models import PrincipalContext, SessionManifest
@@ -42,7 +45,7 @@ class Searcher:
self.hits = hits
self.calls = []
def search(self, embedding, *, top_n, kinds):
def search(self, embedding, *, top_n, kinds, **kwargs):
self.calls.append((embedding, top_n, kinds))
return self.hits
@@ -145,11 +148,21 @@ def test_recall_cli_reconstructs_applied_and_rejected_memory_from_persisted_f2_s
records = [_memory("mem-0001"), _memory("mem-0003")]
searcher = Searcher([
SimpleNamespace(ref="mem-0003", similarity=0.9),
SimpleNamespace(ref="mem-0001", similarity=0.8),
SimpleNamespace(ref="mem-0003", similarity=0.9,
metadata={"memory_revision": "r", "memory_format": 2}),
SimpleNamespace(ref="mem-0001", similarity=0.8,
metadata={"memory_revision": "r", "memory_format": 2}),
])
embedder = Embedder()
save_registry(records, tmp_path / "artifacts" / "memory" / "registry.jsonl")
cards = {r.id: Card(id=r.id, family="domain_clarification", subject=r.subject,
detail=r.detail, scope="psd-clinical", workspace_id="psd-clinical", origin="workflow",
created_at=r.ts, updated_at=r.ts, revision="r", indexed=True) for r in records}
archive = SimpleNamespace(list=lambda query: {}, get=lambda identity: cards[identity],
close=lambda: None)
archive.operation = lambda: nullcontext(archive)
service = MemoryService(archive, PrincipalContext(issuer="local", subject="reviewer"),
store_factory=lambda: None, embedder_factory=Embedder)
monkeypatch.setattr("tht.cli.memory_cmd.memory_service", lambda cfg: service)
monkeypatch.setattr("tht.cli.vector_cmd.open_searcher", lambda cfg: searcher)
monkeypatch.setattr("tht.cli.vector_cmd.make_embedder", lambda cfg: embedder)
@@ -165,4 +178,4 @@ def test_recall_cli_reconstructs_applied_and_rejected_memory_from_persisted_f2_s
assert json.loads(response.stdout) == []
assert current_phase(repository.get(session_id)) == 2
assert embedder.questions == ["active patients"]
assert searcher.calls == [([0.1, 0.2], 5, ["memory"])]
assert searcher.calls == [([0.1, 0.2], 20, ["memory"])]
+121
View File
@@ -0,0 +1,121 @@
"""Deterministic retrieval policy tests, separate from actual embedding/Qdrant recovery."""
from contextlib import nullcontext
from datetime import UTC, datetime
from types import SimpleNamespace
import pytest
from tht.memory.models import Card, MemoryNotFound
from tht.memory.retrieval import RecallScope, expand_and_rank
def card(identity, **values):
return Card(id=identity, workspace_id="sales", family="domain_clarification",
subject=identity, scope="Sales", origin="manual", revision="r", indexed=True,
created_at=datetime.now(UTC), updated_at=datetime.now(UTC), **values)
class Archive:
def __init__(self, *cards):
self.cards = {c.id: c for c in cards}
self.reads = []
def get(self, identity):
self.reads.append(identity)
if identity not in self.cards:
raise MemoryNotFound(identity)
return self.cards[identity]
def operation(self):
return nullcontext(self)
def hit(identity, **metadata):
return SimpleNamespace(ref=identity, metadata={"memory_revision": "r", "memory_format": 2,
**metadata})
def link(identity):
return {"target_id": identity, "meaning": "Requires the grain clarification"}
def rank(repo, hits, **kwargs):
return expand_and_rank(repo, hits, scope=kwargs.pop("scope", RecallScope()),
family="domain_clarification", excluded=kwargs.pop("excluded", set()), top=100, **kwargs)
def test_link_only_candidates_cycles_duplicates_depth_and_current_content():
repo = Archive(card("a", links=[link("b")]),
card("b", detail="Current correction", links=[link("a"), link("c")]),
card("c", links=[link("d")]), card("d"))
results = rank(repo, [hit("a"), hit("a")])
assert [r.card.id for r in results] == ["a", "b", "c"]
assert results[1].card.detail == "Current correction"
assert results[2].path == ("a", "b", "c")
assert len(repo.reads) == len(set(repo.reads)) == 3
assert rank(repo, [hit("a")]) == results # duplicate seeds do not boost a score
def test_direct_and_graph_candidates_are_reranked_together_without_cycle_boost():
repo = Archive(card("a", links=[link("c")]), card("b"), card("c", links=[link("a")]))
results = rank(repo, [hit("a"), hit("b"), hit("c")])
assert [r.card.id for r in results] == ["a", "c", "b"]
assert len(results) == 3
@pytest.mark.parametrize("invalid", ["missing", "pending", "excluded", "wrong_scope", "family"])
def test_ineligible_link_targets_cannot_be_returned_or_used_as_bridges(invalid):
target = card("b", links=[link("c")])
if invalid == "pending":
target.indexed = False
if invalid == "wrong_scope":
target.scope = "Purchases"
if invalid == "family":
target.family = "sql_rule"
repo = Archive(card("a", links=[link("b")]), card("c"),
*([] if invalid == "missing" else [target]))
results = rank(repo, [hit("a")], scope=RecallScope(scope="Sales"),
excluded={"b"} if invalid == "excluded" else set())
assert [r.card.id for r in results] == ["a"]
@pytest.mark.parametrize("metadata", [{"memory_revision": "old"}, {"memory_format": 1}])
def test_stale_seeds_do_not_expand(metadata):
repo = Archive(card("a", links=[link("b")]), card("b"))
assert rank(repo, [hit("a", **metadata)]) == []
def test_physical_scope_matches_one_dependency_and_includes_workspace_and_parent_rules():
scope = RecallScope(database="sales", schema_name="public", table="orders", column="id")
assert scope.matches(card("global"))
assert scope.matches(card("database", dependencies=[{"database": "sales"}]))
assert scope.matches(card("table", dependencies=[{"database": "sales",
"schema_name": "public", "table": "orders"}]))
assert not scope.matches(card("split", dependencies=[
{"database": "sales", "schema_name": "public", "table": "orders", "column": "amount"},
{"database": "purchases", "schema_name": "public", "table": "orders", "column": "id"},
]))
assert not scope.matches(card("schema", dependencies=[
{"database": "sales", "schema_name": "audit", "table": "orders", "column": "id"},
]))
def test_scope_and_concepts_are_explicit_and_combined():
scope = RecallScope(scope="Sales", concepts=["orders", "grain"])
assert scope.matches(card("a", concepts=["orders", "grain"]))
assert not scope.matches(card("b", concepts=["orders"]))
with pytest.raises(ValueError):
RecallScope(column="id")
def test_fanout_and_total_visits_are_bounded():
cards = [card(f"c{i:04}", links=[link(f"c{j:04}") for j in range(i+1, i+101)])
for i in range(500)]
repo = Archive(*cards)
results = rank(repo, [hit("c0000")])
assert "c0100" not in {r.card.id for r in results}
assert len(repo.reads) <= 200
repo.reads.clear()
rank(repo, [hit(c.id) for c in cards[:100]])
assert len(repo.reads) <= 200
@@ -9,7 +9,7 @@ from tht.session.filesystem_repository import FilesystemSessionRepository
from tht.session.models import PrincipalContext, SessionManifest
def test_finalize_commits_session_before_best_effort_post_commit_read_failure(
def test_finalize_commits_session_without_implicitly_reading_or_saving_memory(
tmp_path, monkeypatch, capsys
):
repository = FilesystemSessionRepository(
@@ -83,5 +83,5 @@ def test_finalize_commits_session_before_best_effort_post_commit_read_failure(
captured = capsys.readouterr()
assert repository.get(session_id).manifest.status == "finalized"
assert "finalized snapshot unavailable" in captured.err
assert "finalized snapshot unavailable" not in captured.err
assert f"OK: sessione {session_id} finalizzata" in captured.out