refactor(evidence): remove legacy Python layout (#31)

This commit is contained in:
2026-08-24 02:36:30 +02:00
parent 44f1efa5ba
commit 840848df94
36 changed files with 246 additions and 321 deletions
+4 -4
View File
@@ -1,7 +1,7 @@
import pytest import pytest
from tht.adapters.evidence import FilesystemEvidenceSource, HttpManifestEvidenceSource from tht.evidence.adapters import FilesystemEvidenceSource, HttpManifestEvidenceSource
from tht.adapters.factory import build_evidence_sources from tht.evidence import build_sources
from tht.config import ( from tht.config import (
ConfigError, ConfigError,
PgvectorDirectConfig, PgvectorDirectConfig,
@@ -369,7 +369,7 @@ evidence:
assert "example.test" not in repr(cfg.evidence) assert "example.test" not in repr(cfg.evidence)
assert "example.test" not in cfg.evidence.model_dump_json() assert "example.test" not in cfg.evidence.model_dump_json()
assert cfg.evidence.sources[1].allow_private_hosts is False assert cfg.evidence.sources[1].allow_private_hosts is False
sources = build_evidence_sources(cfg) sources = build_sources(cfg.evidence)
assert isinstance(sources[0], FilesystemEvidenceSource) assert isinstance(sources[0], FilesystemEvidenceSource)
assert isinstance(sources[1], HttpManifestEvidenceSource) assert isinstance(sources[1], HttpManifestEvidenceSource)
assert "example.test" not in repr(sources[1]) assert "example.test" not in repr(sources[1])
@@ -381,7 +381,7 @@ evidence:
source_root: {tmp_path} source_root: {tmp_path}
evidence_dir: curated evidence_dir: curated
""") """)
legacy_source = build_evidence_sources(load_config(legacy))[0] legacy_source = build_sources(load_config(legacy).evidence)[0]
assert isinstance(legacy_source, FilesystemEvidenceSource) assert isinstance(legacy_source, FilesystemEvidenceSource)
assert legacy_source.root == (tmp_path / "curated").resolve() assert legacy_source.root == (tmp_path / "curated").resolve()
+2 -2
View File
@@ -2,8 +2,8 @@ import hashlib
import pytest import pytest
from tht.corpus.chunk import ChunkPolicy, chunk from tht.evidence.corpus.chunk import ChunkPolicy, chunk
from tht.corpus.models import CanonicalDocument, CorpusManifest from tht.evidence.corpus.models import CanonicalDocument, CorpusManifest
def document(content: str) -> CanonicalDocument: def document(content: str) -> CanonicalDocument:
+1 -1
View File
@@ -4,7 +4,7 @@ from datetime import UTC, datetime, timedelta, timezone
import pytest import pytest
from pydantic import ValidationError from pydantic import ValidationError
from tht.corpus.models import CanonicalChunk, CanonicalDocument, CorpusManifest from tht.evidence.corpus.models import CanonicalChunk, CanonicalDocument, CorpusManifest
def document(source_uri: str = "https://host/a.md") -> CanonicalDocument: def document(source_uri: str = "https://host/a.md") -> CanonicalDocument:
+2 -2
View File
@@ -3,8 +3,8 @@ from datetime import UTC, datetime
import pytest import pytest
from tht.corpus.normalize import MAX_DOCUMENT_BYTES, PermanentNormalizationError, normalize from tht.evidence.corpus.normalize import MAX_DOCUMENT_BYTES, PermanentNormalizationError, normalize
from tht.ports.evidence import AcquiredDocument, SourceObject from tht.evidence.contracts import AcquiredDocument, SourceObject
def acquired(content: bytes, *, media_type: str = "text/markdown") -> AcquiredDocument: def acquired(content: bytes, *, media_type: str = "text/markdown") -> AcquiredDocument:
+9 -9
View File
@@ -2,11 +2,11 @@ from datetime import UTC, datetime, timedelta
import pytest import pytest
from tht.corpus.chunk import ChunkPolicy from tht.evidence.corpus.chunk import ChunkPolicy
from tht.corpus.pipeline import CorpusPipeline, PipelineError, PipelineResult from tht.evidence.corpus.pipeline import CorpusPipeline, PipelineError, PipelineResult
from tht.corpus.store import CorpusStore from tht.evidence.corpus.store import CorpusStore
from tht.corpus.models import CanonicalChunk, CanonicalDocument, CorpusManifest from tht.evidence.corpus.models import CanonicalChunk, CanonicalDocument, CorpusManifest
from tht.ports.evidence import AcquiredDocument, SourceObject from tht.evidence.contracts import AcquiredDocument, SourceObject
from tht.ports.vector import VectorCapabilities, VectorHealth from tht.ports.vector import VectorCapabilities, VectorHealth
@@ -273,7 +273,7 @@ def test_gc_preserves_vector_dependencies_of_retained_manifests(tmp_path):
def test_active_searcher_without_active_fails_closed_for_evidence(tmp_path): def test_active_searcher_without_active_fails_closed_for_evidence(tmp_path):
from types import SimpleNamespace from types import SimpleNamespace
from tht.search.evidence import active_searcher from tht.evidence.search import active_searcher
class Delegate: class Delegate:
def search(self, embedding, top_n=10, kinds=None, metadata_filter=None): def search(self, embedding, top_n=10, kinds=None, metadata_filter=None):
@@ -287,7 +287,7 @@ def test_active_searcher_without_active_fails_closed_for_evidence(tmp_path):
def test_active_searcher_splits_default_and_mixed_kinds_before_global_limit(tmp_path): def test_active_searcher_splits_default_and_mixed_kinds_before_global_limit(tmp_path):
from types import SimpleNamespace from types import SimpleNamespace
from tht.search.evidence import ActiveEvidenceSearcher from tht.evidence.search import ActiveEvidenceSearcher
store = CorpusStore(tmp_path / "corpus") store = CorpusStore(tmp_path / "corpus")
generation = store.stage( generation = store.stage(
@@ -319,7 +319,7 @@ def test_active_searcher_splits_default_and_mixed_kinds_before_global_limit(tmp_
def test_active_evidence_query_holds_lock_against_publish(tmp_path): def test_active_evidence_query_holds_lock_against_publish(tmp_path):
import threading import threading
from types import SimpleNamespace from types import SimpleNamespace
from tht.search.evidence import ActiveEvidenceSearcher from tht.evidence.search import ActiveEvidenceSearcher
first_pipeline = pipeline(tmp_path, Source([(item("one", "a"), "old")]), vectors=Vectors()) first_pipeline = pipeline(tmp_path, Source([(item("one", "a"), "old")]), vectors=Vectors())
first_pipeline.run() first_pipeline.run()
@@ -450,7 +450,7 @@ def test_gc_rejects_workspace_mismatch_without_deleting(tmp_path):
@pytest.mark.parametrize("kinds", [None, ["evidence", "memory"], ["memory"]]) @pytest.mark.parametrize("kinds", [None, ["evidence", "memory"], ["memory"]])
def test_active_search_rejects_workspace_mismatch_before_delegate(tmp_path, kinds): def test_active_search_rejects_workspace_mismatch_before_delegate(tmp_path, kinds):
from tht.search.evidence import ActiveEvidenceSearcher, CorpusWorkspaceMismatchError from tht.evidence.search import ActiveEvidenceSearcher, CorpusWorkspaceMismatchError
vectors = Vectors() vectors = Vectors()
owner = pipeline(tmp_path, Source([(item("one", "a"), "stable")]), vectors=vectors) owner = pipeline(tmp_path, Source([(item("one", "a"), "stable")]), vectors=vectors)
+5 -5
View File
@@ -1,8 +1,8 @@
import pytest import pytest
import os import os
from tht.corpus.models import CorpusManifest from tht.evidence.corpus.models import CorpusManifest
from tht.corpus.store import CorpusStore, UnsafeCorpusPath from tht.evidence.corpus.store import CorpusStore, UnsafeCorpusPath
def test_publish_switches_active_atomically_and_resolves_materialized_files(tmp_path): def test_publish_switches_active_atomically_and_resolves_materialized_files(tmp_path):
@@ -60,7 +60,7 @@ def test_publish_restores_previous_active_when_directory_fsync_fails_after_repla
def test_read_document_rejects_symlink_hardlink_and_hash_mismatch(tmp_path): def test_read_document_rejects_symlink_hardlink_and_hash_mismatch(tmp_path):
from tht.corpus.models import CanonicalDocument from tht.evidence.corpus.models import CanonicalDocument
content = "trusted" content = "trusted"
digest = "sha256:" + __import__("hashlib").sha256(content.encode()).hexdigest() digest = "sha256:" + __import__("hashlib").sha256(content.encode()).hexdigest()
@@ -111,7 +111,7 @@ def test_published_inventory_excludes_staged_and_invalid_newer_directories(tmp_p
def test_owned_copy_uses_validated_descriptor_bytes_when_source_is_replaced(tmp_path, monkeypatch): def test_owned_copy_uses_validated_descriptor_bytes_when_source_is_replaced(tmp_path, monkeypatch):
from tht.corpus.models import CanonicalDocument from tht.evidence.corpus.models import CanonicalDocument
import hashlib import hashlib
content = "active bytes" content = "active bytes"
@@ -140,7 +140,7 @@ def test_owned_copy_uses_validated_descriptor_bytes_when_source_is_replaced(tmp_
def test_materialized_snapshot_uses_identified_manifest_when_active_changes(tmp_path): def test_materialized_snapshot_uses_identified_manifest_when_active_changes(tmp_path):
from tht.corpus.models import CanonicalDocument from tht.evidence.corpus.models import CanonicalDocument
import hashlib import hashlib
def doc(content, fingerprint): def doc(content, fingerprint):
+13 -27
View File
@@ -1,12 +1,13 @@
from datetime import UTC, datetime from datetime import UTC, datetime
import hashlib import hashlib
import inspect import inspect
from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
import pytest import pytest
from tht.corpus.models import CanonicalDocument, CorpusManifest from tht.evidence.corpus.models import CanonicalDocument, CorpusManifest
from tht.corpus.store import CorpusStore from tht.evidence.corpus.store import CorpusStore
from tht.decisions import DecisionRecord from tht.decisions import DecisionRecord
from tht.evidence import ( from tht.evidence import (
acquire, acquire,
@@ -18,15 +19,12 @@ from tht.evidence import (
project_session, project_session,
resolve_citation, resolve_citation,
) )
from tht.ports.evidence import ( from tht.evidence.contracts import (
AcquiredDocument, AcquiredDocument,
EvidenceSourceError, EvidenceSourceError,
EvidenceSourceErrorCategory, EvidenceSourceErrorCategory,
SourceObject, SourceObject,
) )
from tht.search.evidence import active_searcher as legacy_active_searcher
from tht.search.evidence import resolve_evidence_file
from tht.session.artifacts import build_evidence_entries
from tht.session.models import Candidate, SchemaLinking from tht.session.models import Candidate, SchemaLinking
@@ -91,8 +89,6 @@ def test_acquisition_facade_preserves_classified_errors():
def test_source_factory_preserves_legacy_first_order_and_filesystem_configuration(tmp_path): def test_source_factory_preserves_legacy_first_order_and_filesystem_configuration(tmp_path):
from tht.adapters.factory import build_evidence_sources
legacy_root = tmp_path / "legacy" legacy_root = tmp_path / "legacy"
configured_root = tmp_path / "configured" configured_root = tmp_path / "configured"
(legacy_root / "evidence").mkdir(parents=True) (legacy_root / "evidence").mkdir(parents=True)
@@ -108,16 +104,14 @@ def test_source_factory_preserves_legacy_first_order_and_filesystem_configuratio
)], )],
)) ))
legacy = build_evidence_sources(cfg)
current = build_sources(cfg.evidence) current = build_sources(cfg.evidence)
assert [type(source) for source in current] == [type(source) for source in legacy]
assert [source.root for source in current] == [ assert [source.root for source in current] == [
(legacy_root / "evidence").resolve(), (legacy_root / "evidence").resolve(),
configured_root.resolve(), configured_root.resolve(),
] ]
assert current[1].patterns == legacy[1].patterns == ("*.md",) assert current[1].patterns == ("*.md",)
assert current[1].max_bytes == legacy[1].max_bytes == 1024 assert current[1].max_bytes == 1024
def test_preprocessing_factory_forwards_only_evidence_pipeline_dependencies(monkeypatch): def test_preprocessing_factory_forwards_only_evidence_pipeline_dependencies(monkeypatch):
@@ -127,7 +121,7 @@ def test_preprocessing_factory_forwards_only_evidence_pipeline_dependencies(monk
def __init__(self, **kwargs): def __init__(self, **kwargs):
captured.update(kwargs) captured.update(kwargs)
monkeypatch.setattr("tht.corpus.pipeline.CorpusPipeline", FakePipeline) monkeypatch.setattr("tht.evidence.preprocessing.CorpusPipeline", FakePipeline)
dependencies = { dependencies = {
"store": object(), "store": object(),
"sources": [object()], "sources": [object()],
@@ -176,18 +170,14 @@ class OrderedDelegate:
def test_search_facade_preserves_active_filtering_and_global_order(tmp_path): def test_search_facade_preserves_active_filtering_and_global_order(tmp_path):
cfg = _active_config(tmp_path) cfg = _active_config(tmp_path)
legacy_delegate = OrderedDelegate()
facade_delegate = OrderedDelegate() facade_delegate = OrderedDelegate()
legacy = legacy_active_searcher(
cfg, legacy_delegate, workspace_id="workspace-a",
).search([1.0], top_n=2, kinds=["evidence", "memory"])
current = active_searcher( current = active_searcher(
cfg, facade_delegate, workspace_id="workspace-a", cfg, facade_delegate, workspace_id="workspace-a",
).search([1.0], top_n=2, kinds=["evidence", "memory"]) ).search([1.0], top_n=2, kinds=["evidence", "memory"])
assert [hit.id for hit in current] == [hit.id for hit in legacy] == ["higher", "lower"] assert [hit.id for hit in current] == ["higher", "lower"]
assert facade_delegate.calls == legacy_delegate.calls assert facade_delegate.calls == [([1.0], 2, ["memory"], None)]
def test_retrieval_entries_preserve_hit_order_and_existing_projection_shape(): def test_retrieval_entries_preserve_hit_order_and_existing_projection_shape():
@@ -225,14 +215,12 @@ def test_citation_facade_matches_active_corpus_resolution(tmp_path):
store = _canonical_store(tmp_path / "corpus", "evi-used") store = _canonical_store(tmp_path / "corpus", "evi-used")
materialized = tmp_path / "materialized" materialized = tmp_path / "materialized"
legacy = resolve_evidence_file(
store, "evi-used", materialized_root=materialized,
)
current = resolve_citation( current = resolve_citation(
store, "evi-used", materialized_root=materialized, store, "evi-used", materialized_root=materialized,
) )
assert current == legacy assert current.endswith(".md")
assert Path(current).read_text(encoding="utf-8") == "# evi-used\n"
assert resolve_citation(store, "missing", materialized_root=materialized) == "" assert resolve_citation(store, "missing", materialized_root=materialized) == ""
@@ -245,7 +233,7 @@ def test_session_projection_routes_corpus_citations_through_the_facade(tmp_path,
calls.append((store.root, evidence_id, materialized_root)) calls.append((store.root, evidence_id, materialized_root))
return f"/materialized/{evidence_id}.md" return f"/materialized/{evidence_id}.md"
monkeypatch.setattr("tht.evidence.resolve_citation", fake_resolve) monkeypatch.setattr("tht.evidence.session.resolve_citation", fake_resolve)
linking = SchemaLinking( linking = SchemaLinking(
question="q", question="q",
candidates=[Candidate( candidates=[Candidate(
@@ -257,7 +245,7 @@ def test_session_projection_routes_corpus_citations_through_the_facade(tmp_path,
)], )],
) )
assert build_evidence_entries([], linking, evidence_root) == [{ assert project_session([], linking, evidence_root) == [{
"id": "evi-used", "id": "evi-used",
"file": "/materialized/evi-used.md", "file": "/materialized/evi-used.md",
"esito": "usata", "esito": "usata",
@@ -299,10 +287,8 @@ def test_session_projection_facade_preserves_outcome_precedence_and_order(tmp_pa
)], )],
) )
legacy = build_evidence_entries(decisions, linking, evidence_root)
current = project_session(decisions, linking, evidence_root) current = project_session(decisions, linking, evidence_root)
assert current == legacy
assert [(row["id"], row["esito"], row["decision_seq"]) for row in current] == [ assert [(row["id"], row["esito"], row["decision_seq"]) for row in current] == [
("used", "usata", 17), ("used", "usata", 17),
("accepted", "accettata", 21), ("accepted", "accettata", 21),
+40
View File
@@ -0,0 +1,40 @@
from pathlib import Path
HARNESS_ROOT = Path(__file__).resolve().parents[1]
LEGACY_PATHS = (
"tht/ports/evidence.py",
"tht/adapters/evidence",
"tht/corpus",
"tht/search/evidence.py",
"tht/session/artifacts.py",
)
LEGACY_REFERENCES = (
"tht.ports.evidence",
"tht.adapters.evidence",
"tht.corpus",
"tht.search.evidence",
"tht.session.artifacts",
"build_evidence_sources",
"build_evidence_entries",
)
def test_replaced_evidence_layout_is_removed():
remaining = [path for path in LEGACY_PATHS if (HARNESS_ROOT / path).exists()]
assert remaining == []
def test_production_and_tests_use_only_the_evidence_module():
offenders = []
for root in (HARNESS_ROOT / "tht", HARNESS_ROOT / "tests"):
for path in root.rglob("*.py"):
if path == Path(__file__).resolve() or "__pycache__" in path.parts:
continue
contents = path.read_text(encoding="utf-8")
matched = [reference for reference in LEGACY_REFERENCES if reference in contents]
if matched:
offenders.append((path.relative_to(HARNESS_ROOT).as_posix(), matched))
assert offenders == []
+1 -1
View File
@@ -3,7 +3,7 @@ from datetime import UTC, datetime, timedelta, timezone
import pytest import pytest
from pydantic import ValidationError from pydantic import ValidationError
from tht.ports.evidence import ( from tht.evidence.contracts import (
AcquiredDocument, AcquiredDocument,
EvidenceSource, EvidenceSource,
EvidenceSourceError, EvidenceSourceError,
@@ -2,8 +2,8 @@ import os
import pytest import pytest
from tht.adapters.evidence import FilesystemEvidenceSource from tht.evidence.adapters import FilesystemEvidenceSource
from tht.ports.evidence import EvidenceSourceError from tht.evidence.contracts import EvidenceSourceError
def test_filesystem_discovery_is_stable_and_acquisition_is_bounded(tmp_path): def test_filesystem_discovery_is_stable_and_acquisition_is_bounded(tmp_path):
+2 -2
View File
@@ -4,8 +4,8 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import pytest import pytest
from tht.adapters.evidence import HttpManifestEvidenceSource from tht.evidence.adapters import HttpManifestEvidenceSource
from tht.ports.evidence import EvidenceSourceError from tht.evidence.contracts import EvidenceSourceError
class Handler(BaseHTTPRequestHandler): class Handler(BaseHTTPRequestHandler):
@@ -6,8 +6,8 @@ import yaml
from pydantic import SecretStr from pydantic import SecretStr
from typer.testing import CliRunner from typer.testing import CliRunner
from tht.adapters.evidence import HttpManifestEvidenceSource from tht.evidence.adapters import HttpManifestEvidenceSource
from tht.adapters.factory import build_evidence_sources from tht.evidence import build_sources
from tht.cli import app from tht.cli import app
from tht.config import ConfigError, load_config from tht.config import ConfigError, load_config
@@ -108,7 +108,7 @@ def test_signed_http_file_resolves_in_memory_and_preserves_provenance_order(tmp_
assert_no_canaries(repr(cfg)) assert_no_canaries(repr(cfg))
assert_no_canaries(cfg.model_dump_json()) assert_no_canaries(cfg.model_dump_json())
adapter = build_evidence_sources(cfg)[0] adapter = build_sources(cfg.evidence)[0]
assert isinstance(adapter, HttpManifestEvidenceSource) assert isinstance(adapter, HttpManifestEvidenceSource)
assert_no_canaries(repr(adapter)) assert_no_canaries(repr(adapter))
+15 -15
View File
@@ -2,7 +2,7 @@ from datetime import UTC, datetime
import pytest import pytest
from tht.ports.evidence import EvidenceSourceError from tht.evidence.contracts import EvidenceSourceError
class Body: class Body:
@@ -28,7 +28,7 @@ class Client:
def test_s3_canonical_uri_version_fingerprint_and_closed_body(): def test_s3_canonical_uri_version_fingerprint_and_closed_body():
from tht.adapters.evidence.s3 import S3EvidenceSource from tht.evidence.adapters.s3 import S3EvidenceSource
client = Client() client = Client()
source = S3EvidenceSource(bucket="evidence", prefix="clinical/", client=client) source = S3EvidenceSource(bucket="evidence", prefix="clinical/", client=client)
item = next(iter(source.discover())) item = next(iter(source.discover()))
@@ -39,14 +39,14 @@ def test_s3_canonical_uri_version_fingerprint_and_closed_body():
def test_s3_etag_fallback_and_bounds(): def test_s3_etag_fallback_and_bounds():
from tht.adapters.evidence.s3 import S3EvidenceSource from tht.evidence.adapters.s3 import S3EvidenceSource
client = Client() client = Client()
with pytest.raises(ValueError): with pytest.raises(ValueError):
S3EvidenceSource(bucket="evidence", client=client, max_objects=0) S3EvidenceSource(bucket="evidence", client=client, max_objects=0)
def test_s3_rejects_private_or_insecure_endpoint_without_explicit_opt_in(): def test_s3_rejects_private_or_insecure_endpoint_without_explicit_opt_in():
from tht.adapters.evidence.s3 import S3EvidenceSource from tht.evidence.adapters.s3 import S3EvidenceSource
with pytest.raises(ValueError, match="trusted"): with pytest.raises(ValueError, match="trusted"):
S3EvidenceSource(bucket="evidence", endpoint_url="https://127.0.0.1:9000", client=Client()) S3EvidenceSource(bucket="evidence", endpoint_url="https://127.0.0.1:9000", client=Client())
with pytest.raises(ValueError, match="HTTPS"): with pytest.raises(ValueError, match="HTTPS"):
@@ -59,20 +59,20 @@ def test_s3_rejects_private_or_insecure_endpoint_without_explicit_opt_in():
@pytest.mark.parametrize("bucket", ["UPPER", "bad_bucket", "-start", "end-", "a..b"]) @pytest.mark.parametrize("bucket", ["UPPER", "bad_bucket", "-start", "end-", "a..b"])
def test_s3_rejects_invalid_bucket_names(bucket): def test_s3_rejects_invalid_bucket_names(bucket):
from tht.adapters.evidence.s3 import S3EvidenceSource from tht.evidence.adapters.s3 import S3EvidenceSource
with pytest.raises(ValueError, match="bucket"): with pytest.raises(ValueError, match="bucket"):
S3EvidenceSource(bucket=bucket, client=Client()) S3EvidenceSource(bucket=bucket, client=Client())
@pytest.mark.parametrize("bucket", ["127.0.0.1", "192.168.1.1"]) @pytest.mark.parametrize("bucket", ["127.0.0.1", "192.168.1.1"])
def test_s3_rejects_ip_shaped_bucket(bucket): def test_s3_rejects_ip_shaped_bucket(bucket):
from tht.adapters.evidence.s3 import S3EvidenceSource from tht.evidence.adapters.s3 import S3EvidenceSource
with pytest.raises(ValueError, match="bucket"): with pytest.raises(ValueError, match="bucket"):
S3EvidenceSource(bucket=bucket, client=Client()) S3EvidenceSource(bucket=bucket, client=Client())
def test_s3_rejects_endpoint_query_path_fragment_and_untrusted_custom_host(): def test_s3_rejects_endpoint_query_path_fragment_and_untrusted_custom_host():
from tht.adapters.evidence.s3 import S3EvidenceSource from tht.evidence.adapters.s3 import S3EvidenceSource
for endpoint in ("https://s3.example.test/path", "https://s3.example.test/?x=1", for endpoint in ("https://s3.example.test/path", "https://s3.example.test/?x=1",
"https://s3.example.test/#x"): "https://s3.example.test/#x"):
with pytest.raises(ValueError, match="root"): with pytest.raises(ValueError, match="root"):
@@ -83,7 +83,7 @@ def test_s3_rejects_endpoint_query_path_fragment_and_untrusted_custom_host():
def test_s3_rejects_out_of_prefix_key_and_missing_validator(): def test_s3_rejects_out_of_prefix_key_and_missing_validator():
from tht.adapters.evidence.s3 import S3EvidenceSource from tht.evidence.adapters.s3 import S3EvidenceSource
client = Client() client = Client()
client.list_objects_v2 = lambda **kwargs: {"Contents": [{"Key": "other/a.md", "ETag": '"x"'}]} client.list_objects_v2 = lambda **kwargs: {"Contents": [{"Key": "other/a.md", "ETag": '"x"'}]}
with pytest.raises(EvidenceSourceError): with pytest.raises(EvidenceSourceError):
@@ -94,7 +94,7 @@ def test_s3_rejects_out_of_prefix_key_and_missing_validator():
def test_s3_rejects_leading_slash_prefix_empty_and_control_keys(): def test_s3_rejects_leading_slash_prefix_empty_and_control_keys():
from tht.adapters.evidence.s3 import S3EvidenceSource from tht.evidence.adapters.s3 import S3EvidenceSource
with pytest.raises(ValueError, match="prefix"): with pytest.raises(ValueError, match="prefix"):
S3EvidenceSource(bucket="evidence", prefix="/clinical", client=Client()) S3EvidenceSource(bucket="evidence", prefix="/clinical", client=Client())
for key in ("", "clinical/a\x00.md", "clinical/a\x7f.md"): for key in ("", "clinical/a\x00.md", "clinical/a\x7f.md"):
@@ -106,7 +106,7 @@ def test_s3_rejects_leading_slash_prefix_empty_and_control_keys():
@pytest.mark.parametrize("prefix", ["/bad", "x" * 1025, "bad\x00prefix", "bad\x7fprefix"]) @pytest.mark.parametrize("prefix", ["/bad", "x" * 1025, "bad\x00prefix", "bad\x7fprefix"])
def test_s3_rejects_invalid_prefix_before_client_request(prefix): def test_s3_rejects_invalid_prefix_before_client_request(prefix):
from tht.adapters.evidence.s3 import S3EvidenceSource from tht.evidence.adapters.s3 import S3EvidenceSource
client = Client() client = Client()
with pytest.raises(ValueError, match="prefix"): with pytest.raises(ValueError, match="prefix"):
S3EvidenceSource(bucket="evidence", prefix=prefix, client=client) S3EvidenceSource(bucket="evidence", prefix=prefix, client=client)
@@ -114,7 +114,7 @@ def test_s3_rejects_invalid_prefix_before_client_request(prefix):
def test_s3_hard_page_limit_never_requests_page_max_plus_one(): def test_s3_hard_page_limit_never_requests_page_max_plus_one():
from tht.adapters.evidence.s3 import S3EvidenceSource from tht.evidence.adapters.s3 import S3EvidenceSource
client = Client() client = Client()
def listing(**kwargs): def listing(**kwargs):
client.list_calls += 1 client.list_calls += 1
@@ -128,7 +128,7 @@ def test_s3_hard_page_limit_never_requests_page_max_plus_one():
def test_s3_acquire_rejects_exact_etag_drift_and_closes_body(): def test_s3_acquire_rejects_exact_etag_drift_and_closes_body():
from tht.adapters.evidence.s3 import S3EvidenceSource from tht.evidence.adapters.s3 import S3EvidenceSource
client = Client() client = Client()
source = S3EvidenceSource(bucket="evidence", client=client) source = S3EvidenceSource(bucket="evidence", client=client)
item = next(iter(source.discover())) item = next(iter(source.discover()))
@@ -140,7 +140,7 @@ def test_s3_acquire_rejects_exact_etag_drift_and_closes_body():
def test_s3_acquire_rejects_forged_reconstructed_item_before_get(): def test_s3_acquire_rejects_forged_reconstructed_item_before_get():
from tht.adapters.evidence.s3 import S3EvidenceSource from tht.evidence.adapters.s3 import S3EvidenceSource
client = Client() client = Client()
source = S3EvidenceSource(bucket="evidence", client=client) source = S3EvidenceSource(bucket="evidence", client=client)
item = next(iter(source.discover())) item = next(iter(source.discover()))
@@ -153,14 +153,14 @@ def test_s3_acquire_rejects_forged_reconstructed_item_before_get():
@pytest.mark.parametrize("host", ["127.0.0.1", "10.0.0.1", "169.254.1.1", "0.0.0.0", @pytest.mark.parametrize("host", ["127.0.0.1", "10.0.0.1", "169.254.1.1", "0.0.0.0",
"[::1]", "[fe80::1]", "[::]"]) "[::1]", "[fe80::1]", "[::]"])
def test_s3_literal_non_global_endpoint_requires_private_opt_in(host): def test_s3_literal_non_global_endpoint_requires_private_opt_in(host):
from tht.adapters.evidence.s3 import S3EvidenceSource from tht.evidence.adapters.s3 import S3EvidenceSource
with pytest.raises(ValueError, match="private"): with pytest.raises(ValueError, match="private"):
S3EvidenceSource(bucket="evidence", endpoint_url=f"https://{host}:9000", S3EvidenceSource(bucket="evidence", endpoint_url=f"https://{host}:9000",
trusted_endpoint=True, client=Client()) trusted_endpoint=True, client=Client())
def test_s3_size_limit_closes_body(): def test_s3_size_limit_closes_body():
from tht.adapters.evidence.s3 import S3EvidenceSource from tht.evidence.adapters.s3 import S3EvidenceSource
client = Client() client = Client()
source = S3EvidenceSource(bucket="evidence", client=client, max_bytes=4) source = S3EvidenceSource(bucket="evidence", client=client, max_bytes=4)
item = next(iter(source.discover())) item = next(iter(source.discover()))
@@ -6,10 +6,10 @@ from datetime import UTC, datetime
from tht.adapters.vector.qdrant import point_id from tht.adapters.vector.qdrant import point_id
from tht.cli.vector_cmd import sync_canonical_records from tht.cli.vector_cmd import sync_canonical_records
from tht.corpus.chunk import ChunkPolicy from tht.evidence.corpus.chunk import ChunkPolicy
from tht.corpus.models import CanonicalChunk from tht.evidence.corpus.models import CanonicalChunk
from tht.corpus.pipeline import CorpusPipeline from tht.evidence.corpus.pipeline import CorpusPipeline
from tht.corpus.store import CorpusStore from tht.evidence.corpus.store import CorpusStore
from tht.memory import MemoryRecord, save_one_memory from tht.memory import MemoryRecord, save_one_memory
from tht.mschema.models import ( from tht.mschema.models import (
Annotations, Annotations,
@@ -6,8 +6,8 @@ import hashlib
import pytest import pytest
from tht.corpus.models import CanonicalDocument, CorpusManifest from tht.evidence.corpus.models import CanonicalDocument, CorpusManifest
from tht.corpus.store import CorpusStore from tht.evidence.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.evidence import project_session
from tht.phase import current_phase, effective_decisions from tht.phase import current_phase, effective_decisions
@@ -1,7 +0,0 @@
"""Evidence source adapter implementations."""
from tht.adapters.evidence.filesystem import FilesystemEvidenceSource
from tht.adapters.evidence.http import HttpManifestEvidenceSource
from tht.adapters.evidence.s3 import S3EvidenceSource
__all__ = ["FilesystemEvidenceSource", "HttpManifestEvidenceSource", "S3EvidenceSource"]
+1 -8
View File
@@ -42,11 +42,4 @@ def build_vector_store(cfg: Config, *, require_write: bool = False) -> VectorSto
raise ConfigError(f"Adapter vector non supportato: {other}") raise ConfigError(f"Adapter vector non supportato: {other}")
def build_evidence_sources(cfg: Config): __all__ = ["build_dwh", "build_vector_store"]
"""Compatibility shim for callers not yet migrated to ``tht.evidence``."""
from tht.evidence import build_sources
return build_sources(cfg.evidence)
__all__ = ["build_dwh", "build_evidence_sources", "build_vector_store"]
+4 -4
View File
@@ -96,8 +96,8 @@ def run_from_config(config: Path, *, dry_run: bool = False, resume: str | None =
from tht.adapters.factory import build_vector_store from tht.adapters.factory import build_vector_store
from tht.cli.schema_cmd import _load_config_or_exit from tht.cli.schema_cmd import _load_config_or_exit
from tht.cli.vector_cmd import make_embedder from tht.cli.vector_cmd import make_embedder
from tht.corpus.chunk import ChunkPolicy from tht.evidence.corpus.chunk import ChunkPolicy
from tht.corpus.store import CorpusStore from tht.evidence.corpus.store import CorpusStore
from tht.evidence import build_preprocessing_pipeline, build_sources from tht.evidence import build_preprocessing_pipeline, build_sources
cfg = _load_config_or_exit(config) cfg = _load_config_or_exit(config)
@@ -130,8 +130,8 @@ def gc_from_config(config: Path, *, dry_run: bool = False):
from tht.adapters.factory import build_vector_store from tht.adapters.factory import build_vector_store
from tht.cli.schema_cmd import _load_config_or_exit from tht.cli.schema_cmd import _load_config_or_exit
from tht.cli.vector_cmd import make_embedder from tht.cli.vector_cmd import make_embedder
from tht.corpus.chunk import ChunkPolicy from tht.evidence.corpus.chunk import ChunkPolicy
from tht.corpus.store import CorpusStore from tht.evidence.corpus.store import CorpusStore
from tht.evidence import build_preprocessing_pipeline, build_sources from tht.evidence import build_preprocessing_pipeline, build_sources
cfg = _load_config_or_exit(config) cfg = _load_config_or_exit(config)
-1
View File
@@ -1 +0,0 @@
"""Canonical, transport-independent Evidence corpus."""
+14 -149
View File
@@ -1,14 +1,4 @@
"""Cohesive public entrypoint for Evidence domain capabilities. """Cohesive public entrypoint for Evidence domain capabilities."""
The implementation is introduced beside the legacy module layout so production callers can
migrate one path at a time. These functions deliberately preserve the existing objects, ordering,
and exceptions; they do not define a cross-domain service protocol.
"""
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
from tht.evidence.acquisition import acquire, discover from tht.evidence.acquisition import acquire, discover
from tht.evidence.contracts import ( from tht.evidence.contracts import (
@@ -22,145 +12,17 @@ from tht.evidence.contracts import (
validate_namespaced_value, validate_namespaced_value,
validate_safe_metadata, validate_safe_metadata,
) )
from tht.evidence.preprocessing import EvidenceEmbedder, build_preprocessing_pipeline
if TYPE_CHECKING: from tht.evidence.search import (
from tht.config import EvidenceSourcesConfig ActiveEvidenceSearcher,
from tht.corpus.chunk import ChunkPolicy CorpusWorkspaceMismatchError,
from tht.corpus.pipeline import CorpusPipeline active_searcher,
from tht.corpus.store import CorpusStore build_retrieval_entries,
from tht.decisions import DecisionRecord resolve_citation,
from tht.evidence.preprocessing import EvidenceEmbedder validate_corpus_workspace,
from tht.ports.vector import VectorStore
from tht.search.evidence import ActiveEvidenceSearcher
from tht.session.models import SchemaLinking
def build_sources(evidence: "EvidenceSourcesConfig | None") -> list[EvidenceSource]:
"""Build configured Evidence source adapters in the existing deterministic order."""
from tht.evidence.sources import build_sources as build_configured_sources
return build_configured_sources(evidence)
def build_preprocessing_pipeline(
*,
store: "CorpusStore",
sources: list[EvidenceSource],
embedder: "EvidenceEmbedder",
vector_store: "VectorStore",
embedding_model: str,
embedding_dimensions: int,
chunk_policy: "ChunkPolicy",
pipeline_version: str,
retain_published_generations: int = 3,
workspace_id: str | None = None,
) -> "CorpusPipeline":
"""Construct the Evidence preprocessing use case from core-owned infrastructure."""
from tht.evidence.preprocessing import build_preprocessing_pipeline as build_pipeline
return build_pipeline(
store=store,
sources=sources,
embedder=embedder,
vector_store=vector_store,
embedding_model=embedding_model,
embedding_dimensions=embedding_dimensions,
chunk_policy=chunk_policy,
pipeline_version=pipeline_version,
retain_published_generations=retain_published_generations,
workspace_id=workspace_id,
) )
from tht.evidence.session import project_session
from tht.evidence.sources import build_sources
def active_searcher(
cfg,
delegate,
*,
workspace_id: str | None = None,
) -> ActiveEvidenceSearcher:
"""Bind vector search to the atomically ACTIVE Evidence corpus generation."""
from tht.search.evidence import active_searcher as legacy_active_searcher
return legacy_active_searcher(cfg, delegate, workspace_id=workspace_id)
def validate_corpus_workspace(cfg, workspace_id: str) -> None:
"""Validate persisted Evidence corpus ownership before runtime retrieval setup."""
from tht.search.evidence import validate_corpus_workspace as legacy_validate
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(
store: "CorpusStore",
evidence_id: str,
*,
materialized_root: Path | None = None,
) -> str:
"""Resolve an Evidence identifier to its immutable ACTIVE materialization."""
from tht.search.evidence import resolve_evidence_file
return resolve_evidence_file(
store,
evidence_id,
materialized_root=materialized_root,
)
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(
decisions: list["DecisionRecord"],
linking: "SchemaLinking",
evidence_root: Path,
) -> list[dict]:
"""Project cited and reviewed Evidence into the existing session artifact shape."""
entries: dict[str, dict] = {}
for candidate in linking.candidates:
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__ = [
@@ -168,7 +30,10 @@ __all__ = [
"EvidenceSource", "EvidenceSource",
"EvidenceSourceError", "EvidenceSourceError",
"EvidenceSourceErrorCategory", "EvidenceSourceErrorCategory",
"EvidenceEmbedder",
"SourceObject", "SourceObject",
"ActiveEvidenceSearcher",
"CorpusWorkspaceMismatchError",
"acquire", "acquire",
"active_searcher", "active_searcher",
"build_preprocessing_pipeline", "build_preprocessing_pipeline",
@@ -0,0 +1,7 @@
"""Evidence-owned source adapter implementations."""
from tht.evidence.adapters.filesystem import FilesystemEvidenceSource
from tht.evidence.adapters.http import HttpManifestEvidenceSource
from tht.evidence.adapters.s3 import S3EvidenceSource
__all__ = ["FilesystemEvidenceSource", "HttpManifestEvidenceSource", "S3EvidenceSource"]
@@ -1,4 +1,4 @@
"""Contained, race-safe filesystem Evidence source.""" """Evidence-owned, race-safe filesystem source."""
import hashlib import hashlib
import os import os
@@ -1,4 +1,4 @@
"""Explicit-manifest HTTP Evidence source with SSRF-safe bounded acquisition.""" """Evidence-owned explicit-manifest HTTP source with SSRF-safe bounded acquisition."""
import hashlib import hashlib
import ipaddress import ipaddress
@@ -1,4 +1,4 @@
"""Bounded S3-compatible Evidence source using the supported boto3 client.""" """Evidence-owned bounded S3-compatible source using the supported boto3 client."""
import hashlib import hashlib
import ipaddress import ipaddress
+1
View File
@@ -0,0 +1 @@
"""Evidence-owned canonical, transport-independent corpus."""
@@ -1,11 +1,11 @@
"""Versioned deterministic chunking for canonical corpus documents.""" """Evidence-owned deterministic chunking for canonical corpus documents."""
import hashlib import hashlib
import json import json
import re import re
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from tht.corpus.models import CanonicalChunk, CanonicalDocument from tht.evidence.corpus.models import CanonicalChunk, CanonicalDocument
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
@@ -1,4 +1,4 @@
"""Immutable records emitted by the Evidence preprocessing pipeline.""" """Evidence-owned immutable records emitted by preprocessing."""
import hashlib import hashlib
import re import re
@@ -1,4 +1,4 @@
"""Pure, deterministic conversion of acquired bytes into canonical text.""" """Evidence-owned deterministic conversion of acquired bytes into canonical text."""
import hashlib import hashlib
import re import re
@@ -10,7 +10,7 @@ from pydantic import JsonValue, TypeAdapter, ValidationError
from yaml.events import AliasEvent from yaml.events import AliasEvent
from yaml.nodes import MappingNode from yaml.nodes import MappingNode
from tht.corpus.models import CanonicalDocument from tht.evidence.corpus.models import CanonicalDocument
from tht.evidence.contracts import AcquiredDocument, canonical_provenance_uri from tht.evidence.contracts import AcquiredDocument, canonical_provenance_uri
@@ -1,4 +1,4 @@
"""Incremental Evidence preprocessing with generation-isolated vector writes.""" """Evidence-owned incremental preprocessing with generation-isolated vector writes."""
from __future__ import annotations from __future__ import annotations
@@ -11,10 +11,10 @@ from dataclasses import asdict, dataclass, field
from datetime import UTC from datetime import UTC
from pathlib import Path from pathlib import Path
from tht.corpus.chunk import ChunkPolicy, chunk from tht.evidence.corpus.chunk import ChunkPolicy, chunk
from tht.corpus.models import CanonicalChunk, CanonicalDocument, CorpusManifest from tht.evidence.corpus.models import CanonicalChunk, CanonicalDocument, CorpusManifest
from tht.corpus.normalize import normalize from tht.evidence.corpus.normalize import normalize
from tht.corpus.store import CorpusStore from tht.evidence.corpus.store import CorpusStore
import tht.evidence.acquisition as evidence_acquisition import tht.evidence.acquisition as evidence_acquisition
from tht.evidence.contracts import EvidenceSource, SourceObject, canonical_provenance_uri from tht.evidence.contracts import EvidenceSource, SourceObject, canonical_provenance_uri
from tht.ports.vector import VectorStore, VectorWriteRecord from tht.ports.vector import VectorStore, VectorWriteRecord
@@ -1,4 +1,4 @@
"""Durable immutable corpus generations and an atomic ACTIVE pointer.""" """Evidence-owned immutable corpus generations and an atomic ACTIVE pointer."""
from __future__ import annotations from __future__ import annotations
@@ -15,7 +15,7 @@ from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from contextlib import contextmanager from contextlib import contextmanager
from tht.corpus.models import CorpusManifest from tht.evidence.corpus.models import CorpusManifest
_GENERATION = re.compile(r"^gen:[0-9a-f]{32}$") _GENERATION = re.compile(r"^gen:[0-9a-f]{32}$")
+3 -3
View File
@@ -2,9 +2,9 @@
from typing import Protocol from typing import Protocol
from tht.corpus.chunk import ChunkPolicy from tht.evidence.corpus.chunk import ChunkPolicy
from tht.corpus.pipeline import CorpusPipeline from tht.evidence.corpus.pipeline import CorpusPipeline
from tht.corpus.store import CorpusStore from tht.evidence.corpus.store import CorpusStore
from tht.evidence.contracts import EvidenceSource from tht.evidence.contracts import EvidenceSource
from tht.ports.vector import VectorStore from tht.ports.vector import VectorStore
@@ -1,8 +1,8 @@
"""Runtime Evidence lookup bound to the atomically active corpus generation.""" """Evidence-owned runtime lookup bound to the atomically active corpus generation."""
import re import re
from tht.corpus.store import CorpusStore from tht.evidence.corpus.store import CorpusStore
class CorpusWorkspaceMismatchError(RuntimeError): class CorpusWorkspaceMismatchError(RuntimeError):
@@ -96,7 +96,7 @@ def validate_corpus_workspace(cfg, workspace_id: str) -> None:
) )
def resolve_evidence_file( def resolve_citation(
store: CorpusStore, evidence_id: str, *, materialized_root=None, store: CorpusStore, evidence_id: str, *, materialized_root=None,
) -> str: ) -> str:
with store.writer_lock(): with store.writer_lock():
@@ -114,3 +114,25 @@ def resolve_evidence_file(
) )
return str(path) if path else "" return str(path) if path else ""
return "" return ""
def build_retrieval_entries(results, *, excerpt_chars: int) -> list[dict]:
"""Project ordered Evidence search hits into the retrieval-pack shape."""
return [
{
"title": result.label,
"status": result.status,
"excerpt": result.content[:excerpt_chars],
}
for result in results
]
__all__ = [
"ActiveEvidenceSearcher",
"CorpusWorkspaceMismatchError",
"active_searcher",
"build_retrieval_entries",
"resolve_citation",
"validate_corpus_workspace",
]
+59
View File
@@ -0,0 +1,59 @@
"""Evidence-specific projection into persisted session artifacts."""
from pathlib import Path
from typing import TYPE_CHECKING
from tht.evidence.corpus.store import CorpusStore
from tht.evidence.search import resolve_citation
if TYPE_CHECKING:
from tht.decisions import DecisionRecord
from tht.session.models import SchemaLinking
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():
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(
decisions: list["DecisionRecord"],
linking: "SchemaLinking",
evidence_root: Path,
) -> list[dict]:
"""Project cited and reviewed Evidence into the session artifact shape."""
entries: dict[str, dict] = {}
for candidate in linking.candidates:
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__ = ["project_session"]
+11 -3
View File
@@ -1,10 +1,16 @@
"""Construction of configured Evidence source adapters.""" """Construction of configured Evidence source adapters."""
from tht.adapters.evidence import FilesystemEvidenceSource, HttpManifestEvidenceSource from __future__ import annotations
from tht.adapters.evidence.s3 import S3EvidenceSource
from tht.config import ConfigError, EvidenceSourcesConfig from typing import TYPE_CHECKING
from tht.evidence.adapters import FilesystemEvidenceSource, HttpManifestEvidenceSource
from tht.evidence.adapters.s3 import S3EvidenceSource
from tht.evidence.contracts import EvidenceSource from tht.evidence.contracts import EvidenceSource
if TYPE_CHECKING:
from tht.config import EvidenceSourcesConfig
def build_sources(evidence: EvidenceSourcesConfig | None) -> list[EvidenceSource]: def build_sources(evidence: EvidenceSourcesConfig | None) -> list[EvidenceSource]:
"""Build configured Evidence adapters in the existing deterministic order.""" """Build configured Evidence adapters in the existing deterministic order."""
@@ -59,6 +65,8 @@ def build_sources(evidence: EvidenceSourcesConfig | None) -> list[EvidenceSource
) )
) )
case other: # pragma: no cover - Pydantic rejects unsupported discriminators. case other: # pragma: no cover - Pydantic rejects unsupported discriminators.
from tht.config import ConfigError
raise ConfigError(f"Adapter evidence non supportato: {other}") raise ConfigError(f"Adapter evidence non supportato: {other}")
return sources return sources
-31
View File
@@ -1,31 +0,0 @@
"""Legacy import path for Evidence contracts.
New production code imports the leaf contracts from ``tht.evidence.contracts``. This one-way
compatibility shim remains only until the old Evidence layout is removed.
"""
from tht.evidence.contracts import (
AcquiredDocument,
EvidenceSource,
EvidenceSourceError,
EvidenceSourceErrorCategory,
SourceObject,
canonical_provenance_uri,
normalize_aware_datetime,
validate_canonical_uri,
validate_namespaced_value,
validate_safe_metadata,
)
__all__ = [
"AcquiredDocument",
"EvidenceSource",
"EvidenceSourceError",
"EvidenceSourceErrorCategory",
"SourceObject",
"canonical_provenance_uri",
"normalize_aware_datetime",
"validate_canonical_uri",
"validate_namespaced_value",
"validate_safe_metadata",
]
-17
View File
@@ -1,17 +0,0 @@
"""Legacy session-artifact entrypoints retained during the Evidence migration."""
from pathlib import Path
from tht.decisions import DecisionRecord
from tht.session.models import SchemaLinking
def build_evidence_entries(
decisions: list[DecisionRecord],
linking: SchemaLinking,
evidence_root: Path,
) -> list[dict]:
"""Compatibility shim for callers not yet migrated to ``tht.evidence``."""
from tht.evidence import project_session
return project_session(decisions, linking, evidence_root)