refactor(evidence): migrate runtime consumption (#29)
This commit is contained in:
@@ -77,7 +77,7 @@ def test_finalize_commits_session_before_best_effort_post_commit_read_failure(
|
|||||||
monkeypatch.setattr("tht.execute.warnings.runtime_warnings", lambda *args: [])
|
monkeypatch.setattr("tht.execute.warnings.runtime_warnings", lambda *args: [])
|
||||||
monkeypatch.setattr("tht.execute.warnings.static_warnings", lambda *args: [])
|
monkeypatch.setattr("tht.execute.warnings.static_warnings", lambda *args: [])
|
||||||
monkeypatch.setattr("tht.report.render_validation_report", lambda **kwargs: "verified\n")
|
monkeypatch.setattr("tht.report.render_validation_report", lambda **kwargs: "verified\n")
|
||||||
monkeypatch.setattr("tht.session.artifacts.build_evidence_entries", lambda *args: [])
|
monkeypatch.setattr("tht.evidence.project_session", lambda *args: [])
|
||||||
|
|
||||||
session_cmd.finalize_cmd(session_id, config=Path("unused.yaml"))
|
session_cmd.finalize_cmd(session_id, config=Path("unused.yaml"))
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from tht.decisions import DecisionRecord
|
|||||||
from tht.evidence import (
|
from tht.evidence import (
|
||||||
acquire,
|
acquire,
|
||||||
active_searcher,
|
active_searcher,
|
||||||
|
build_retrieval_entries,
|
||||||
discover,
|
discover,
|
||||||
project_session,
|
project_session,
|
||||||
resolve_citation,
|
resolve_citation,
|
||||||
@@ -125,6 +126,18 @@ def test_search_facade_preserves_active_filtering_and_global_order(tmp_path):
|
|||||||
assert facade_delegate.calls == legacy_delegate.calls
|
assert facade_delegate.calls == legacy_delegate.calls
|
||||||
|
|
||||||
|
|
||||||
|
def test_retrieval_entries_preserve_hit_order_and_existing_projection_shape():
|
||||||
|
hits = [
|
||||||
|
SimpleNamespace(label="Second", status="reviewed", content="abcdefgh"),
|
||||||
|
SimpleNamespace(label="First", status=None, content="12345678"),
|
||||||
|
]
|
||||||
|
|
||||||
|
assert build_retrieval_entries(hits, excerpt_chars=5) == [
|
||||||
|
{"title": "Second", "status": "reviewed", "excerpt": "abcde"},
|
||||||
|
{"title": "First", "status": None, "excerpt": "12345"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _canonical_store(root, evidence_id):
|
def _canonical_store(root, evidence_id):
|
||||||
content = f"# {evidence_id}\n"
|
content = f"# {evidence_id}\n"
|
||||||
digest = hashlib.sha256(content.encode()).hexdigest()
|
digest = hashlib.sha256(content.encode()).hexdigest()
|
||||||
@@ -159,6 +172,40 @@ def test_citation_facade_matches_active_corpus_resolution(tmp_path):
|
|||||||
assert resolve_citation(store, "missing", materialized_root=materialized) == ""
|
assert resolve_citation(store, "missing", materialized_root=materialized) == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_projection_routes_corpus_citations_through_the_facade(tmp_path, monkeypatch):
|
||||||
|
evidence_root = tmp_path / "artifacts" / "evidence"
|
||||||
|
(tmp_path / "corpus").mkdir()
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_resolve(store, evidence_id, *, materialized_root=None):
|
||||||
|
calls.append((store.root, evidence_id, materialized_root))
|
||||||
|
return f"/materialized/{evidence_id}.md"
|
||||||
|
|
||||||
|
monkeypatch.setattr("tht.evidence.resolve_citation", fake_resolve)
|
||||||
|
linking = SchemaLinking(
|
||||||
|
question="q",
|
||||||
|
candidates=[Candidate(
|
||||||
|
kind="table",
|
||||||
|
name="fact_procedure",
|
||||||
|
evidence=["evi-used"],
|
||||||
|
decision="promoted",
|
||||||
|
decision_seq=17,
|
||||||
|
)],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert build_evidence_entries([], linking, evidence_root) == [{
|
||||||
|
"id": "evi-used",
|
||||||
|
"file": "/materialized/evi-used.md",
|
||||||
|
"esito": "usata",
|
||||||
|
"decision_seq": 17,
|
||||||
|
}]
|
||||||
|
assert calls == [(
|
||||||
|
tmp_path / "corpus",
|
||||||
|
"evi-used",
|
||||||
|
tmp_path / "artifacts" / ".materialized-evidence",
|
||||||
|
)]
|
||||||
|
|
||||||
|
|
||||||
def _record(seq, type_, subject):
|
def _record(seq, type_, subject):
|
||||||
return DecisionRecord(
|
return DecisionRecord(
|
||||||
seq=seq,
|
seq=seq,
|
||||||
|
|||||||
@@ -93,6 +93,18 @@ def _patch(monkeypatch, embedder, searcher):
|
|||||||
|
|
||||||
|
|
||||||
def test_pack_single_embed_and_sections(tmp_path, monkeypatch):
|
def test_pack_single_embed_and_sections(tmp_path, monkeypatch):
|
||||||
|
import tht.evidence as evidence_facade
|
||||||
|
|
||||||
|
projected = []
|
||||||
|
build_retrieval_entries = evidence_facade.build_retrieval_entries
|
||||||
|
monkeypatch.setattr(
|
||||||
|
evidence_facade,
|
||||||
|
"build_retrieval_entries",
|
||||||
|
lambda results, *, excerpt_chars: (
|
||||||
|
projected.append((list(results), excerpt_chars))
|
||||||
|
or build_retrieval_entries(results, excerpt_chars=excerpt_chars)
|
||||||
|
),
|
||||||
|
)
|
||||||
cfg = _workspace(tmp_path)
|
cfg = _workspace(tmp_path)
|
||||||
emb = _FakeEmbedder()
|
emb = _FakeEmbedder()
|
||||||
searcher = _FakeSearcher()
|
searcher = _FakeSearcher()
|
||||||
@@ -109,6 +121,7 @@ def test_pack_single_embed_and_sections(tmp_path, monkeypatch):
|
|||||||
# must not leak into a new search pack.
|
# must not leak into a new search pack.
|
||||||
assert "Dominio ablazione" not in res.output
|
assert "Dominio ablazione" not in res.output
|
||||||
assert "SELECT 1" in res.output
|
assert "SELECT 1" in res.output
|
||||||
|
assert projected == [([], 400)]
|
||||||
|
|
||||||
|
|
||||||
def test_pack_json_and_session_file(tmp_path, monkeypatch):
|
def test_pack_json_and_session_file(tmp_path, monkeypatch):
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import pytest
|
|||||||
from tht.corpus.models import CanonicalDocument, CorpusManifest
|
from tht.corpus.models import CanonicalDocument, CorpusManifest
|
||||||
from tht.corpus.store import CorpusStore
|
from tht.corpus.store import CorpusStore
|
||||||
from tht.decisions import DecisionRecord, append_decision
|
from tht.decisions import DecisionRecord, append_decision
|
||||||
|
from tht.evidence import project_session
|
||||||
from tht.phase import current_phase, effective_decisions
|
from tht.phase import current_phase, effective_decisions
|
||||||
from tht.session.artifacts import build_evidence_entries
|
|
||||||
from tht.session.models import Candidate, SchemaLinking
|
from tht.session.models import Candidate, SchemaLinking
|
||||||
from tht.workflow import load_workflow
|
from tht.workflow import load_workflow
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ def test_schema_linking_evidence_used_resolves_from_the_active_canonical_corpus(
|
|||||||
)
|
)
|
||||||
store.publish(generation)
|
store.publish(generation)
|
||||||
|
|
||||||
entries = build_evidence_entries(
|
entries = project_session(
|
||||||
[],
|
[],
|
||||||
_linking("evi-used"),
|
_linking("evi-used"),
|
||||||
tmp_path / "artifacts" / "evidence",
|
tmp_path / "artifacts" / "evidence",
|
||||||
@@ -195,7 +195,7 @@ def test_legacy_evidence_without_a_canonical_corpus_keeps_used_and_reviewed_outc
|
|||||||
for evidence_id in ("evi-used", "evi-accepted", "evi-rejected"):
|
for evidence_id in ("evi-used", "evi-accepted", "evi-rejected"):
|
||||||
(evidence_root / f"{evidence_id}.md").write_text(f"# {evidence_id}\n")
|
(evidence_root / f"{evidence_id}.md").write_text(f"# {evidence_id}\n")
|
||||||
|
|
||||||
entries = build_evidence_entries(
|
entries = project_session(
|
||||||
[
|
[
|
||||||
_record(21, "evidence_accepted", "evi-accepted"),
|
_record(21, "evidence_accepted", "evi-accepted"),
|
||||||
_record(22, "evidence_rejected", "evi-rejected"),
|
_record(22, "evidence_rejected", "evi-rejected"),
|
||||||
|
|||||||
@@ -54,18 +54,16 @@ def search_cmd(
|
|||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
from tht.cli.vector_cmd import make_embedder, open_searcher, require_vector_cfg
|
from tht.cli.vector_cmd import make_embedder, open_searcher, require_vector_cfg
|
||||||
|
from tht.evidence import active_searcher, validate_corpus_workspace
|
||||||
from tht.lshindex import LshIndexError, load_index, query_index
|
from tht.lshindex import LshIndexError, load_index, query_index
|
||||||
from tht.search import combined_search
|
from tht.search import combined_search
|
||||||
|
|
||||||
cfg = _load_config_or_exit(config)
|
cfg = _load_config_or_exit(config)
|
||||||
from tht.search.evidence import validate_corpus_workspace
|
|
||||||
|
|
||||||
workspace_id = workspace_id_for_config(cfg, config)
|
workspace_id = workspace_id_for_config(cfg, config)
|
||||||
validate_corpus_workspace(cfg, workspace_id)
|
validate_corpus_workspace(cfg, workspace_id)
|
||||||
dwh_snapshot = _leased_dwh_snapshot(cfg, ctx)
|
dwh_snapshot = _leased_dwh_snapshot(cfg, ctx)
|
||||||
require_vector_cfg(cfg)
|
require_vector_cfg(cfg)
|
||||||
from tht.search.evidence import active_searcher
|
|
||||||
|
|
||||||
runtime_searcher = active_searcher(
|
runtime_searcher = active_searcher(
|
||||||
cfg, open_searcher(cfg),
|
cfg, open_searcher(cfg),
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
@@ -255,13 +253,17 @@ def pack_cmd(
|
|||||||
from sqlalchemy.exc import OperationalError
|
from sqlalchemy.exc import OperationalError
|
||||||
|
|
||||||
from tht.cli.vector_cmd import make_embedder, open_searcher, require_vector_cfg
|
from tht.cli.vector_cmd import make_embedder, open_searcher, require_vector_cfg
|
||||||
|
from tht.evidence import (
|
||||||
|
active_searcher,
|
||||||
|
build_retrieval_entries,
|
||||||
|
validate_corpus_workspace,
|
||||||
|
)
|
||||||
from tht.ports.vector import VectorReadUnavailable, VectorStoreError
|
from tht.ports.vector import VectorReadUnavailable, VectorStoreError
|
||||||
from tht.search import combined_search, schema_tables
|
from tht.search import combined_search, schema_tables
|
||||||
from tht.memory import SOLVED_KIND
|
from tht.memory import SOLVED_KIND
|
||||||
from tht.vectorstore.embeddings import EmbeddingsError
|
from tht.vectorstore.embeddings import EmbeddingsError
|
||||||
|
|
||||||
cfg = _load_config_or_exit(config)
|
cfg = _load_config_or_exit(config)
|
||||||
from tht.search.evidence import validate_corpus_workspace
|
|
||||||
|
|
||||||
workspace_id = workspace_id_for_config(cfg, config)
|
workspace_id = workspace_id_for_config(cfg, config)
|
||||||
validate_corpus_workspace(cfg, workspace_id)
|
validate_corpus_workspace(cfg, workspace_id)
|
||||||
@@ -277,8 +279,6 @@ def pack_cmd(
|
|||||||
vec = None
|
vec = None
|
||||||
searcher = embedder = None
|
searcher = embedder = None
|
||||||
try:
|
try:
|
||||||
from tht.search.evidence import active_searcher
|
|
||||||
|
|
||||||
searcher = active_searcher(
|
searcher = active_searcher(
|
||||||
cfg, open_searcher(cfg),
|
cfg, open_searcher(cfg),
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
@@ -314,11 +314,7 @@ def pack_cmd(
|
|||||||
top=PACK_EVIDENCE_TOP, rrf_k=cfg.search.rrf_k,
|
top=PACK_EVIDENCE_TOP, rrf_k=cfg.search.rrf_k,
|
||||||
kinds=KIND_MAP["evidence"], query_vec=vec,
|
kinds=KIND_MAP["evidence"], query_vec=vec,
|
||||||
)
|
)
|
||||||
evidence = [
|
evidence = build_retrieval_entries(ev, excerpt_chars=PACK_EXCERPT_CHARS)
|
||||||
{"title": r.label, "status": r.status,
|
|
||||||
"excerpt": r.content[:PACK_EXCERPT_CHARS]}
|
|
||||||
for r in ev
|
|
||||||
]
|
|
||||||
except degrade as e:
|
except degrade as e:
|
||||||
warnings.append(f"ricerca evidence fallita ({e})")
|
warnings.append(f"ricerca evidence fallita ({e})")
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -518,7 +518,7 @@ def finalize_cmd(session_id: str = typer.Argument(...), config: Path = CONFIG_OP
|
|||||||
from tht.execute import ExecutionError
|
from tht.execute import ExecutionError
|
||||||
from tht.execute.warnings import plan_warnings, runtime_warnings, static_warnings
|
from tht.execute.warnings import plan_warnings, runtime_warnings, static_warnings
|
||||||
from tht.report import extract_reviewer_notes, render_validation_report
|
from tht.report import extract_reviewer_notes, render_validation_report
|
||||||
from tht.session.artifacts import build_evidence_entries
|
from tht.evidence import project_session
|
||||||
from tht.phase import cte_plan as effective_cte_plan
|
from tht.phase import cte_plan as effective_cte_plan
|
||||||
from tht.phase import effective_decisions
|
from tht.phase import effective_decisions
|
||||||
from tht.session.models import SchemaLinking
|
from tht.session.models import SchemaLinking
|
||||||
@@ -614,7 +614,7 @@ def finalize_cmd(session_id: str = typer.Argument(...), config: Path = CONFIG_OP
|
|||||||
linking = SchemaLinking.model_validate(
|
linking = SchemaLinking.model_validate(
|
||||||
json.loads(snapshot.artifacts["schema_linking"])
|
json.loads(snapshot.artifacts["schema_linking"])
|
||||||
)
|
)
|
||||||
entries = build_evidence_entries(decisions, linking, cfg.paths.artifacts / "evidence")
|
entries = project_session(decisions, linking, cfg.paths.artifacts / "evidence")
|
||||||
evidence = json.dumps(entries, ensure_ascii=False, indent=2)
|
evidence = json.dumps(entries, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
# --- manifest + riepilogo ---
|
# --- manifest + riepilogo ---
|
||||||
|
|||||||
@@ -55,6 +55,18 @@ def validate_corpus_workspace(cfg, workspace_id: str) -> None:
|
|||||||
legacy_validate(cfg, workspace_id)
|
legacy_validate(cfg, workspace_id)
|
||||||
|
|
||||||
|
|
||||||
|
def build_retrieval_entries(results, *, excerpt_chars: int) -> list[dict]:
|
||||||
|
"""Project ordered Evidence search hits into the existing retrieval-pack shape."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"title": result.label,
|
||||||
|
"status": result.status,
|
||||||
|
"excerpt": result.content[:excerpt_chars],
|
||||||
|
}
|
||||||
|
for result in results
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def resolve_citation(
|
def resolve_citation(
|
||||||
store: "CorpusStore",
|
store: "CorpusStore",
|
||||||
evidence_id: str,
|
evidence_id: str,
|
||||||
@@ -71,15 +83,48 @@ def resolve_citation(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_session_citation(evidence_root: Path, evidence_id: str) -> str:
|
||||||
|
# New deployments resolve only immutable materialized files from ACTIVE. Keep the
|
||||||
|
# curated-tree fallback for sessions created before a canonical corpus exists.
|
||||||
|
corpus_root = evidence_root.parent.parent / "corpus"
|
||||||
|
if corpus_root.exists():
|
||||||
|
from tht.corpus.store import CorpusStore
|
||||||
|
|
||||||
|
return resolve_citation(
|
||||||
|
CorpusStore(corpus_root),
|
||||||
|
evidence_id,
|
||||||
|
materialized_root=evidence_root.parent / ".materialized-evidence",
|
||||||
|
)
|
||||||
|
for match in evidence_root.rglob(f"{evidence_id}.md"):
|
||||||
|
return str(match)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def project_session(
|
def project_session(
|
||||||
decisions: list["DecisionRecord"],
|
decisions: list["DecisionRecord"],
|
||||||
linking: "SchemaLinking",
|
linking: "SchemaLinking",
|
||||||
evidence_root: Path,
|
evidence_root: Path,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""Project cited and reviewed Evidence into the existing session artifact shape."""
|
"""Project cited and reviewed Evidence into the existing session artifact shape."""
|
||||||
from tht.session.artifacts import build_evidence_entries
|
entries: dict[str, dict] = {}
|
||||||
|
for candidate in linking.candidates:
|
||||||
return build_evidence_entries(decisions, linking, evidence_root)
|
for evidence_id in candidate.evidence:
|
||||||
|
entries.setdefault(evidence_id, {
|
||||||
|
"id": evidence_id,
|
||||||
|
"file": _resolve_session_citation(evidence_root, evidence_id),
|
||||||
|
"esito": "usata",
|
||||||
|
"decision_seq": candidate.decision_seq,
|
||||||
|
})
|
||||||
|
for decision in decisions:
|
||||||
|
if decision.type not in ("evidence_accepted", "evidence_rejected"):
|
||||||
|
continue
|
||||||
|
entries[decision.subject] = {
|
||||||
|
"id": decision.subject,
|
||||||
|
"file": _resolve_session_citation(evidence_root, decision.subject),
|
||||||
|
"esito": "accettata" if decision.type == "evidence_accepted" else "scartata",
|
||||||
|
"decision_seq": decision.seq,
|
||||||
|
}
|
||||||
|
return list(entries.values())
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -90,6 +135,7 @@ __all__ = [
|
|||||||
"SourceObject",
|
"SourceObject",
|
||||||
"acquire",
|
"acquire",
|
||||||
"active_searcher",
|
"active_searcher",
|
||||||
|
"build_retrieval_entries",
|
||||||
"discover",
|
"discover",
|
||||||
"project_session",
|
"project_session",
|
||||||
"resolve_citation",
|
"resolve_citation",
|
||||||
|
|||||||
@@ -1,49 +1,17 @@
|
|||||||
|
"""Legacy session-artifact entrypoints retained during the Evidence migration."""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from tht.decisions import DecisionRecord
|
from tht.decisions import DecisionRecord
|
||||||
from tht.session.models import SchemaLinking
|
from tht.session.models import SchemaLinking
|
||||||
|
|
||||||
|
|
||||||
def _find_evidence_file(evidence_root: Path, evidence_id: str) -> str:
|
|
||||||
# New deployments resolve only immutable materialized files from ACTIVE. Keep
|
|
||||||
# the legacy curated-tree fallback for sessions created before a corpus exists.
|
|
||||||
corpus_root = evidence_root.parent.parent / "corpus"
|
|
||||||
if corpus_root.exists():
|
|
||||||
from tht.corpus.store import CorpusStore
|
|
||||||
from tht.search.evidence import resolve_evidence_file
|
|
||||||
return resolve_evidence_file(
|
|
||||||
CorpusStore(corpus_root), evidence_id,
|
|
||||||
materialized_root=evidence_root.parent / ".materialized-evidence",
|
|
||||||
)
|
|
||||||
for match in evidence_root.rglob(f"{evidence_id}.md"):
|
|
||||||
return str(match)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def build_evidence_entries(
|
def build_evidence_entries(
|
||||||
decisions: list[DecisionRecord],
|
decisions: list[DecisionRecord],
|
||||||
linking: SchemaLinking,
|
linking: SchemaLinking,
|
||||||
evidence_root: Path,
|
evidence_root: Path,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""Elenco {id, file, esito, decision_seq}: evidence citate nello schema linking
|
"""Compatibility shim for callers not yet migrated to ``tht.evidence``."""
|
||||||
(esito 'usata') e decisioni esplicite del reviewer (accettata/scartata, che
|
from tht.evidence import project_session
|
||||||
prevalgono sul linking)."""
|
|
||||||
entries: dict[str, dict] = {}
|
return project_session(decisions, linking, evidence_root)
|
||||||
for candidate in linking.candidates:
|
|
||||||
for evidence_id in candidate.evidence:
|
|
||||||
entries.setdefault(evidence_id, {
|
|
||||||
"id": evidence_id,
|
|
||||||
"file": _find_evidence_file(evidence_root, evidence_id),
|
|
||||||
"esito": "usata",
|
|
||||||
"decision_seq": candidate.decision_seq,
|
|
||||||
})
|
|
||||||
for d in decisions:
|
|
||||||
if d.type not in ("evidence_accepted", "evidence_rejected"):
|
|
||||||
continue
|
|
||||||
entries[d.subject] = {
|
|
||||||
"id": d.subject,
|
|
||||||
"file": _find_evidence_file(evidence_root, d.subject),
|
|
||||||
"esito": "accettata" if d.type == "evidence_accepted" else "scartata",
|
|
||||||
"decision_seq": d.seq,
|
|
||||||
}
|
|
||||||
return list(entries.values())
|
|
||||||
|
|||||||
Reference in New Issue
Block a user