diff --git a/harness/tests/test_config_resources.py b/harness/tests/test_config_resources.py index d20853cc..ad72015c 100644 --- a/harness/tests/test_config_resources.py +++ b/harness/tests/test_config_resources.py @@ -1,7 +1,7 @@ import pytest -from tht.adapters.evidence import FilesystemEvidenceSource, HttpManifestEvidenceSource -from tht.adapters.factory import build_evidence_sources +from tht.evidence.adapters import FilesystemEvidenceSource, HttpManifestEvidenceSource +from tht.evidence import build_sources from tht.config import ( ConfigError, PgvectorDirectConfig, @@ -369,7 +369,7 @@ evidence: assert "example.test" not in repr(cfg.evidence) assert "example.test" not in cfg.evidence.model_dump_json() 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[1], HttpManifestEvidenceSource) assert "example.test" not in repr(sources[1]) @@ -381,7 +381,7 @@ evidence: source_root: {tmp_path} 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 legacy_source.root == (tmp_path / "curated").resolve() diff --git a/harness/tests/test_corpus_chunk.py b/harness/tests/test_corpus_chunk.py index 341e13bb..804b95d2 100644 --- a/harness/tests/test_corpus_chunk.py +++ b/harness/tests/test_corpus_chunk.py @@ -2,8 +2,8 @@ import hashlib import pytest -from tht.corpus.chunk import ChunkPolicy, chunk -from tht.corpus.models import CanonicalDocument, CorpusManifest +from tht.evidence.corpus.chunk import ChunkPolicy, chunk +from tht.evidence.corpus.models import CanonicalDocument, CorpusManifest def document(content: str) -> CanonicalDocument: diff --git a/harness/tests/test_corpus_models.py b/harness/tests/test_corpus_models.py index 0e9ee673..a2c053bb 100644 --- a/harness/tests/test_corpus_models.py +++ b/harness/tests/test_corpus_models.py @@ -4,7 +4,7 @@ from datetime import UTC, datetime, timedelta, timezone import pytest 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: diff --git a/harness/tests/test_corpus_normalize.py b/harness/tests/test_corpus_normalize.py index 96a115e6..3b2165d1 100644 --- a/harness/tests/test_corpus_normalize.py +++ b/harness/tests/test_corpus_normalize.py @@ -3,8 +3,8 @@ from datetime import UTC, datetime import pytest -from tht.corpus.normalize import MAX_DOCUMENT_BYTES, PermanentNormalizationError, normalize -from tht.ports.evidence import AcquiredDocument, SourceObject +from tht.evidence.corpus.normalize import MAX_DOCUMENT_BYTES, PermanentNormalizationError, normalize +from tht.evidence.contracts import AcquiredDocument, SourceObject def acquired(content: bytes, *, media_type: str = "text/markdown") -> AcquiredDocument: diff --git a/harness/tests/test_corpus_pipeline.py b/harness/tests/test_corpus_pipeline.py index 572938ef..5edd3891 100644 --- a/harness/tests/test_corpus_pipeline.py +++ b/harness/tests/test_corpus_pipeline.py @@ -2,11 +2,11 @@ from datetime import UTC, datetime, timedelta import pytest -from tht.corpus.chunk import ChunkPolicy -from tht.corpus.pipeline import CorpusPipeline, PipelineError, PipelineResult -from tht.corpus.store import CorpusStore -from tht.corpus.models import CanonicalChunk, CanonicalDocument, CorpusManifest -from tht.ports.evidence import AcquiredDocument, SourceObject +from tht.evidence.corpus.chunk import ChunkPolicy +from tht.evidence.corpus.pipeline import CorpusPipeline, PipelineError, PipelineResult +from tht.evidence.corpus.store import CorpusStore +from tht.evidence.corpus.models import CanonicalChunk, CanonicalDocument, CorpusManifest +from tht.evidence.contracts import AcquiredDocument, SourceObject 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): from types import SimpleNamespace - from tht.search.evidence import active_searcher + from tht.evidence.search import active_searcher class Delegate: 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): from types import SimpleNamespace - from tht.search.evidence import ActiveEvidenceSearcher + from tht.evidence.search import ActiveEvidenceSearcher store = CorpusStore(tmp_path / "corpus") 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): import threading 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.run() @@ -450,7 +450,7 @@ def test_gc_rejects_workspace_mismatch_without_deleting(tmp_path): @pytest.mark.parametrize("kinds", [None, ["evidence", "memory"], ["memory"]]) 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() owner = pipeline(tmp_path, Source([(item("one", "a"), "stable")]), vectors=vectors) diff --git a/harness/tests/test_corpus_publish.py b/harness/tests/test_corpus_publish.py index 0596bd3a..3e8fca75 100644 --- a/harness/tests/test_corpus_publish.py +++ b/harness/tests/test_corpus_publish.py @@ -1,8 +1,8 @@ import pytest import os -from tht.corpus.models import CorpusManifest -from tht.corpus.store import CorpusStore, UnsafeCorpusPath +from tht.evidence.corpus.models import CorpusManifest +from tht.evidence.corpus.store import CorpusStore, UnsafeCorpusPath 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): - from tht.corpus.models import CanonicalDocument + from tht.evidence.corpus.models import CanonicalDocument content = "trusted" 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): - from tht.corpus.models import CanonicalDocument + from tht.evidence.corpus.models import CanonicalDocument import hashlib 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): - from tht.corpus.models import CanonicalDocument + from tht.evidence.corpus.models import CanonicalDocument import hashlib def doc(content, fingerprint): diff --git a/harness/tests/test_evidence_facade_contract.py b/harness/tests/test_evidence_facade_contract.py index aac845aa..6964d45f 100644 --- a/harness/tests/test_evidence_facade_contract.py +++ b/harness/tests/test_evidence_facade_contract.py @@ -1,12 +1,13 @@ from datetime import UTC, datetime import hashlib import inspect +from pathlib import Path from types import SimpleNamespace import pytest -from tht.corpus.models import CanonicalDocument, CorpusManifest -from tht.corpus.store import CorpusStore +from tht.evidence.corpus.models import CanonicalDocument, CorpusManifest +from tht.evidence.corpus.store import CorpusStore from tht.decisions import DecisionRecord from tht.evidence import ( acquire, @@ -18,15 +19,12 @@ from tht.evidence import ( project_session, resolve_citation, ) -from tht.ports.evidence import ( +from tht.evidence.contracts import ( AcquiredDocument, EvidenceSourceError, EvidenceSourceErrorCategory, 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 @@ -91,8 +89,6 @@ def test_acquisition_facade_preserves_classified_errors(): 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" configured_root = tmp_path / "configured" (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) - assert [type(source) for source in current] == [type(source) for source in legacy] assert [source.root for source in current] == [ (legacy_root / "evidence").resolve(), configured_root.resolve(), ] - assert current[1].patterns == legacy[1].patterns == ("*.md",) - assert current[1].max_bytes == legacy[1].max_bytes == 1024 + assert current[1].patterns == ("*.md",) + assert current[1].max_bytes == 1024 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): captured.update(kwargs) - monkeypatch.setattr("tht.corpus.pipeline.CorpusPipeline", FakePipeline) + monkeypatch.setattr("tht.evidence.preprocessing.CorpusPipeline", FakePipeline) dependencies = { "store": object(), "sources": [object()], @@ -176,18 +170,14 @@ class OrderedDelegate: def test_search_facade_preserves_active_filtering_and_global_order(tmp_path): cfg = _active_config(tmp_path) - legacy_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( cfg, facade_delegate, workspace_id="workspace-a", ).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 facade_delegate.calls == legacy_delegate.calls + assert [hit.id for hit in current] == ["higher", "lower"] + assert facade_delegate.calls == [([1.0], 2, ["memory"], None)] 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") materialized = tmp_path / "materialized" - legacy = resolve_evidence_file( - store, "evi-used", materialized_root=materialized, - ) current = resolve_citation( 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) == "" @@ -245,7 +233,7 @@ def test_session_projection_routes_corpus_citations_through_the_facade(tmp_path, calls.append((store.root, evidence_id, materialized_root)) 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( question="q", 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", "file": "/materialized/evi-used.md", "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) - assert current == legacy assert [(row["id"], row["esito"], row["decision_seq"]) for row in current] == [ ("used", "usata", 17), ("accepted", "accettata", 21), diff --git a/harness/tests/test_evidence_layout.py b/harness/tests/test_evidence_layout.py new file mode 100644 index 00000000..bf5cb748 --- /dev/null +++ b/harness/tests/test_evidence_layout.py @@ -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 == [] diff --git a/harness/tests/test_evidence_port_contract.py b/harness/tests/test_evidence_port_contract.py index eb28fc13..81251d56 100644 --- a/harness/tests/test_evidence_port_contract.py +++ b/harness/tests/test_evidence_port_contract.py @@ -3,7 +3,7 @@ from datetime import UTC, datetime, timedelta, timezone import pytest from pydantic import ValidationError -from tht.ports.evidence import ( +from tht.evidence.contracts import ( AcquiredDocument, EvidenceSource, EvidenceSourceError, diff --git a/harness/tests/test_filesystem_evidence_source.py b/harness/tests/test_filesystem_evidence_source.py index 3f93789d..c27c35ab 100644 --- a/harness/tests/test_filesystem_evidence_source.py +++ b/harness/tests/test_filesystem_evidence_source.py @@ -2,8 +2,8 @@ import os import pytest -from tht.adapters.evidence import FilesystemEvidenceSource -from tht.ports.evidence import EvidenceSourceError +from tht.evidence.adapters import FilesystemEvidenceSource +from tht.evidence.contracts import EvidenceSourceError def test_filesystem_discovery_is_stable_and_acquisition_is_bounded(tmp_path): diff --git a/harness/tests/test_http_evidence_source.py b/harness/tests/test_http_evidence_source.py index 38883dea..4bd24d0e 100644 --- a/harness/tests/test_http_evidence_source.py +++ b/harness/tests/test_http_evidence_source.py @@ -4,8 +4,8 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer import pytest -from tht.adapters.evidence import HttpManifestEvidenceSource -from tht.ports.evidence import EvidenceSourceError +from tht.evidence.adapters import HttpManifestEvidenceSource +from tht.evidence.contracts import EvidenceSourceError class Handler(BaseHTTPRequestHandler): diff --git a/harness/tests/test_registry_evidence_config.py b/harness/tests/test_registry_evidence_config.py index 358061fd..33d22a20 100644 --- a/harness/tests/test_registry_evidence_config.py +++ b/harness/tests/test_registry_evidence_config.py @@ -6,8 +6,8 @@ import yaml from pydantic import SecretStr from typer.testing import CliRunner -from tht.adapters.evidence import HttpManifestEvidenceSource -from tht.adapters.factory import build_evidence_sources +from tht.evidence.adapters import HttpManifestEvidenceSource +from tht.evidence import build_sources from tht.cli import app 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(cfg.model_dump_json()) - adapter = build_evidence_sources(cfg)[0] + adapter = build_sources(cfg.evidence)[0] assert isinstance(adapter, HttpManifestEvidenceSource) assert_no_canaries(repr(adapter)) diff --git a/harness/tests/test_s3_evidence_source.py b/harness/tests/test_s3_evidence_source.py index 710b44fc..43a865c9 100644 --- a/harness/tests/test_s3_evidence_source.py +++ b/harness/tests/test_s3_evidence_source.py @@ -2,7 +2,7 @@ from datetime import UTC, datetime import pytest -from tht.ports.evidence import EvidenceSourceError +from tht.evidence.contracts import EvidenceSourceError class Body: @@ -28,7 +28,7 @@ class Client: 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() source = S3EvidenceSource(bucket="evidence", prefix="clinical/", client=client) 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(): - from tht.adapters.evidence.s3 import S3EvidenceSource + from tht.evidence.adapters.s3 import S3EvidenceSource client = Client() with pytest.raises(ValueError): S3EvidenceSource(bucket="evidence", client=client, max_objects=0) 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"): S3EvidenceSource(bucket="evidence", endpoint_url="https://127.0.0.1:9000", client=Client()) 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"]) 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"): S3EvidenceSource(bucket=bucket, client=Client()) @pytest.mark.parametrize("bucket", ["127.0.0.1", "192.168.1.1"]) 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"): S3EvidenceSource(bucket=bucket, client=Client()) 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", "https://s3.example.test/#x"): 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(): - from tht.adapters.evidence.s3 import S3EvidenceSource + from tht.evidence.adapters.s3 import S3EvidenceSource client = Client() client.list_objects_v2 = lambda **kwargs: {"Contents": [{"Key": "other/a.md", "ETag": '"x"'}]} 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(): - from tht.adapters.evidence.s3 import S3EvidenceSource + from tht.evidence.adapters.s3 import S3EvidenceSource with pytest.raises(ValueError, match="prefix"): S3EvidenceSource(bucket="evidence", prefix="/clinical", client=Client()) 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"]) 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() with pytest.raises(ValueError, match="prefix"): 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(): - from tht.adapters.evidence.s3 import S3EvidenceSource + from tht.evidence.adapters.s3 import S3EvidenceSource client = Client() def listing(**kwargs): 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(): - from tht.adapters.evidence.s3 import S3EvidenceSource + from tht.evidence.adapters.s3 import S3EvidenceSource client = Client() source = S3EvidenceSource(bucket="evidence", client=client) 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(): - from tht.adapters.evidence.s3 import S3EvidenceSource + from tht.evidence.adapters.s3 import S3EvidenceSource client = Client() source = S3EvidenceSource(bucket="evidence", client=client) 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", "[::1]", "[fe80::1]", "[::]"]) 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"): S3EvidenceSource(bucket="evidence", endpoint_url=f"https://{host}:9000", trusted_endpoint=True, client=Client()) def test_s3_size_limit_closes_body(): - from tht.adapters.evidence.s3 import S3EvidenceSource + from tht.evidence.adapters.s3 import S3EvidenceSource client = Client() source = S3EvidenceSource(bucket="evidence", client=client, max_bytes=4) item = next(iter(source.discover())) diff --git a/harness/tests/test_semantic_kind_isolation.py b/harness/tests/test_semantic_kind_isolation.py index 5f000a46..9a5403df 100644 --- a/harness/tests/test_semantic_kind_isolation.py +++ b/harness/tests/test_semantic_kind_isolation.py @@ -6,10 +6,10 @@ from datetime import UTC, datetime from tht.adapters.vector.qdrant import point_id from tht.cli.vector_cmd import sync_canonical_records -from tht.corpus.chunk import ChunkPolicy -from tht.corpus.models import CanonicalChunk -from tht.corpus.pipeline import CorpusPipeline -from tht.corpus.store import CorpusStore +from tht.evidence.corpus.chunk import ChunkPolicy +from tht.evidence.corpus.models import CanonicalChunk +from tht.evidence.corpus.pipeline import CorpusPipeline +from tht.evidence.corpus.store import CorpusStore from tht.memory import MemoryRecord, save_one_memory from tht.mschema.models import ( Annotations, diff --git a/harness/tests/test_workflow_observable_contract.py b/harness/tests/test_workflow_observable_contract.py index 9382a527..29d90002 100644 --- a/harness/tests/test_workflow_observable_contract.py +++ b/harness/tests/test_workflow_observable_contract.py @@ -6,8 +6,8 @@ import hashlib import pytest -from tht.corpus.models import CanonicalDocument, CorpusManifest -from tht.corpus.store import CorpusStore +from tht.evidence.corpus.models import CanonicalDocument, CorpusManifest +from tht.evidence.corpus.store import CorpusStore from tht.decisions import DecisionRecord, append_decision from tht.evidence import project_session from tht.phase import current_phase, effective_decisions diff --git a/harness/tht/adapters/evidence/__init__.py b/harness/tht/adapters/evidence/__init__.py deleted file mode 100644 index 805c886b..00000000 --- a/harness/tht/adapters/evidence/__init__.py +++ /dev/null @@ -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"] diff --git a/harness/tht/adapters/factory.py b/harness/tht/adapters/factory.py index faa465b2..d21f1731 100644 --- a/harness/tht/adapters/factory.py +++ b/harness/tht/adapters/factory.py @@ -42,11 +42,4 @@ def build_vector_store(cfg: Config, *, require_write: bool = False) -> VectorSto raise ConfigError(f"Adapter vector non supportato: {other}") -def build_evidence_sources(cfg: Config): - """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"] +__all__ = ["build_dwh", "build_vector_store"] diff --git a/harness/tht/cli/preprocess_cmd.py b/harness/tht/cli/preprocess_cmd.py index aa6f1238..7190c5c8 100644 --- a/harness/tht/cli/preprocess_cmd.py +++ b/harness/tht/cli/preprocess_cmd.py @@ -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.cli.schema_cmd import _load_config_or_exit from tht.cli.vector_cmd import make_embedder - from tht.corpus.chunk import ChunkPolicy - from tht.corpus.store import CorpusStore + from tht.evidence.corpus.chunk import ChunkPolicy + from tht.evidence.corpus.store import CorpusStore from tht.evidence import build_preprocessing_pipeline, build_sources 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.cli.schema_cmd import _load_config_or_exit from tht.cli.vector_cmd import make_embedder - from tht.corpus.chunk import ChunkPolicy - from tht.corpus.store import CorpusStore + from tht.evidence.corpus.chunk import ChunkPolicy + from tht.evidence.corpus.store import CorpusStore from tht.evidence import build_preprocessing_pipeline, build_sources cfg = _load_config_or_exit(config) diff --git a/harness/tht/corpus/__init__.py b/harness/tht/corpus/__init__.py deleted file mode 100644 index 34ac3790..00000000 --- a/harness/tht/corpus/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Canonical, transport-independent Evidence corpus.""" diff --git a/harness/tht/evidence/__init__.py b/harness/tht/evidence/__init__.py index d2756b57..fba55717 100644 --- a/harness/tht/evidence/__init__.py +++ b/harness/tht/evidence/__init__.py @@ -1,14 +1,4 @@ -"""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 +"""Cohesive public entrypoint for Evidence domain capabilities.""" from tht.evidence.acquisition import acquire, discover from tht.evidence.contracts import ( @@ -22,145 +12,17 @@ from tht.evidence.contracts import ( validate_namespaced_value, validate_safe_metadata, ) - -if TYPE_CHECKING: - from tht.config import EvidenceSourcesConfig - from tht.corpus.chunk import ChunkPolicy - from tht.corpus.pipeline import CorpusPipeline - from tht.corpus.store import CorpusStore - from tht.decisions import DecisionRecord - from tht.evidence.preprocessing import EvidenceEmbedder - 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, - ) - - -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()) +from tht.evidence.preprocessing import EvidenceEmbedder, build_preprocessing_pipeline +from tht.evidence.search import ( + ActiveEvidenceSearcher, + CorpusWorkspaceMismatchError, + active_searcher, + build_retrieval_entries, + resolve_citation, + validate_corpus_workspace, +) +from tht.evidence.session import project_session +from tht.evidence.sources import build_sources __all__ = [ @@ -168,7 +30,10 @@ __all__ = [ "EvidenceSource", "EvidenceSourceError", "EvidenceSourceErrorCategory", + "EvidenceEmbedder", "SourceObject", + "ActiveEvidenceSearcher", + "CorpusWorkspaceMismatchError", "acquire", "active_searcher", "build_preprocessing_pipeline", diff --git a/harness/tht/evidence/adapters/__init__.py b/harness/tht/evidence/adapters/__init__.py new file mode 100644 index 00000000..c6e09780 --- /dev/null +++ b/harness/tht/evidence/adapters/__init__.py @@ -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"] diff --git a/harness/tht/adapters/evidence/filesystem.py b/harness/tht/evidence/adapters/filesystem.py similarity index 99% rename from harness/tht/adapters/evidence/filesystem.py rename to harness/tht/evidence/adapters/filesystem.py index ae415607..50fa3662 100644 --- a/harness/tht/adapters/evidence/filesystem.py +++ b/harness/tht/evidence/adapters/filesystem.py @@ -1,4 +1,4 @@ -"""Contained, race-safe filesystem Evidence source.""" +"""Evidence-owned, race-safe filesystem source.""" import hashlib import os diff --git a/harness/tht/adapters/evidence/http.py b/harness/tht/evidence/adapters/http.py similarity index 99% rename from harness/tht/adapters/evidence/http.py rename to harness/tht/evidence/adapters/http.py index ff423d22..f8614bfc 100644 --- a/harness/tht/adapters/evidence/http.py +++ b/harness/tht/evidence/adapters/http.py @@ -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 ipaddress diff --git a/harness/tht/adapters/evidence/s3.py b/harness/tht/evidence/adapters/s3.py similarity index 98% rename from harness/tht/adapters/evidence/s3.py rename to harness/tht/evidence/adapters/s3.py index cbbfee90..5239a5f6 100644 --- a/harness/tht/adapters/evidence/s3.py +++ b/harness/tht/evidence/adapters/s3.py @@ -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 ipaddress diff --git a/harness/tht/evidence/corpus/__init__.py b/harness/tht/evidence/corpus/__init__.py new file mode 100644 index 00000000..6ee33155 --- /dev/null +++ b/harness/tht/evidence/corpus/__init__.py @@ -0,0 +1 @@ +"""Evidence-owned canonical, transport-independent corpus.""" diff --git a/harness/tht/corpus/chunk.py b/harness/tht/evidence/corpus/chunk.py similarity index 94% rename from harness/tht/corpus/chunk.py rename to harness/tht/evidence/corpus/chunk.py index 3c297435..5c868295 100644 --- a/harness/tht/corpus/chunk.py +++ b/harness/tht/evidence/corpus/chunk.py @@ -1,11 +1,11 @@ -"""Versioned deterministic chunking for canonical corpus documents.""" +"""Evidence-owned deterministic chunking for canonical corpus documents.""" import hashlib import json import re 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) diff --git a/harness/tht/corpus/models.py b/harness/tht/evidence/corpus/models.py similarity index 98% rename from harness/tht/corpus/models.py rename to harness/tht/evidence/corpus/models.py index d398124c..22afca93 100644 --- a/harness/tht/corpus/models.py +++ b/harness/tht/evidence/corpus/models.py @@ -1,4 +1,4 @@ -"""Immutable records emitted by the Evidence preprocessing pipeline.""" +"""Evidence-owned immutable records emitted by preprocessing.""" import hashlib import re diff --git a/harness/tht/corpus/normalize.py b/harness/tht/evidence/corpus/normalize.py similarity index 97% rename from harness/tht/corpus/normalize.py rename to harness/tht/evidence/corpus/normalize.py index 22d4ac21..b55c6e88 100644 --- a/harness/tht/corpus/normalize.py +++ b/harness/tht/evidence/corpus/normalize.py @@ -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 re @@ -10,7 +10,7 @@ from pydantic import JsonValue, TypeAdapter, ValidationError from yaml.events import AliasEvent 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 diff --git a/harness/tht/corpus/pipeline.py b/harness/tht/evidence/corpus/pipeline.py similarity index 99% rename from harness/tht/corpus/pipeline.py rename to harness/tht/evidence/corpus/pipeline.py index 7a55012d..23b67219 100644 --- a/harness/tht/corpus/pipeline.py +++ b/harness/tht/evidence/corpus/pipeline.py @@ -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 @@ -11,10 +11,10 @@ from dataclasses import asdict, dataclass, field from datetime import UTC from pathlib import Path -from tht.corpus.chunk import ChunkPolicy, chunk -from tht.corpus.models import CanonicalChunk, CanonicalDocument, CorpusManifest -from tht.corpus.normalize import normalize -from tht.corpus.store import CorpusStore +from tht.evidence.corpus.chunk import ChunkPolicy, chunk +from tht.evidence.corpus.models import CanonicalChunk, CanonicalDocument, CorpusManifest +from tht.evidence.corpus.normalize import normalize +from tht.evidence.corpus.store import CorpusStore import tht.evidence.acquisition as evidence_acquisition from tht.evidence.contracts import EvidenceSource, SourceObject, canonical_provenance_uri from tht.ports.vector import VectorStore, VectorWriteRecord diff --git a/harness/tht/corpus/store.py b/harness/tht/evidence/corpus/store.py similarity index 98% rename from harness/tht/corpus/store.py rename to harness/tht/evidence/corpus/store.py index 0202e566..af28b9a8 100644 --- a/harness/tht/corpus/store.py +++ b/harness/tht/evidence/corpus/store.py @@ -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 @@ -15,7 +15,7 @@ from datetime import UTC, datetime from pathlib import Path 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}$") diff --git a/harness/tht/evidence/preprocessing.py b/harness/tht/evidence/preprocessing.py index e1bf29e2..fd6c0c5e 100644 --- a/harness/tht/evidence/preprocessing.py +++ b/harness/tht/evidence/preprocessing.py @@ -2,9 +2,9 @@ from typing import Protocol -from tht.corpus.chunk import ChunkPolicy -from tht.corpus.pipeline import CorpusPipeline -from tht.corpus.store import CorpusStore +from tht.evidence.corpus.chunk import ChunkPolicy +from tht.evidence.corpus.pipeline import CorpusPipeline +from tht.evidence.corpus.store import CorpusStore from tht.evidence.contracts import EvidenceSource from tht.ports.vector import VectorStore diff --git a/harness/tht/search/evidence.py b/harness/tht/evidence/search.py similarity index 88% rename from harness/tht/search/evidence.py rename to harness/tht/evidence/search.py index 077b3355..108dc229 100644 --- a/harness/tht/search/evidence.py +++ b/harness/tht/evidence/search.py @@ -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 -from tht.corpus.store import CorpusStore +from tht.evidence.corpus.store import CorpusStore 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, ) -> str: with store.writer_lock(): @@ -114,3 +114,25 @@ def resolve_evidence_file( ) return str(path) if path else "" 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", +] diff --git a/harness/tht/evidence/session.py b/harness/tht/evidence/session.py new file mode 100644 index 00000000..37f27066 --- /dev/null +++ b/harness/tht/evidence/session.py @@ -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"] diff --git a/harness/tht/evidence/sources.py b/harness/tht/evidence/sources.py index 4c2a6c87..dc81f593 100644 --- a/harness/tht/evidence/sources.py +++ b/harness/tht/evidence/sources.py @@ -1,10 +1,16 @@ """Construction of configured Evidence source adapters.""" -from tht.adapters.evidence import FilesystemEvidenceSource, HttpManifestEvidenceSource -from tht.adapters.evidence.s3 import S3EvidenceSource -from tht.config import ConfigError, EvidenceSourcesConfig +from __future__ import annotations + +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 +if TYPE_CHECKING: + from tht.config import EvidenceSourcesConfig + def build_sources(evidence: EvidenceSourcesConfig | None) -> list[EvidenceSource]: """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. + from tht.config import ConfigError + raise ConfigError(f"Adapter evidence non supportato: {other}") return sources diff --git a/harness/tht/ports/evidence.py b/harness/tht/ports/evidence.py deleted file mode 100644 index a56a1a6b..00000000 --- a/harness/tht/ports/evidence.py +++ /dev/null @@ -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", -] diff --git a/harness/tht/session/artifacts.py b/harness/tht/session/artifacts.py deleted file mode 100644 index 159aa3e6..00000000 --- a/harness/tht/session/artifacts.py +++ /dev/null @@ -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)