feat: complete evidence restructuring worktree
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
"decision add-batch",
|
||||
"decision add-join-set",
|
||||
"evidence evaluate",
|
||||
"evidence migrate",
|
||||
"evidence prepare",
|
||||
"evidence resolve",
|
||||
"evidence validate",
|
||||
|
||||
@@ -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"]) == 60
|
||||
assert len(approved["maintained"]) == 61
|
||||
assert len(approved["enhanced"]) == 8
|
||||
assert len(approved["erased"]) == 14
|
||||
assert not (expected & set(approved["erased"]))
|
||||
|
||||
@@ -121,7 +121,7 @@ def pipeline(tmp_path, source, *, embedder=None, vectors=None, model="model-a",
|
||||
def test_pipeline_embeds_validated_curated_evidence_as_semantic_fragments(tmp_path):
|
||||
evidence = CuratedEvidence.model_validate(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"schema_version": 2,
|
||||
"id": "evidence:fascia-pediatrica",
|
||||
"title": "Fascia pediatrica",
|
||||
"kind": "formula",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import hashlib
|
||||
import subprocess
|
||||
import threading
|
||||
import unicodedata
|
||||
|
||||
@@ -15,6 +16,7 @@ from tht.evidence import (
|
||||
dump_manifest,
|
||||
load_curated_tree,
|
||||
load_manifest,
|
||||
migrate_workspace_evidence,
|
||||
prepare_workspace_evidence,
|
||||
validate_workspace_evidence,
|
||||
)
|
||||
@@ -346,9 +348,41 @@ def test_prepare_changed_source_uses_one_model_call_and_applies_a_valid_batch(tm
|
||||
assert report.created == ()
|
||||
assert len(restructurer.requests) == 1
|
||||
assert restructurer.requests[0].previous_units[0].id == "evidence:fascia-pediatrica"
|
||||
curated_path = tmp_path / "evidence" / "curated" / "domain" / "fascia-pediatrica.md"
|
||||
curated = load_curated_tree(tmp_path / "evidence" / "curated")[0]
|
||||
assert curated.schema_version == 2
|
||||
assert "# Fascia pediatrica\n" in curated_path.read_text(encoding="utf-8")
|
||||
assert validate_workspace_evidence(tmp_path).publishable is True
|
||||
|
||||
|
||||
def test_migrate_workspace_evidence_rewrites_v1_units_without_a_model_call(tmp_path):
|
||||
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
||||
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
||||
|
||||
report = migrate_workspace_evidence(tmp_path, git_status=lambda _: ())
|
||||
|
||||
curated_path = tmp_path / "evidence" / "curated" / "domain" / "fascia-pediatrica.md"
|
||||
migrated = load_curated_tree(tmp_path / "evidence" / "curated")[0]
|
||||
assert report.migrated == ("evidence:fascia-pediatrica",)
|
||||
assert report.unchanged == ()
|
||||
assert migrated.schema_version == 2
|
||||
assert migrated.payload.rule == "La fascia pediatrica comprende i minori."
|
||||
assert "## Regola\n\nLa fascia pediatrica comprende i minori." in curated_path.read_text(
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert report.findings == ()
|
||||
|
||||
|
||||
def test_migrate_workspace_evidence_rejects_dirty_curated_files_in_a_nested_workspace(tmp_path):
|
||||
subprocess.run(["git", "init", "--quiet", str(tmp_path)], check=True)
|
||||
workspace_root = tmp_path / "psd-clinical"
|
||||
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
||||
_write_workspace(workspace_root, _evidence(source_text), source_text)
|
||||
|
||||
with pytest.raises(EvidencePreparationError, match="authoring_worktree_dirty"):
|
||||
migrate_workspace_evidence(workspace_root)
|
||||
|
||||
|
||||
def test_prepare_can_issue_independent_source_calls_concurrently(tmp_path):
|
||||
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
||||
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
||||
@@ -512,6 +546,7 @@ def test_prepare_marks_an_omitted_prior_unit_for_human_review(tmp_path):
|
||||
"supporting_excerpt_missing", "unresolved_review_item",
|
||||
]
|
||||
retained = load_curated_tree(tmp_path / "evidence" / "curated")[0]
|
||||
assert retained.schema_version == 2
|
||||
assert retained.review_items[0].code == "source_no_longer_supports_unit"
|
||||
|
||||
|
||||
|
||||
@@ -206,6 +206,154 @@ def _formula_evidence() -> CuratedEvidence:
|
||||
})
|
||||
|
||||
|
||||
def _domain_evidence_v2() -> CuratedEvidence:
|
||||
return CuratedEvidence.model_validate({
|
||||
**COMMON,
|
||||
"schema_version": 2,
|
||||
"title": "Dominio Ablazione",
|
||||
"kind": "domain",
|
||||
"payload": {
|
||||
"rule": (
|
||||
"Il dominio Ablazione rappresenta la procedura transcatetere.\n\n"
|
||||
"La fact centrale è `clinical.fact_ablazione`."
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
def test_v2_curated_markdown_renders_domain_content_in_the_markdown_body(tmp_path):
|
||||
evidence = _domain_evidence_v2()
|
||||
path = tmp_path / "curated" / "domain" / "dominio-ablazione.md"
|
||||
|
||||
text = dump_curated_markdown(evidence)
|
||||
frontmatter = text.split("---\n", 2)[1]
|
||||
parsed = parse_curated_markdown(text, path=path)
|
||||
|
||||
assert "domain:" not in frontmatter
|
||||
assert "supporting_excerpts:" not in frontmatter
|
||||
assert "review_items:" not in frontmatter
|
||||
assert "# Dominio Ablazione\n" in text
|
||||
assert "## Regola\n\nIl dominio Ablazione" in text
|
||||
assert "## Estratti di supporto\n\n> I pazienti sotto i 18 anni sono pediatrici." in text
|
||||
assert parsed == evidence
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("kind", "payload", "rendered"), [
|
||||
("glossary", {
|
||||
"definition": "Un paziente con età inferiore a 18 anni.",
|
||||
"synonyms": ["minore"],
|
||||
"variants": ["pediatrico"],
|
||||
}, "## Sinonimi\n\n- minore"),
|
||||
("enum", {
|
||||
"column": "clinical.episode.discharge_status",
|
||||
"values": {"D": "dimesso", "T": "trasferito | altra struttura"},
|
||||
}, "| `D` | dimesso |"),
|
||||
("example", {
|
||||
"question": "Come riconosco un paziente pediatrico?",
|
||||
"interpretation": "Applicare la formula della fascia pediatrica.",
|
||||
}, "## Domanda\n\nCome riconosco un paziente pediatrico?"),
|
||||
("mapping", {
|
||||
"concept": "fascia pediatrica",
|
||||
"tables": ["clinical.patient"],
|
||||
"columns": ["clinical.patient.birth_date"],
|
||||
}, "## Tabelle\n\n- `clinical.patient`"),
|
||||
("normalization", {
|
||||
"input": "PEDS",
|
||||
"output": "pediatrico",
|
||||
"rule": "Converte il codice abbreviato nella forma canonica.",
|
||||
}, "## Output\n\npediatrico"),
|
||||
("formula", {
|
||||
"concept": "fascia pediatrica",
|
||||
"columns": ["clinical.patient.birth_date"],
|
||||
"sql": "CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END",
|
||||
}, "```sql\nCASE WHEN age < 18"),
|
||||
("reference", {
|
||||
"url": "https://example.test/linea-guida",
|
||||
"label": "Linea guida",
|
||||
"description": "Criteri clinici di riferimento.",
|
||||
}, "## URL\n\n<https://example.test/linea-guida>"),
|
||||
])
|
||||
def test_v2_curated_markdown_renders_and_round_trips_every_typed_payload(
|
||||
tmp_path, kind, payload, rendered,
|
||||
):
|
||||
evidence = CuratedEvidence.model_validate({
|
||||
**COMMON,
|
||||
"schema_version": 2,
|
||||
"kind": kind,
|
||||
"payload": payload,
|
||||
})
|
||||
path = tmp_path / "curated" / kind / "fascia-pediatrica.md"
|
||||
|
||||
text = dump_curated_markdown(evidence)
|
||||
|
||||
assert rendered in text
|
||||
assert parse_curated_markdown(text, path=path) == evidence
|
||||
|
||||
|
||||
def test_v2_curated_markdown_renders_review_items_as_readable_blocks(tmp_path):
|
||||
evidence = CuratedEvidence.model_validate({
|
||||
**COMMON,
|
||||
"schema_version": 2,
|
||||
"kind": "domain",
|
||||
"review_items": [{
|
||||
"code": "ambiguous_source_statement",
|
||||
"message": "Il sorgente non chiarisce la data di riferimento.",
|
||||
"field": "domain.rule",
|
||||
}],
|
||||
"payload": {"rule": "L'età è calcolata alla data di ricovero."},
|
||||
})
|
||||
path = tmp_path / "curated" / "domain" / "fascia-pediatrica.md"
|
||||
|
||||
text = dump_curated_markdown(evidence)
|
||||
|
||||
assert "## Elementi da rivedere" in text
|
||||
assert "### `ambiguous_source_statement`" in text
|
||||
assert "Il sorgente non chiarisce la data di riferimento." in text
|
||||
assert "**Campo:** `domain.rule`" in text
|
||||
assert parse_curated_markdown(text, path=path) == evidence
|
||||
|
||||
|
||||
@pytest.mark.parametrize("legacy_field", [
|
||||
"review_items: []\n",
|
||||
"payload:\n rule: should-not-be-ignored\n",
|
||||
])
|
||||
def test_v2_curated_markdown_rejects_body_owned_fields_in_frontmatter(legacy_field):
|
||||
text = dump_curated_markdown(_domain_evidence_v2()).replace(
|
||||
"kind: domain\n",
|
||||
f"kind: domain\n{legacy_field}",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="frontmatter"):
|
||||
parse_curated_markdown(text)
|
||||
|
||||
|
||||
def test_v2_curated_markdown_rejects_unstructured_body_content():
|
||||
text = dump_curated_markdown(_domain_evidence_v2()) + "should-not-be-ignored\n"
|
||||
|
||||
with pytest.raises(ValueError, match="unstructured"):
|
||||
parse_curated_markdown(text)
|
||||
|
||||
|
||||
def test_v2_curated_markdown_round_trips_an_empty_enum_as_an_explicit_empty_state(tmp_path):
|
||||
evidence = CuratedEvidence.model_validate({
|
||||
**COMMON,
|
||||
"schema_version": 2,
|
||||
"kind": "enum",
|
||||
"payload": {
|
||||
"column": "clinical.episode.discharge_status",
|
||||
"values": {},
|
||||
},
|
||||
})
|
||||
|
||||
text = dump_curated_markdown(evidence)
|
||||
|
||||
assert "Nessun elemento" in text
|
||||
assert parse_curated_markdown(
|
||||
text,
|
||||
path=tmp_path / "curated" / "enum" / "discharge-status.md",
|
||||
) == evidence
|
||||
|
||||
|
||||
def test_curated_markdown_round_trip_uses_the_kind_specific_key(tmp_path):
|
||||
evidence = _formula_evidence()
|
||||
path = tmp_path / "curated" / "formula" / "fascia-pediatrica.md"
|
||||
|
||||
@@ -21,10 +21,40 @@ def test_evidence_authoring_commands_are_distinct_from_runtime_preprocessing():
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "prepare" in result.output
|
||||
assert "migrate" in result.output
|
||||
assert "resolve" in result.output
|
||||
assert "validate" in result.output
|
||||
|
||||
|
||||
def test_evidence_migrate_json_is_pristine(monkeypatch, tmp_path):
|
||||
from tht.cli import evidence_cmd
|
||||
from tht.evidence import EvidenceMigrationReport
|
||||
|
||||
monkeypatch.setattr(evidence_cmd, "_canonical_worktree", lambda root: root)
|
||||
monkeypatch.setattr(
|
||||
evidence_cmd,
|
||||
"migrate_workspace_evidence",
|
||||
lambda *args, **kwargs: EvidenceMigrationReport(
|
||||
migrated=("evidence:fascia-pediatrica",),
|
||||
unchanged=("evidence:fascia-adulta",),
|
||||
findings=(),
|
||||
),
|
||||
)
|
||||
|
||||
result = CliRunner().invoke(app, ["evidence", "migrate", str(tmp_path), "--json"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert result.stderr == ""
|
||||
assert json.loads(result.stdout) == {
|
||||
"findings": [],
|
||||
"migrated": ["evidence:fascia-pediatrica"],
|
||||
"operation": "evidence_migrate",
|
||||
"schemaVersion": 1,
|
||||
"status": "migrated",
|
||||
"unchanged": ["evidence:fascia-adulta"],
|
||||
}
|
||||
|
||||
|
||||
def test_evidence_prepare_failure_identifies_the_source_file(monkeypatch, tmp_path):
|
||||
from tht.cli import evidence_cmd
|
||||
from tht.evidence import EvidencePreparationError
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
"""Migration boundary: legacy formulas become curated evidence or session proposals."""
|
||||
|
||||
import hashlib
|
||||
|
||||
from tht.evidence import formula_store
|
||||
from tht.evidence.authoring import normalize_source_text
|
||||
from tht.evidence.canonical import CuratedEvidence
|
||||
from tht.evidence.formula_store import ConceptFormula
|
||||
|
||||
|
||||
def test_reviewed_formula_migration_has_deterministic_provenance_hash():
|
||||
formula = ConceptFormula(
|
||||
concept="fascia pediatrica",
|
||||
columns=["clinical.patient.birth_date"],
|
||||
sql="CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END",
|
||||
status="reviewed",
|
||||
sources=["Regola clinica approvata dal gruppo pediatrico."],
|
||||
)
|
||||
|
||||
original_source = """---
|
||||
concept: fascia pediatrica
|
||||
columns: [clinical.patient.birth_date]
|
||||
status: reviewed
|
||||
sources:
|
||||
- Regola clinica approvata dal gruppo pediatrico.
|
||||
---
|
||||
CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END
|
||||
"""
|
||||
|
||||
first = formula_store.legacy_formula_to_curated(
|
||||
formula,
|
||||
legacy_path="formulas/fascia-pediatrica-1.sql.md",
|
||||
source_content=original_source,
|
||||
)
|
||||
second = formula_store.legacy_formula_to_curated(
|
||||
formula,
|
||||
legacy_path="formulas/fascia-pediatrica-1.sql.md",
|
||||
source_content=original_source,
|
||||
)
|
||||
|
||||
assert isinstance(first, CuratedEvidence)
|
||||
assert isinstance(second, CuratedEvidence)
|
||||
assert first.provenance.source_sha256 == second.provenance.source_sha256
|
||||
expected = hashlib.sha256(normalize_source_text(original_source).encode("utf-8")).hexdigest()
|
||||
assert first.provenance.source_sha256 == f"sha256:{expected}"
|
||||
|
||||
|
||||
def test_reviewed_formula_without_original_source_fails_closed():
|
||||
formula = ConceptFormula(
|
||||
concept="fascia pediatrica",
|
||||
columns=["clinical.patient.birth_date"],
|
||||
sql="CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END",
|
||||
status="reviewed",
|
||||
sources=["Regola clinica approvata dal gruppo pediatrico."],
|
||||
)
|
||||
|
||||
outcome = formula_store.legacy_formula_to_curated(
|
||||
formula, legacy_path="formulas/fascia-pediatrica-1.sql.md",
|
||||
)
|
||||
|
||||
assert outcome.code == "legacy_formula_requires_manual_review"
|
||||
assert outcome.problems == ("original_source_required",)
|
||||
|
||||
|
||||
def test_reviewed_formula_without_verified_supporting_excerpts_fails_closed():
|
||||
formula = ConceptFormula(
|
||||
concept="fascia pediatrica",
|
||||
columns=["clinical.patient.birth_date"],
|
||||
sql="CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END",
|
||||
status="reviewed",
|
||||
sources=["Nota non presente nel sorgente originale."],
|
||||
)
|
||||
original_source = """---
|
||||
concept: fascia pediatrica
|
||||
columns: [clinical.patient.birth_date]
|
||||
status: reviewed
|
||||
sources:
|
||||
- >-
|
||||
Nota non presente nel
|
||||
sorgente originale.
|
||||
---
|
||||
CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END
|
||||
"""
|
||||
|
||||
outcome = formula_store.legacy_formula_to_curated(
|
||||
formula,
|
||||
legacy_path="formulas/fascia-pediatrica-1.sql.md",
|
||||
source_content=original_source,
|
||||
)
|
||||
|
||||
assert outcome.code == "legacy_formula_requires_manual_review"
|
||||
assert outcome.problems == ("supporting_excerpt_unverified",)
|
||||
|
||||
|
||||
def test_reviewed_formula_without_provenance_notes_fails_closed():
|
||||
formula = ConceptFormula(
|
||||
concept="fascia pediatrica",
|
||||
columns=["clinical.patient.birth_date"],
|
||||
sql="CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END",
|
||||
status="reviewed",
|
||||
sources=[],
|
||||
)
|
||||
|
||||
outcome = formula_store.legacy_formula_to_curated(
|
||||
formula,
|
||||
legacy_path="formulas/fascia-pediatrica-1.sql.md",
|
||||
source_content=formula.dump(),
|
||||
)
|
||||
|
||||
assert outcome.code == "legacy_formula_requires_manual_review"
|
||||
assert outcome.problems == ("supporting_excerpts_required",)
|
||||
|
||||
|
||||
def test_reviewed_formula_must_match_the_original_source_record():
|
||||
formula = ConceptFormula(
|
||||
concept="fascia pediatrica",
|
||||
columns=["clinical.patient.birth_date"],
|
||||
sql="CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END",
|
||||
status="reviewed",
|
||||
sources=["Regola clinica approvata dal gruppo pediatrico."],
|
||||
)
|
||||
different_source = ConceptFormula(
|
||||
concept=formula.concept,
|
||||
columns=formula.columns,
|
||||
sql="CASE WHEN age < 16 THEN 'pediatrica' ELSE 'adulta' END",
|
||||
status=formula.status,
|
||||
sources=formula.sources,
|
||||
).dump()
|
||||
|
||||
outcome = formula_store.legacy_formula_to_curated(
|
||||
formula,
|
||||
legacy_path="formulas/fascia-pediatrica-1.sql.md",
|
||||
source_content=different_source,
|
||||
)
|
||||
|
||||
assert outcome.code == "legacy_formula_requires_manual_review"
|
||||
assert outcome.problems == ("original_source_mismatch",)
|
||||
|
||||
|
||||
def test_incompatible_reviewed_legacy_formula_fails_closed_with_the_original_record():
|
||||
formula = ConceptFormula(
|
||||
concept="ablazione",
|
||||
columns=["testo"],
|
||||
sql="SELECT 2",
|
||||
status="reviewed",
|
||||
sources=["legacy manual"],
|
||||
)
|
||||
|
||||
outcome = formula_store.legacy_formula_to_curated(
|
||||
formula, legacy_path="formulas/ablazione-2.sql.md",
|
||||
)
|
||||
|
||||
assert outcome.code == "legacy_formula_requires_manual_review"
|
||||
assert outcome.legacy_path == "formulas/ablazione-2.sql.md"
|
||||
assert outcome.formula == formula
|
||||
+12
-145
@@ -1,161 +1,22 @@
|
||||
"""L1: SQL formula evidence -- concept->formula units (spec D14b).
|
||||
|
||||
A concept (e.g. 'fascia pediatrica') maps to a SQL formula (CASE WHEN ...) that
|
||||
derives it from physical columns. These are reusable, reviewable units: the gate
|
||||
surfaces a candidate formula, the reviewer approves or rejects it (recorded via
|
||||
concept_formula_approved / concept_formula_rejected), and approved formulas are
|
||||
part of the schema-linking artifact. The store is frontmatter-YAML + SQL body.
|
||||
"""
|
||||
|
||||
"""L1: session-local Formula proposals and their reviewer decisions."""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from tht.decisions import DecisionRecord
|
||||
from tht.evidence import formula_store
|
||||
from tht.evidence.formula_store import ConceptFormula, retrieve_formula, save_formula
|
||||
from tht.evidence.session import project_session
|
||||
from tht.session.models import SchemaLinking
|
||||
|
||||
|
||||
def test_formula_retrieval_by_concept(tmp_path):
|
||||
f = ConceptFormula(
|
||||
concept="fascia pediatrica",
|
||||
columns=["data_nascita"],
|
||||
sql="CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulto' END",
|
||||
status="reviewed",
|
||||
sources=["src1"],
|
||||
)
|
||||
save_formula(tmp_path, f)
|
||||
results = retrieve_formula(tmp_path, "fascia pediatrica")
|
||||
assert len(results) == 1
|
||||
assert results[0].sql.startswith("CASE WHEN")
|
||||
assert results[0].concept == "fascia pediatrica"
|
||||
assert results[0].status == "reviewed"
|
||||
assert "data_nascita" in results[0].columns
|
||||
|
||||
|
||||
def test_save_and_reload_roundtrip_preserves_sql_body(tmp_path):
|
||||
f = ConceptFormula(
|
||||
concept="fascia pediatrica",
|
||||
columns=["data_nascita"],
|
||||
sql="CASE\n WHEN x THEN 1\nEND",
|
||||
status="draft",
|
||||
sources=[],
|
||||
)
|
||||
path = save_formula(tmp_path, f)
|
||||
assert path.exists()
|
||||
# the file is frontmatter YAML + SQL body
|
||||
text = path.read_text()
|
||||
assert text.startswith("---")
|
||||
assert "concept: fascia pediatrica" in text
|
||||
assert "CASE" in text # SQL body preserved
|
||||
|
||||
|
||||
def test_retrieve_multiple_formulas_for_concept(tmp_path):
|
||||
# two competing formulas for the same concept (different sources/status)
|
||||
save_formula(tmp_path, ConceptFormula(concept="ablazione", columns=["flag"],
|
||||
sql="SELECT 1", status="draft", sources=["a"]))
|
||||
save_formula(tmp_path, ConceptFormula(concept="ablazione", columns=["testo"],
|
||||
sql="SELECT 2", status="reviewed", sources=["b"]))
|
||||
results = retrieve_formula(tmp_path, "ablazione")
|
||||
assert len(results) == 2
|
||||
statuses = {r.status for r in results}
|
||||
assert statuses == {"draft", "reviewed"}
|
||||
|
||||
|
||||
def test_retrieve_empty_when_no_match(tmp_path):
|
||||
save_formula(tmp_path, ConceptFormula(concept="altro", columns=["c"],
|
||||
sql="SELECT 1", status="reviewed", sources=[]))
|
||||
assert retrieve_formula(tmp_path, "inesistente") == []
|
||||
|
||||
|
||||
def test_retrieve_empty_on_missing_dir(tmp_path):
|
||||
# no formulas dir at all -> empty list, not error
|
||||
assert retrieve_formula(tmp_path / "nope", "anything") == []
|
||||
|
||||
|
||||
def test_concept_formula_decision_types_exist():
|
||||
import typing
|
||||
|
||||
from tht.decisions import DecisionType
|
||||
|
||||
args = typing.get_args(DecisionType)
|
||||
assert "concept_formula_approved" in args
|
||||
assert "concept_formula_rejected" in args
|
||||
|
||||
|
||||
def test_concept_formula_default_status(tmp_path):
|
||||
# status has a sensible default so an author can write a draft quickly
|
||||
f = ConceptFormula(concept="x", columns=["c"], sql="SELECT 1")
|
||||
assert f.status == "draft" # not yet reviewed
|
||||
assert f.sources == []
|
||||
|
||||
|
||||
def test_reviewed_legacy_formula_becomes_curated_formula_with_stable_provenance():
|
||||
formula = ConceptFormula(
|
||||
concept="fascia pediatrica",
|
||||
columns=["clinical.patient.birth_date"],
|
||||
sql="CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END",
|
||||
status="reviewed",
|
||||
sources=["Regola clinica approvata dal gruppo pediatrico."],
|
||||
)
|
||||
|
||||
migrated = formula_store.legacy_formula_to_curated(
|
||||
formula,
|
||||
legacy_path="formulas/fascia-pediatrica-1.sql.md",
|
||||
source_content=formula.dump(),
|
||||
)
|
||||
|
||||
assert migrated is not None
|
||||
assert migrated.id.startswith("evidence:fascia-pediatrica-")
|
||||
assert migrated.title == "Fascia pediatrica"
|
||||
assert migrated.kind == "formula"
|
||||
assert migrated.payload.concept == formula.concept
|
||||
assert migrated.payload.columns == ("clinical.patient.birth_date",)
|
||||
assert migrated.payload.sql == formula.sql
|
||||
assert migrated.provenance.source_file == "source/formulas/fascia-pediatrica-1.sql.md"
|
||||
assert migrated.provenance.supporting_excerpts == tuple(formula.sources)
|
||||
assert migrated.review_items == ()
|
||||
assert formula_store.legacy_formula_to_curated(
|
||||
formula,
|
||||
legacy_path="formulas/fascia-pediatrica-1.sql.md",
|
||||
source_content=formula.dump(),
|
||||
).id == migrated.id
|
||||
|
||||
|
||||
def test_reviewed_legacy_formulas_with_the_same_concept_keep_distinct_path_identities():
|
||||
first = ConceptFormula(
|
||||
concept="fascia pediatrica",
|
||||
columns=["clinical.patient.birth_date"],
|
||||
sql="CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END",
|
||||
status="reviewed",
|
||||
sources=["Regola legacy revisionata."],
|
||||
)
|
||||
second = ConceptFormula(
|
||||
concept="fascia pediatrica",
|
||||
columns=["clinical.patient.birth_date"],
|
||||
sql="CASE WHEN age < 16 THEN 'pediatrica' ELSE 'adulta' END",
|
||||
status="reviewed",
|
||||
sources=["Regola legacy revisionata."],
|
||||
)
|
||||
|
||||
first_migration = formula_store.legacy_formula_to_curated(
|
||||
first,
|
||||
legacy_path="formulas/fascia-pediatrica-1.sql.md",
|
||||
source_content=first.dump(),
|
||||
)
|
||||
second_migration = formula_store.legacy_formula_to_curated(
|
||||
second,
|
||||
legacy_path="formulas/fascia-pediatrica-2.sql.md",
|
||||
source_content=second.dump(),
|
||||
)
|
||||
|
||||
assert first_migration is not None
|
||||
assert second_migration is not None
|
||||
assert first_migration.id != second_migration.id
|
||||
assert first_migration.id.startswith("evidence:fascia-pediatrica-")
|
||||
assert second_migration.id.startswith("evidence:fascia-pediatrica-")
|
||||
|
||||
|
||||
def test_session_formula_proposal_is_versioned_and_is_not_published_evidence(tmp_path):
|
||||
linking = SchemaLinking(
|
||||
question="Conta i pazienti pediatrici",
|
||||
@@ -199,12 +60,18 @@ def test_session_formula_proposals_require_an_unretracted_positive_f4_decision(t
|
||||
)
|
||||
decisions = [
|
||||
DecisionRecord(
|
||||
seq=3, ts=datetime(2026, 8, 25, tzinfo=UTC), type="concept_formula_approved",
|
||||
subject="phase:4", detail="approvata",
|
||||
seq=3,
|
||||
ts=datetime(2026, 8, 25, tzinfo=UTC),
|
||||
type="concept_formula_approved",
|
||||
subject="phase:4",
|
||||
detail="approvata",
|
||||
),
|
||||
DecisionRecord(
|
||||
seq=4, ts=datetime(2026, 8, 25, tzinfo=UTC), type="concept_formula_rejected",
|
||||
subject="phase:4", detail="rifiutata",
|
||||
seq=4,
|
||||
ts=datetime(2026, 8, 25, tzinfo=UTC),
|
||||
type="concept_formula_rejected",
|
||||
subject="phase:4",
|
||||
detail="rifiutata",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -1,63 +1,14 @@
|
||||
"""D14b wiring: status=auto, search_formulas, and evidence loader skips formula files.
|
||||
"""L1: Formula Evidence search uses only the typed, published Evidence surface."""
|
||||
|
||||
Completes the formula layer beyond the store: the `auto` status the spec requires
|
||||
(§4.7.2), the concept-substring retrieval that `tht search find --kind formula` uses,
|
||||
and the guarantee that load_evidence_dir does NOT choke on *.sql.md formula files when
|
||||
they live under the evidence root.
|
||||
"""
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from tht.cli import app, search_cmd
|
||||
from tht.evidence import formula_store
|
||||
from tht.evidence.formula_store import ConceptFormula, save_formula, search_formulas
|
||||
from tht.evidence.model import EvidenceDoc, load_evidence_dir
|
||||
from tht.evidence.search import EvidenceSearchOutcome
|
||||
|
||||
|
||||
def test_status_auto_is_valid():
|
||||
f = ConceptFormula(concept="x", sql="SELECT 1", status="auto")
|
||||
assert f.status == "auto"
|
||||
# round-trips through parse/dump
|
||||
assert ConceptFormula.parse(f.dump()).status == "auto"
|
||||
|
||||
|
||||
def test_search_formulas_substring_case_insensitive(tmp_path):
|
||||
save_formula(tmp_path, ConceptFormula(concept="fascia pediatrica", sql="SELECT 1"))
|
||||
save_formula(tmp_path, ConceptFormula(concept="indice di Charlson", sql="SELECT 2"))
|
||||
hits = search_formulas(tmp_path, "PEDIATRICA")
|
||||
assert len(hits) == 1
|
||||
assert hits[0].concept == "fascia pediatrica"
|
||||
assert search_formulas(tmp_path, "charlson")[0].concept == "indice di Charlson"
|
||||
|
||||
|
||||
def test_load_evidence_dir_skips_formula_files(tmp_path):
|
||||
# a real evidence doc + a formula file under the same root
|
||||
(tmp_path / "ev1.md").write_text(
|
||||
"---\nid: ev1\ntitle: T\n---\nbody text\n"
|
||||
)
|
||||
save_formula(tmp_path, ConceptFormula(concept="ablazione", sql="SELECT 1"))
|
||||
docs = load_evidence_dir(tmp_path)
|
||||
ids = [d.id for d in docs]
|
||||
assert ids == ["ev1"] # the .sql.md formula file is skipped, no crash
|
||||
assert all(isinstance(d, EvidenceDoc) for d in docs)
|
||||
|
||||
|
||||
def test_unreviewed_legacy_formulas_cannot_become_curated_evidence():
|
||||
for status in ("auto", "draft"):
|
||||
formula = ConceptFormula(
|
||||
concept="fascia pediatrica",
|
||||
columns=["clinical.patient.birth_date"],
|
||||
sql="CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END",
|
||||
status=status,
|
||||
)
|
||||
assert formula_store.legacy_formula_to_curated(
|
||||
formula, legacy_path="formulas/pediatric-1.sql.md",
|
||||
) is None
|
||||
|
||||
|
||||
def test_formula_search_uses_typed_evidence_with_a_formula_constraint():
|
||||
class Searcher:
|
||||
vector_generation = "gen:" + "a" * 32
|
||||
@@ -68,8 +19,11 @@ def test_formula_search_uses_typed_evidence_with_a_formula_constraint():
|
||||
def search(self, embedding, **kwargs):
|
||||
self.calls.append((embedding, kwargs))
|
||||
return [SimpleNamespace(
|
||||
id="fragment:formula", similarity=0.9, content="formula excerpt",
|
||||
title="Fascia pediatrica", metadata={
|
||||
id="fragment:formula",
|
||||
similarity=0.9,
|
||||
content="formula excerpt",
|
||||
title="Fascia pediatrica",
|
||||
metadata={
|
||||
"evidence_id": "evidence:fascia-pediatrica",
|
||||
"evidence_kind": "formula",
|
||||
"document_id": "doc:formula",
|
||||
@@ -89,16 +43,26 @@ def test_formula_search_uses_typed_evidence_with_a_formula_constraint():
|
||||
)
|
||||
|
||||
assert outcome.status == "available"
|
||||
assert [result.evidence_id for result in outcome.results] == ["evidence:fascia-pediatrica"]
|
||||
assert [result.evidence_id for result in outcome.results] == [
|
||||
"evidence:fascia-pediatrica",
|
||||
]
|
||||
assert searcher.calls[0][1]["metadata_filter"]["required_kinds"] == ["formula"]
|
||||
|
||||
|
||||
def test_formula_search_json_is_pristine_while_human_output_warns_about_legacy_store(monkeypatch):
|
||||
monkeypatch.setattr(search_cmd, "_load_config_or_exit", lambda _path: SimpleNamespace(embeddings=object()))
|
||||
def test_formula_search_json_is_pristine_and_human_output_uses_only_published_evidence(
|
||||
monkeypatch,
|
||||
):
|
||||
monkeypatch.setattr(
|
||||
search_cmd,
|
||||
"_load_config_or_exit",
|
||||
lambda _path: SimpleNamespace(embeddings=object()),
|
||||
)
|
||||
monkeypatch.setattr(search_cmd, "workspace_id_for_config", lambda _cfg, _path: "workspace-a")
|
||||
monkeypatch.setattr(search_cmd, "search_formula_evidence", lambda *args, **kwargs: EvidenceSearchOutcome(
|
||||
"available", "gen:" + "a" * 32,
|
||||
))
|
||||
monkeypatch.setattr(
|
||||
search_cmd,
|
||||
"search_formula_evidence",
|
||||
lambda *args, **kwargs: EvidenceSearchOutcome("available", "gen:" + "a" * 32),
|
||||
)
|
||||
monkeypatch.setattr("tht.cli.vector_cmd.require_vector_cfg", lambda _cfg: None)
|
||||
monkeypatch.setattr("tht.cli.vector_cmd.open_searcher", lambda _cfg: object())
|
||||
monkeypatch.setattr("tht.cli.vector_cmd.make_embedder", lambda _cfg: object())
|
||||
@@ -114,6 +78,6 @@ def test_formula_search_json_is_pristine_while_human_output_warns_about_legacy_s
|
||||
|
||||
assert json_result.exit_code == 0
|
||||
assert json.loads(json_result.stdout) == []
|
||||
assert "ATTENZIONE" not in json_result.stdout
|
||||
assert human_result.exit_code == 0
|
||||
assert "ATTENZIONE" in human_result.output
|
||||
assert "ATTENZIONE" not in human_result.output
|
||||
assert "Nessuna formula" in human_result.output
|
||||
|
||||
Reference in New Issue
Block a user