refactor(evidence): unify formulas with typed evidence
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
"""Migration boundary: legacy formulas become curated evidence or session proposals."""
|
||||
|
||||
from tht.evidence import formula_store
|
||||
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."],
|
||||
)
|
||||
|
||||
first = formula_store.legacy_formula_to_curated(
|
||||
formula, legacy_path="formulas/fascia-pediatrica-1.sql.md",
|
||||
)
|
||||
second = formula_store.legacy_formula_to_curated(
|
||||
formula, legacy_path="formulas/fascia-pediatrica-1.sql.md",
|
||||
)
|
||||
|
||||
assert first is not None
|
||||
assert second is not None
|
||||
assert first.provenance.source_sha256 == second.provenance.source_sha256
|
||||
assert first.provenance.source_sha256.startswith("sha256:")
|
||||
@@ -8,7 +8,13 @@ part of the schema-linking artifact. The store is frontmatter-YAML + SQL body.
|
||||
"""
|
||||
|
||||
|
||||
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):
|
||||
@@ -82,3 +88,63 @@ def test_concept_formula_default_status(tmp_path):
|
||||
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",
|
||||
)
|
||||
|
||||
assert migrated is not None
|
||||
assert migrated.id == "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",
|
||||
).id == migrated.id
|
||||
|
||||
|
||||
def test_session_formula_proposal_is_versioned_and_is_not_published_evidence(tmp_path):
|
||||
linking = SchemaLinking(
|
||||
question="Conta i pazienti pediatrici",
|
||||
concept_formulas=[{
|
||||
"concept": "fascia pediatrica",
|
||||
"columns": ["clinical.patient.birth_date"],
|
||||
"sql": "CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END",
|
||||
"sources": ["Sintetizzata nella sessione"],
|
||||
}],
|
||||
)
|
||||
decisions = [DecisionRecord(
|
||||
seq=9,
|
||||
ts=datetime(2026, 8, 25, tzinfo=UTC),
|
||||
type="concept_formula_approved",
|
||||
subject="phase:4",
|
||||
detail="fascia pediatrica",
|
||||
)]
|
||||
|
||||
projected = project_session(decisions, linking, tmp_path / "evidence")
|
||||
|
||||
assert projected == [{
|
||||
"schema_version": 1,
|
||||
"kind": "formula_proposal",
|
||||
"publication": "session_only",
|
||||
"concept": "fascia pediatrica",
|
||||
"columns": ["clinical.patient.birth_date"],
|
||||
"sql": "CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END",
|
||||
"sources": ["Sintetizzata nella sessione"],
|
||||
"decision_seq": 9,
|
||||
}]
|
||||
|
||||
@@ -5,8 +5,16 @@ Completes the formula layer beyond the store: the `auto` status the spec require
|
||||
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():
|
||||
@@ -35,3 +43,77 @@ def test_load_evidence_dir_skips_formula_files(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
|
||||
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
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={
|
||||
"evidence_id": "evidence:fascia-pediatrica",
|
||||
"evidence_kind": "formula",
|
||||
"document_id": "doc:formula",
|
||||
"ordinal": 0,
|
||||
"source_uri": "file:///curated/formula/fascia-pediatrica.md",
|
||||
"provenance": {},
|
||||
},
|
||||
)]
|
||||
|
||||
class Embedder:
|
||||
def embed_query(self, query):
|
||||
return [0.25]
|
||||
|
||||
searcher = Searcher()
|
||||
outcome = search_cmd.search_formula_evidence(
|
||||
"fascia pediatrica", searcher=searcher, embedder=Embedder(), top=3,
|
||||
)
|
||||
|
||||
assert outcome.status == "available"
|
||||
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()))
|
||||
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("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())
|
||||
monkeypatch.setattr("tht.evidence.active_searcher", lambda *args, **kwargs: object())
|
||||
monkeypatch.setattr("tht.evidence.validate_corpus_workspace", lambda _cfg, _workspace: None)
|
||||
|
||||
json_result = CliRunner().invoke(
|
||||
app, ["search", "find", "fascia pediatrica", "--kind", "formula", "--top", "1", "--json"],
|
||||
)
|
||||
human_result = CliRunner().invoke(
|
||||
app, ["search", "find", "fascia pediatrica", "--kind", "formula", "--top", "1"],
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -9,7 +9,7 @@ from tht.pi_skill_projection import (
|
||||
render_projection,
|
||||
)
|
||||
|
||||
BASELINE_SHA256 = "bb6daa6fe83d22f5e701025c5334d72ec9eb35349e90d24cb9d7f6290d0fecfe"
|
||||
BASELINE_SHA256 = "62bfa0dbc1179b43b2d80dc48155a6119421488a1ffc160e9c664f1fe280ce52"
|
||||
|
||||
|
||||
def test_modular_pi_skill_renders_the_byte_identical_approved_projection():
|
||||
@@ -23,6 +23,7 @@ def test_modular_pi_skill_renders_the_byte_identical_approved_projection():
|
||||
("{{DISAMBIGUATION_REWRITING_INSTRUCTIONS}}", "disambiguation/phase-3.md"),
|
||||
("{{MEMORY_SOLVED_SEARCH_F4}}", "memory/solved-search-f4.md"),
|
||||
("{{DISAMBIGUATION_SCHEMA_GROUNDING}}", "disambiguation/schema-grounding.md"),
|
||||
("{{EVIDENCE_FORMULA_PROPOSALS}}", "evidence/formula-proposals.md"),
|
||||
("{{MEMORY_SOLVED_SEARCH_F6}}", "memory/solved-search-f6.md"),
|
||||
("{{MEMORY_SOLVED_SEARCH_F7}}", "memory/solved-search-f7.md"),
|
||||
("{{MEMORY_PROMOTION_F8}}", "memory/phase-8-promotion.md"),
|
||||
|
||||
Reference in New Issue
Block a user