feat(evidence): resolve curated evidence explicitly

This commit is contained in:
2026-08-24 20:52:56 +02:00
parent f5c7cc6198
commit 8adc085746
7 changed files with 442 additions and 4 deletions
+1
View File
@@ -11,6 +11,7 @@
"decision add-batch",
"decision add-join-set",
"evidence prepare",
"evidence resolve",
"evidence validate",
"memory promote",
"memory save-one",
+1 -1
View File
@@ -32,7 +32,7 @@ def test_typer_tree_matches_the_approved_command_surface():
approved = _approved_surface()
expected = set(approved["maintained"]) | set(approved["enhanced"])
assert len(approved["maintained"]) == 57
assert len(approved["maintained"]) == 58
assert len(approved["enhanced"]) == 8
assert len(approved["erased"]) == 14
assert not (expected & set(approved["erased"]))
+187 -3
View File
@@ -316,18 +316,18 @@ class _Restructurer(EvidenceRestructurer):
return tuple(self.candidates)
def _candidate(*, title="Fascia pediatrica", existing_id=None):
def _candidate(*, title="Fascia pediatrica", existing_id=None, kind="domain", payload=None):
return RestructureCandidate.model_validate({
"schema_version": 1,
"existing_id": existing_id,
"title": title,
"kind": "domain",
"kind": kind,
"purposes": ["disambiguation"],
"applies_to": {"concepts": ["fascia pediatrica"]},
"language": "it",
"supporting_excerpts": ["I pazienti sotto i 18 anni sono pediatrici."],
"review_items": [],
"payload": {"rule": "La fascia pediatrica comprende i minori."},
"payload": payload or {"rule": "La fascia pediatrica comprende i minori."},
})
@@ -378,6 +378,47 @@ def test_prepare_rejects_dirty_curated_state_before_model_call(tmp_path):
assert restructurer.requests == []
def test_prepare_refuses_an_incompatible_pipeline_version_without_writing(tmp_path):
source_text = "I pazienti sotto i 18 anni sono pediatrici."
_write_workspace(tmp_path, _evidence(source_text), source_text)
manifest_path = tmp_path / "evidence" / "manifest.yaml"
curated_path = tmp_path / "evidence" / "curated" / "domain" / "fascia-pediatrica.md"
manifest_path.write_text(
manifest_path.read_text(encoding="utf-8").replace("evidence-authoring-v1", "evidence-authoring-v2"),
encoding="utf-8",
)
original_manifest = manifest_path.read_text(encoding="utf-8")
original_curated = curated_path.read_text(encoding="utf-8")
with pytest.raises(EvidencePreparationError, match="pipeline_upgrade_required"):
prepare_workspace_evidence(tmp_path, restructurer=_Restructurer([]), git_status=lambda _: ())
assert manifest_path.read_text(encoding="utf-8") == original_manifest
assert curated_path.read_text(encoding="utf-8") == original_curated
def test_prepare_preserves_an_id_when_reclassified_and_allocates_a_new_id_for_a_split(tmp_path):
source_text = "I pazienti sotto i 18 anni sono pediatrici."
_write_workspace(tmp_path, _evidence(source_text), source_text)
source_path = tmp_path / "evidence" / "source" / "domain" / "patient.md"
source_path.write_text(source_text + " I pazienti sopra i 18 anni sono adulti.\n", encoding="utf-8")
restructurer = _Restructurer([
_candidate(
existing_id="evidence:fascia-pediatrica",
kind="glossary",
payload={"definition": "Paziente con meno di 18 anni."},
),
_candidate(title="Fascia adulta"),
])
report = prepare_workspace_evidence(tmp_path, restructurer=restructurer, git_status=lambda _: ())
assert report.created == ("evidence:fascia-adulta",)
documents = {document.id: document for document in load_curated_tree(tmp_path / "evidence" / "curated")}
assert documents["evidence:fascia-pediatrica"].kind == "glossary"
assert documents["evidence:fascia-adulta"].kind == "domain"
def test_prepare_retains_units_from_a_removed_source_as_blocking_orphans(tmp_path):
source_text = "I pazienti sotto i 18 anni sono pediatrici."
_write_workspace(tmp_path, _evidence(source_text), source_text)
@@ -437,3 +478,146 @@ def test_prepare_marks_an_omitted_prior_unit_for_human_review(tmp_path):
]
retained = load_curated_tree(tmp_path / "evidence" / "curated")[0]
assert retained.review_items[0].code == "source_no_longer_supports_unit"
def test_resolve_retires_an_orphan_and_updates_curated_content_and_manifest_atomically(tmp_path):
from tht.evidence import resolve_workspace_evidence
source_text = "I pazienti sotto i 18 anni sono pediatrici."
_write_workspace(tmp_path, _evidence(source_text), source_text)
source_path = tmp_path / "evidence" / "source" / "domain" / "patient.md"
source_path.unlink()
prepare_workspace_evidence(tmp_path, restructurer=_Restructurer([]), git_status=lambda _: ())
report = resolve_workspace_evidence(
tmp_path,
"evidence:fascia-pediatrica",
retire=True,
git_status=lambda _: (),
)
assert report.action == "retired"
assert report.evidence_id == "evidence:fascia-pediatrica"
assert not (tmp_path / "evidence" / "curated" / "domain" / "fascia-pediatrica.md").exists()
manifest = load_manifest(tmp_path / "evidence" / "manifest.yaml")
assert manifest.orphans == ()
assert manifest.sources == {}
def test_resolve_relinks_an_unit_and_updates_its_manifest_membership(tmp_path):
from tht.evidence import resolve_workspace_evidence
source_text = "I pazienti sotto i 18 anni sono pediatrici."
_write_workspace(tmp_path, _evidence(source_text), source_text)
next_source = tmp_path / "evidence" / "source" / "domain" / "patient-age.md"
next_source.write_text(source_text, encoding="utf-8")
report = resolve_workspace_evidence(
tmp_path,
"evidence:fascia-pediatrica",
source="source/domain/patient-age.md",
git_status=lambda _: (),
)
assert report.action == "relinked"
assert report.source_file == "source/domain/patient-age.md"
document = load_curated_tree(tmp_path / "evidence" / "curated")[0]
assert document.provenance.source_file == "source/domain/patient-age.md"
manifest = load_manifest(tmp_path / "evidence" / "manifest.yaml")
assert manifest.sources["source/domain/patient.md"].units == ()
assert manifest.sources["source/domain/patient-age.md"].units == ("evidence:fascia-pediatrica",)
assert validate_workspace_evidence(tmp_path).publishable is True
def test_resolve_relink_clears_an_unsupported_review_item_when_the_new_source_supports_it(tmp_path):
from tht.evidence import resolve_workspace_evidence
source_text = "I pazienti sotto i 18 anni sono pediatrici."
_write_workspace(tmp_path, _evidence(source_text), source_text)
source_path = tmp_path / "evidence" / "source" / "domain" / "patient.md"
source_path.write_text("La classificazione pediatrica non è documentata.\n", encoding="utf-8")
prepare_workspace_evidence(tmp_path, restructurer=_Restructurer([]), git_status=lambda _: ())
replacement_source = tmp_path / "evidence" / "source" / "domain" / "patient-revised.md"
replacement_source.write_text(source_text, encoding="utf-8")
report = resolve_workspace_evidence(
tmp_path,
"evidence:fascia-pediatrica",
source="source/domain/patient-revised.md",
git_status=lambda _: (),
)
assert report.findings == ()
document = load_curated_tree(tmp_path / "evidence" / "curated")[0]
assert all(item.code != "source_no_longer_supports_unit" for item in document.review_items)
assert validate_workspace_evidence(tmp_path).publishable is True
def test_resolve_refuses_an_unsafe_relink_without_writing(tmp_path):
from tht.evidence import EvidencePreparationError, resolve_workspace_evidence
source_text = "I pazienti sotto i 18 anni sono pediatrici."
_write_workspace(tmp_path, _evidence(source_text), source_text)
curated_path = tmp_path / "evidence" / "curated" / "domain" / "fascia-pediatrica.md"
manifest_path = tmp_path / "evidence" / "manifest.yaml"
original_curated = curated_path.read_text(encoding="utf-8")
original_manifest = manifest_path.read_text(encoding="utf-8")
with pytest.raises(EvidencePreparationError, match="source_invalid"):
resolve_workspace_evidence(
tmp_path,
"evidence:fascia-pediatrica",
source="../outside.md",
git_status=lambda _: (),
)
assert curated_path.read_text(encoding="utf-8") == original_curated
assert manifest_path.read_text(encoding="utf-8") == original_manifest
def test_resolve_refuses_a_relink_through_a_symlinked_source_directory(tmp_path):
from tht.evidence import EvidencePreparationError, resolve_workspace_evidence
source_text = "I pazienti sotto i 18 anni sono pediatrici."
_write_workspace(tmp_path, _evidence(source_text), source_text)
curated_path = tmp_path / "evidence" / "curated" / "domain" / "fascia-pediatrica.md"
manifest_path = tmp_path / "evidence" / "manifest.yaml"
original_curated = curated_path.read_text(encoding="utf-8")
original_manifest = manifest_path.read_text(encoding="utf-8")
source_root = tmp_path / "evidence" / "source"
outside_source = tmp_path / "outside-source"
source_root.rename(outside_source)
source_root.symlink_to(outside_source, target_is_directory=True)
with pytest.raises(EvidencePreparationError, match="source_invalid"):
resolve_workspace_evidence(
tmp_path,
"evidence:fascia-pediatrica",
source="source/domain/patient.md",
git_status=lambda _: (),
)
assert curated_path.read_text(encoding="utf-8") == original_curated
assert manifest_path.read_text(encoding="utf-8") == original_manifest
def test_resolve_rejects_any_dirty_worktree_without_writing(tmp_path):
from tht.evidence import EvidencePreparationError, resolve_workspace_evidence
source_text = "I pazienti sotto i 18 anni sono pediatrici."
_write_workspace(tmp_path, _evidence(source_text), source_text)
curated_path = tmp_path / "evidence" / "curated" / "domain" / "fascia-pediatrica.md"
manifest_path = tmp_path / "evidence" / "manifest.yaml"
original_curated = curated_path.read_text(encoding="utf-8")
original_manifest = manifest_path.read_text(encoding="utf-8")
with pytest.raises(EvidencePreparationError, match="worktree_dirty"):
resolve_workspace_evidence(
tmp_path,
"evidence:fascia-pediatrica",
retire=True,
git_status=lambda _: (" M README.md",),
)
assert curated_path.read_text(encoding="utf-8") == original_curated
assert manifest_path.read_text(encoding="utf-8") == original_manifest
+77
View File
@@ -10,9 +10,86 @@ def test_evidence_authoring_commands_are_distinct_from_runtime_preprocessing():
assert result.exit_code == 0
assert "prepare" in result.output
assert "resolve" in result.output
assert "validate" in result.output
def test_evidence_resolve_json_is_pristine(monkeypatch, tmp_path):
from tht.cli import evidence_cmd
from tht.evidence.authoring import EvidenceResolutionReport
monkeypatch.setattr(evidence_cmd, "_canonical_worktree", lambda root: root)
monkeypatch.setattr(evidence_cmd, "resolve_workspace_evidence", lambda *args, **kwargs: EvidenceResolutionReport(
action="retired",
evidence_id="evidence:fascia-pediatrica",
source_file=None,
findings=(),
))
result = CliRunner().invoke(app, [
"evidence", "resolve", str(tmp_path), "evidence:fascia-pediatrica", "--retire", "--json",
])
assert result.exit_code == 0
assert result.stderr == ""
assert json.loads(result.stdout) == {
"action": "retired",
"evidenceId": "evidence:fascia-pediatrica",
"findings": [],
"operation": "evidence_resolve",
"schemaVersion": 1,
"sourceFile": None,
"status": "resolved",
}
def test_evidence_resolve_reports_unsafe_source_as_cli_misuse(monkeypatch, tmp_path):
from tht.cli import evidence_cmd
from tht.evidence import EvidencePreparationError
monkeypatch.setattr(evidence_cmd, "_canonical_worktree", lambda root: root)
monkeypatch.setattr(
evidence_cmd,
"resolve_workspace_evidence",
lambda *args, **kwargs: (_ for _ in ()).throw(EvidencePreparationError("source_invalid")),
)
result = CliRunner().invoke(app, [
"evidence", "resolve", str(tmp_path), "evidence:fascia-pediatrica", "--source", "source/domain/x.md", "--json",
])
assert result.exit_code == 2
assert result.stderr == ""
assert json.loads(result.stdout) == {
"code": "source_invalid",
"operation": "evidence_resolve",
"schemaVersion": 1,
"status": "failed",
}
def test_evidence_resolve_reports_structural_findings_as_operational_failure(monkeypatch, tmp_path):
from tht.cli import evidence_cmd
from tht.evidence import ValidationFinding
from tht.evidence.authoring import EvidenceResolutionReport
monkeypatch.setattr(evidence_cmd, "_canonical_worktree", lambda root: root)
monkeypatch.setattr(evidence_cmd, "resolve_workspace_evidence", lambda *args, **kwargs: EvidenceResolutionReport(
action="relinked",
evidence_id="evidence:fascia-pediatrica",
source_file="source/domain/patient.md",
findings=(ValidationFinding(
"error", "supporting_excerpt_missing", "source/domain/patient.md", "Excerpt missing.",
),),
))
result = CliRunner().invoke(app, [
"evidence", "resolve", str(tmp_path), "evidence:fascia-pediatrica", "--source", "source/domain/patient.md",
])
assert result.exit_code == 1
def test_evidence_validate_json_is_pristine_and_reports_review_required(monkeypatch, tmp_path):
from tht.cli import evidence_cmd
from tht.evidence import ValidationFinding, ValidationReport