refactor(evidence): remove legacy Python layout (#31)
This commit is contained in:
@@ -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,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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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 == []
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
|
||||||
@@ -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"]
|
|
||||||
|
|||||||
@@ -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 +0,0 @@
|
|||||||
"""Canonical, transport-independent Evidence corpus."""
|
|
||||||
@@ -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.evidence.session import project_session
|
||||||
from tht.session.models import SchemaLinking
|
from tht.evidence.sources import build_sources
|
||||||
|
|
||||||
|
|
||||||
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())
|
|
||||||
|
|
||||||
|
|
||||||
__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
-1
@@ -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
|
||||||
@@ -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}$")
|
||||||
@@ -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",
|
||||||
|
]
|
||||||
@@ -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"]
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
|
||||||
]
|
|
||||||
@@ -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)
|
|
||||||
Reference in New Issue
Block a user