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
+68 -119
View File
@@ -1,141 +1,90 @@
"""L1: `tht memory solved-search` — degrado gentile e mapping dei risultati.
SKILL.md prescrive solved-search in F4/F6/F7 di OGNI sessione: se lo store
semantico è irraggiungibile (Qdrant/Ollama non disponibili) il comando non deve morire con un
traceback grezzo ma degradare a un avviso di una riga su stderr, con stdout
puro (`[]` in modalita' --json) ed exit 0, cosi' il modello prosegue senza
exemplar. Il finalize-hook gestisce gia' lo stesso scenario in modo analogo.
"""
"""CLI adaptation: trusted context, verified recall and explicit availability failures."""
import json
from datetime import UTC, datetime
from types import SimpleNamespace
import pytest
from typer.testing import CliRunner
from tht.cli import app
from tht.memory import MemoryRecord, save_registry
from tht.memory.models import MemoryUnavailable
from tht.ports.vector import VectorReadUnavailable, VectorStoreError
from tht.vectorstore.store import VectorHit
def _cfg(tmp_path):
cfg = tmp_path / "workspace.yaml"
cfg.write_text(
"database: {database: d, schema: s, user: u, password: p, transport: direct}\n"
f"paths: {{artifacts: {tmp_path/'a'}, indexes: {tmp_path/'i'}, sessions: {tmp_path/'se'}}}\n"
"vector_db: {database: v, schema: vectors, user: u, password: p, transport: direct}\n"
"embeddings: {base_url: 'http://localhost:11434', model: qwen3-embedding:0.6b, dim: 1024}\n"
)
return cfg
@pytest.fixture
def runtime(monkeypatch):
service = SimpleNamespace(close=lambda: None, recall=lambda *a, **kw: [])
monkeypatch.setattr("tht.cli.memory_cmd._load_config_or_exit", lambda path: object())
# The config is supplied by the runner; mapping to the service is tested here.
monkeypatch.setattr("tht.cli.memory_cmd._load_config_or_exit",
lambda path: SimpleNamespace(embeddings=object(),
database=SimpleNamespace(database="sales", db_schema="public")))
monkeypatch.setattr("tht.cli.memory_cmd.memory_service", lambda cfg: service)
monkeypatch.setattr("tht.cli.vector_cmd.open_searcher", lambda cfg: object())
monkeypatch.setattr("tht.cli.vector_cmd.make_embedder", lambda cfg: object())
return service
def test_solved_search_degrades_when_vectordb_unreachable(tmp_path, monkeypatch):
@pytest.mark.parametrize("failure", [VectorStoreError, VectorReadUnavailable])
def test_solved_search_warns_when_vector_is_unavailable(runtime, monkeypatch, failure):
def boom(cfg):
raise VectorStoreError("Qdrant non raggiungibile")
raise failure("private adapter details")
monkeypatch.setattr("tht.cli.vector_cmd.open_searcher", boom)
res = CliRunner().invoke(
app, ["memory", "solved-search", "quante ablazioni", "--json", "-c", str(_cfg(tmp_path))]
)
assert res.exit_code == 0, res.output
assert json.loads(res.stdout) == [] # stdout puro: JSON valido
assert "exemplar non disponibili" in res.stderr
result = CliRunner().invoke(app, ["memory", "solved-search", "orders", "--json"])
assert result.exit_code == 0
assert json.loads(result.stdout) == []
assert "exemplar non disponibili" in result.stderr
assert "private adapter details" not in result.output
def test_solved_search_degrades_direct_vector_read_error(tmp_path, monkeypatch):
def boom(cfg):
raise VectorReadUnavailable("Vector read operation unavailable")
monkeypatch.setattr("tht.cli.vector_cmd.open_searcher", boom)
res = CliRunner().invoke(
app, ["memory", "solved-search", "quante ablazioni", "--json", "-c", str(_cfg(tmp_path))]
)
assert res.exit_code == 0, res.output
assert json.loads(res.stdout) == []
assert "exemplar non disponibili" in res.stderr
def test_solved_search_passes_only_verified_service_payload(runtime):
def recall(question, **kwargs):
assert question == "orders"
assert kwargs["solved"] and kwargs["top"] == 3
assert kwargs["scope"].database == "sales"
assert kwargs["scope"].schema_name == "public"
return [{"id": "mem-id", "question": "Orders?", "sql": "select 1", "tables": []}]
runtime.recall = recall
result = CliRunner().invoke(app, ["memory", "solved-search", "orders", "--json"])
assert result.exit_code == 0
assert json.loads(result.stdout)[0]["sql"] == "select 1"
def test_solved_search_degrades_human_mode(tmp_path, monkeypatch):
def boom(cfg):
raise VectorStoreError("Qdrant non raggiungibile")
monkeypatch.setattr("tht.cli.vector_cmd.open_searcher", boom)
res = CliRunner().invoke(
app, ["memory", "solved-search", "quante ablazioni", "-c", str(_cfg(tmp_path))]
)
assert res.exit_code == 0, res.output
assert "exemplar non disponibili" in res.stderr
assert "Traceback" not in res.stderr
def test_archive_failure_is_not_an_empty_success(runtime):
def recall(*args, **kwargs):
raise MemoryUnavailable("Memory archive is unavailable")
runtime.recall = recall
result = CliRunner().invoke(app, ["memory", "solved-search", "orders", "--json"])
assert result.exit_code == 1
assert json.loads(result.stdout)["status"] == 503
def test_solved_search_json_maps_hit_metadata(tmp_path, monkeypatch):
hit = VectorHit(
id="solved:s1", kind="solved_question", ref="s1", title="quante ablazioni nel 2023",
content="quante ablazioni nel 2023",
metadata={
"session_id": "s1", "question": "quante ablazioni nel 2023",
"sql": "SELECT 1", "tables": ["fact_seeablazione"],
},
similarity=0.91,
)
class FakeSearcher:
def search(self, vec, top_n=10, kinds=None):
assert kinds == ["solved_question"]
return [hit]
class FakeEmbedder:
def embed_query(self, text):
return [0.1] * 8
monkeypatch.setattr("tht.cli.vector_cmd.open_searcher", lambda cfg: FakeSearcher())
monkeypatch.setattr("tht.cli.vector_cmd.make_embedder", lambda e: FakeEmbedder())
res = CliRunner().invoke(
app, ["memory", "solved-search", "quante ablazioni", "--json", "-c", str(_cfg(tmp_path))]
)
assert res.exit_code == 0, res.output
data = json.loads(res.stdout)
assert data == [{
"session_id": "s1", "question": "quante ablazioni nel 2023",
"sql": "SELECT 1", "tables": ["fact_seeablazione"], "score": 0.91,
}]
def test_missing_principal_cannot_bypass_admin(monkeypatch, tmp_path):
monkeypatch.delenv("THT_PRINCIPAL_ISSUER", raising=False)
monkeypatch.delenv("THT_PRINCIPAL_SUBJECT", raising=False)
request = tmp_path / "request.json"
request.write_text(json.dumps({"action": "list", "runtime": {}}))
result = CliRunner().invoke(app, ["memory", "admin", "--workspace", "sales", "-c", str(request)])
assert result.exit_code == 1
assert json.loads(result.stdout)["status"] == 403
def test_memory_search_excludes_legacy_table_records(tmp_path, monkeypatch):
records = [
MemoryRecord(
id="mem-0001", ts=datetime(2026, 1, 1, tzinfo=UTC), session_id="s1",
decision_seq=1, type="table_promoted", subject="fact_pazienti",
),
MemoryRecord(
id="mem-0002", ts=datetime(2026, 1, 1, tzinfo=UTC), session_id="s1",
decision_seq=2, type="concept_clarified", subject="paziente attivo",
detail="flag_attivo = TRUE",
),
]
cfg = _cfg(tmp_path)
save_registry(records, tmp_path / "a" / "memory" / "registry.jsonl")
@pytest.mark.parametrize("command", ["search", "solved-search"])
def test_recall_cannot_override_runtime_database_context(runtime, command):
result = CliRunner().invoke(app, ["memory", command, "orders", "--json",
"--filters", '{"database":"outside"}'])
assert result.exit_code == 1
assert json.loads(result.stdout)["status"] == 400
class FakeSearcher:
def search(self, vec, top_n=10, kinds=None):
return [
VectorHit(
id=f"memory:{record.id}", kind="memory", ref=record.id,
title=record.subject, content=record.detail, metadata={},
similarity=0.9,
)
for record in records
]
class FakeEmbedder:
def embed_query(self, text):
return [0.1] * 8
monkeypatch.setattr("tht.cli.vector_cmd.open_searcher", lambda workspace: FakeSearcher())
monkeypatch.setattr("tht.cli.vector_cmd.make_embedder", lambda embeddings: FakeEmbedder())
res = CliRunner().invoke(
app, ["memory", "search", "pazienti", "--json", "-c", str(cfg)]
)
assert res.exit_code == 0, res.output
assert [record["id"] for record in json.loads(res.stdout)] == ["mem-0002"]
@pytest.mark.parametrize("command", ["search", "solved-search"])
def test_recall_passes_explicit_business_and_table_filters(runtime, command):
def recall(question, **kwargs):
scope = kwargs["scope"]
assert (scope.database, scope.schema_name, scope.table, scope.scope) == (
"sales", "public", "orders", "Sales")
assert scope.concepts == ["grain"]
return []
runtime.recall = recall
result = CliRunner().invoke(app, ["memory", command, "orders", "--json", "--filters",
'{"table":"orders","scope":"Sales","concepts":["grain"]}'])
assert result.exit_code == 0, result.output