refactor(evidence): unify formulas with typed evidence
This commit is contained in:
@@ -348,6 +348,14 @@ Prerequisite: Phase 3 closed.
|
|||||||
"<concept>"` (or derive it from the evidence/context), present it, and let the
|
"<concept>"` (or derive it from the evidence/context), present it, and let the
|
||||||
reviewer approve/reject (`concept_formula_approved`/`concept_formula_rejected`).
|
reviewer approve/reject (`concept_formula_approved`/`concept_formula_rejected`).
|
||||||
Reflect the approved formula in `schema_linking.json` (`concept_formulas`).
|
Reflect the approved formula in `schema_linking.json` (`concept_formulas`).
|
||||||
|
4. **Formula proposals.** A `kind=formula` result from Evidence search is Published
|
||||||
|
Evidence and can be cited with its provenance. If no published formula is suitable and
|
||||||
|
you synthesize one for this question, present it to the reviewer and, after their F4
|
||||||
|
decision, include `{concept, columns, sql, sources}` in `concept_formulas`. This creates
|
||||||
|
a schema-versioned, **session-only Formula proposal**: it helps this session but is not
|
||||||
|
Published Evidence, has no `evidence:` ID, is not returned by runtime search, and never
|
||||||
|
writes to the workspace repository. A curator must separately import, review, and
|
||||||
|
publish it before another session can treat it as Evidence.
|
||||||
5. Persist the **joins** (and any `concept_formulas`/`open_questions`) with the gate's
|
5. Persist the **joins** (and any `concept_formulas`/`open_questions`) with the gate's
|
||||||
`write_schema_linking` tool — it validates the object against the `SchemaLinking`
|
`write_schema_linking` tool — it validates the object against the `SchemaLinking`
|
||||||
model and writes the file deterministically (never hand-write it, never edit it
|
model and writes the file deterministically (never hand-write it, never edit it
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
4. **Formula proposals.** A `kind=formula` result from Evidence search is Published
|
||||||
|
Evidence and can be cited with its provenance. If no published formula is suitable and
|
||||||
|
you synthesize one for this question, present it to the reviewer and, after their F4
|
||||||
|
decision, include `{concept, columns, sql, sources}` in `concept_formulas`. This creates
|
||||||
|
a schema-versioned, **session-only Formula proposal**: it helps this session but is not
|
||||||
|
Published Evidence, has no `evidence:` ID, is not returned by runtime search, and never
|
||||||
|
writes to the workspace repository. A curator must separately import, review, and
|
||||||
|
publish it before another session can treat it as Evidence.
|
||||||
@@ -216,6 +216,7 @@ Prerequisite: Phase 3 closed.
|
|||||||
those to joins you derive yourself, and flag to the reviewer any join you need
|
those to joins you derive yourself, and flag to the reviewer any join you need
|
||||||
that is NOT in the list.
|
that is NOT in the list.
|
||||||
{{DISAMBIGUATION_SCHEMA_GROUNDING}}
|
{{DISAMBIGUATION_SCHEMA_GROUNDING}}
|
||||||
|
{{EVIDENCE_FORMULA_PROPOSALS}}
|
||||||
5. Persist the **joins** (and any `concept_formulas`/`open_questions`) with the gate's
|
5. Persist the **joins** (and any `concept_formulas`/`open_questions`) with the gate's
|
||||||
`write_schema_linking` tool — it validates the object against the `SchemaLinking`
|
`write_schema_linking` tool — it validates the object against the `SchemaLinking`
|
||||||
model and writes the file deterministically (never hand-write it, never edit it
|
model and writes the file deterministically (never hand-write it, never edit it
|
||||||
|
|||||||
@@ -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.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):
|
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")
|
f = ConceptFormula(concept="x", columns=["c"], sql="SELECT 1")
|
||||||
assert f.status == "draft" # not yet reviewed
|
assert f.status == "draft" # not yet reviewed
|
||||||
assert f.sources == []
|
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
|
and the guarantee that load_evidence_dir does NOT choke on *.sql.md formula files when
|
||||||
they live under the evidence root.
|
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.formula_store import ConceptFormula, save_formula, search_formulas
|
||||||
from tht.evidence.model import EvidenceDoc, load_evidence_dir
|
from tht.evidence.model import EvidenceDoc, load_evidence_dir
|
||||||
|
from tht.evidence.search import EvidenceSearchOutcome
|
||||||
|
|
||||||
|
|
||||||
def test_status_auto_is_valid():
|
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]
|
ids = [d.id for d in docs]
|
||||||
assert ids == ["ev1"] # the .sql.md formula file is skipped, no crash
|
assert ids == ["ev1"] # the .sql.md formula file is skipped, no crash
|
||||||
assert all(isinstance(d, EvidenceDoc) for d in docs)
|
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,
|
render_projection,
|
||||||
)
|
)
|
||||||
|
|
||||||
BASELINE_SHA256 = "bb6daa6fe83d22f5e701025c5334d72ec9eb35349e90d24cb9d7f6290d0fecfe"
|
BASELINE_SHA256 = "62bfa0dbc1179b43b2d80dc48155a6119421488a1ffc160e9c664f1fe280ce52"
|
||||||
|
|
||||||
|
|
||||||
def test_modular_pi_skill_renders_the_byte_identical_approved_projection():
|
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"),
|
("{{DISAMBIGUATION_REWRITING_INSTRUCTIONS}}", "disambiguation/phase-3.md"),
|
||||||
("{{MEMORY_SOLVED_SEARCH_F4}}", "memory/solved-search-f4.md"),
|
("{{MEMORY_SOLVED_SEARCH_F4}}", "memory/solved-search-f4.md"),
|
||||||
("{{DISAMBIGUATION_SCHEMA_GROUNDING}}", "disambiguation/schema-grounding.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_F6}}", "memory/solved-search-f6.md"),
|
||||||
("{{MEMORY_SOLVED_SEARCH_F7}}", "memory/solved-search-f7.md"),
|
("{{MEMORY_SOLVED_SEARCH_F7}}", "memory/solved-search-f7.md"),
|
||||||
("{{MEMORY_PROMOTION_F8}}", "memory/phase-8-promotion.md"),
|
("{{MEMORY_PROMOTION_F8}}", "memory/phase-8-promotion.md"),
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ KIND_MAP = {
|
|||||||
"evidence": ["evidence"],
|
"evidence": ["evidence"],
|
||||||
"schema": ["schema_table", "schema_column"],
|
"schema": ["schema_table", "schema_column"],
|
||||||
"values": [], # solo LSH
|
"values": [], # solo LSH
|
||||||
"formula": [], # solo formula store (D14b), niente LSH/vector
|
"formula": ["evidence"],
|
||||||
}
|
}
|
||||||
|
|
||||||
_STAGE_PURPOSES = {
|
_STAGE_PURPOSES = {
|
||||||
@@ -30,6 +30,20 @@ DEFAULT_TOP_FALLBACK = 10
|
|||||||
search_app = typer.Typer(help="Ricerca semantica (evidence/schema/values) nel vectorstore")
|
search_app = typer.Typer(help="Ricerca semantica (evidence/schema/values) nel vectorstore")
|
||||||
|
|
||||||
|
|
||||||
|
def search_formula_evidence(keyword: str, *, searcher, embedder, top: int):
|
||||||
|
"""Search only published Formula Evidence through the shared typed facade."""
|
||||||
|
from tht.evidence import EvidenceSearchContext, search_evidence
|
||||||
|
|
||||||
|
return search_evidence(
|
||||||
|
keyword,
|
||||||
|
"sql_generation",
|
||||||
|
EvidenceSearchContext(required_kinds=("formula",)),
|
||||||
|
searcher=searcher,
|
||||||
|
embedder=embedder,
|
||||||
|
top_n=top,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _leased_dwh_snapshot(cfg, context: typer.Context):
|
def _leased_dwh_snapshot(cfg, context: typer.Context):
|
||||||
from tht.jobs.dwh_pipeline import lease_dwh_snapshot
|
from tht.jobs.dwh_pipeline import lease_dwh_snapshot
|
||||||
|
|
||||||
@@ -143,12 +157,6 @@ def search_cmd(
|
|||||||
|
|
||||||
workspace_id = workspace_id_for_config(cfg, config)
|
workspace_id = workspace_id_for_config(cfg, config)
|
||||||
validate_corpus_workspace(cfg, workspace_id)
|
validate_corpus_workspace(cfg, workspace_id)
|
||||||
dwh_snapshot = _leased_dwh_snapshot(cfg, ctx)
|
|
||||||
require_vector_cfg(cfg)
|
|
||||||
runtime_searcher = active_searcher(
|
|
||||||
cfg, open_searcher(cfg),
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
)
|
|
||||||
if kind is not None and kind not in KIND_MAP:
|
if kind is not None and kind not in KIND_MAP:
|
||||||
typer.secho(
|
typer.secho(
|
||||||
f"ERRORE: --kind sconosciuto: {kind} (validi: {', '.join(KIND_MAP)})",
|
f"ERRORE: --kind sconosciuto: {kind} (validi: {', '.join(KIND_MAP)})",
|
||||||
@@ -160,29 +168,60 @@ def search_cmd(
|
|||||||
top = cfg.search.top_schema_tables if kind == "schema" else DEFAULT_TOP_FALLBACK
|
top = cfg.search.top_schema_tables if kind == "schema" else DEFAULT_TOP_FALLBACK
|
||||||
|
|
||||||
if kind == "formula":
|
if kind == "formula":
|
||||||
# D14b: recupero formule di concetto dallo store locale (niente LSH/vector).
|
require_vector_cfg(cfg)
|
||||||
from tht.cli.evidence_cmd import evidence_root
|
outcome = search_formula_evidence(
|
||||||
from tht.evidence.formula_store import search_formulas
|
keyword,
|
||||||
|
searcher=active_searcher(cfg, open_searcher(cfg), workspace_id=workspace_id),
|
||||||
formulas = search_formulas(evidence_root(cfg), keyword)[:top]
|
embedder=make_embedder(cfg.embeddings),
|
||||||
|
top=top,
|
||||||
|
)
|
||||||
|
if outcome.status == "unavailable":
|
||||||
|
payload = {"status": outcome.status, "code": outcome.code, "message": outcome.message}
|
||||||
if json_out:
|
if json_out:
|
||||||
typer.echo(json.dumps(
|
typer.echo(json.dumps(payload, ensure_ascii=False))
|
||||||
[f.model_dump(mode="json") for f in formulas], ensure_ascii=False, indent=2))
|
else:
|
||||||
|
typer.secho(f"ERRORE: {outcome.message}", fg=typer.colors.RED, err=True)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
if json_out:
|
||||||
|
typer.echo(json.dumps([
|
||||||
|
{
|
||||||
|
"evidence_id": result.evidence_id,
|
||||||
|
"title": result.title,
|
||||||
|
"kind": result.kind,
|
||||||
|
"excerpts": list(result.excerpts),
|
||||||
|
"provenance": result.provenance,
|
||||||
|
"citation": result.citation,
|
||||||
|
"document_id": result.document_id,
|
||||||
|
}
|
||||||
|
for result in outcome.results
|
||||||
|
], ensure_ascii=False, indent=2))
|
||||||
return
|
return
|
||||||
if not formulas:
|
typer.secho(
|
||||||
|
"ATTENZIONE: lo store formule legacy non viene più consultato; "
|
||||||
|
"sono disponibili solo Formula Evidence pubblicate.",
|
||||||
|
fg=typer.colors.YELLOW,
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
if not outcome.results:
|
||||||
typer.secho(f"Nessuna formula per '{keyword}'.", fg=typer.colors.YELLOW)
|
typer.secho(f"Nessuna formula per '{keyword}'.", fg=typer.colors.YELLOW)
|
||||||
return
|
return
|
||||||
table = Table(title=f"Formule per '{keyword}'")
|
table = Table(title=f"Formula Evidence per '{keyword}'")
|
||||||
table.add_column("Concetto")
|
table.add_column("Formula")
|
||||||
table.add_column("Status")
|
table.add_column("Provenienza")
|
||||||
table.add_column("Colonne")
|
table.add_column("Estratto")
|
||||||
table.add_column("SQL")
|
for result in outcome.results:
|
||||||
for f in formulas:
|
excerpt = result.excerpts[0] if result.excerpts else ""
|
||||||
sql_preview = (f.sql[:80] + "…") if len(f.sql) > 80 else f.sql
|
table.add_row(result.title, result.citation, excerpt[:120])
|
||||||
table.add_row(f.concept, f.status, ", ".join(f.columns), sql_preview)
|
|
||||||
Console().print(table)
|
Console().print(table)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
dwh_snapshot = _leased_dwh_snapshot(cfg, ctx)
|
||||||
|
require_vector_cfg(cfg)
|
||||||
|
runtime_searcher = active_searcher(
|
||||||
|
cfg, open_searcher(cfg),
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
)
|
||||||
|
|
||||||
lsh_hits = None
|
lsh_hits = None
|
||||||
try:
|
try:
|
||||||
lsh, minhashes, meta = load_index(
|
lsh, minhashes, meta = load_index(
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
"""SQL concept->formula evidence store (spec D14b, §4.7).
|
"""Legacy ConceptFormula reader and one-way migration into Curated Evidence.
|
||||||
|
|
||||||
A concept (e.g. 'fascia pediatrica', 'ablazione') maps to a reusable SQL formula
|
The ``formulas/*.sql.md`` store is retained only for the migration window. Runtime
|
||||||
(a CASE WHEN ...) that derives it from physical columns. These are reviewable
|
lookup uses typed, published ``kind=formula`` Evidence instead. A session reviewer
|
||||||
units: the gate surfaces a candidate formula, the reviewer approves or rejects it
|
may still approve a formula locally; that is a proposal, not publication.
|
||||||
(decision types concept_formula_approved / concept_formula_rejected), and approved
|
|
||||||
formulas travel with the schema-linking artifact.
|
|
||||||
|
|
||||||
Storage: one file per formula, frontmatter YAML + SQL body (same shape as
|
|
||||||
EvidenceDoc.parse). Directory layout: <root>/formulas/<slug>-<n>.sql.md.
|
|
||||||
Retrieve is by concept (may return several, e.g. competing drafts vs reviewed).
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
@@ -19,6 +14,8 @@ from typing import Literal
|
|||||||
import yaml
|
import yaml
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from tht.evidence.canonical import CuratedEvidence
|
||||||
|
|
||||||
FORMULAS_SUBDIR = "formulas"
|
FORMULAS_SUBDIR = "formulas"
|
||||||
_SUFFIX_RE = re.compile(r"^(.*?)-(\d+)\.sql\.md$")
|
_SUFFIX_RE = re.compile(r"^(.*?)-(\d+)\.sql\.md$")
|
||||||
|
|
||||||
@@ -104,8 +101,45 @@ def retrieve_formula(root: Path | str, concept: str) -> list[ConceptFormula]:
|
|||||||
|
|
||||||
|
|
||||||
def search_formulas(root: Path | str, query: str) -> list[ConceptFormula]:
|
def search_formulas(root: Path | str, query: str) -> list[ConceptFormula]:
|
||||||
"""Formulas whose concept contains `query` (case-insensitive). Used by
|
"""Read legacy formulas for migration tooling only (case-insensitive concept match)."""
|
||||||
`tht search find --kind formula` (D14b retrieval, §4.7.2): the reviewer searches a
|
|
||||||
concept term and gets the candidate formulas to approve before they reach the CTE."""
|
|
||||||
q = query.strip().lower()
|
q = query.strip().lower()
|
||||||
return [f for f in _load_all(Path(root)) if q in f.concept.lower()]
|
return [f for f in _load_all(Path(root)) if q in f.concept.lower()]
|
||||||
|
|
||||||
|
|
||||||
|
def legacy_formula_to_curated(
|
||||||
|
formula: ConceptFormula,
|
||||||
|
*,
|
||||||
|
legacy_path: str,
|
||||||
|
) -> CuratedEvidence | None:
|
||||||
|
"""Convert one reviewed legacy formula into its deterministic curated counterpart.
|
||||||
|
|
||||||
|
Drafts and model-generated formulas have no global publication status. Their
|
||||||
|
caller must project them as session-local Formula proposals instead.
|
||||||
|
"""
|
||||||
|
if formula.status != "reviewed":
|
||||||
|
return None
|
||||||
|
source_notes = tuple(formula.sources) or (
|
||||||
|
"Legacy formula migrated without a recorded provenance note.",
|
||||||
|
)
|
||||||
|
source_sha256 = hashlib.sha256(formula.dump().encode("utf-8")).hexdigest()
|
||||||
|
source_file = legacy_path if legacy_path.startswith("source/") else f"source/{legacy_path}"
|
||||||
|
return CuratedEvidence.model_validate({
|
||||||
|
"schema_version": 1,
|
||||||
|
"id": f"evidence:{formula._slug}",
|
||||||
|
"title": formula.concept[:1].upper() + formula.concept[1:],
|
||||||
|
"kind": "formula",
|
||||||
|
"purposes": ["schema_linking", "sql_generation"],
|
||||||
|
"applies_to": {"concepts": [formula.concept], "columns": formula.columns},
|
||||||
|
"language": "it",
|
||||||
|
"provenance": {
|
||||||
|
"source_file": source_file,
|
||||||
|
"source_sha256": f"sha256:{source_sha256}",
|
||||||
|
"supporting_excerpts": source_notes,
|
||||||
|
},
|
||||||
|
"review_items": [],
|
||||||
|
"payload": {
|
||||||
|
"concept": formula.concept,
|
||||||
|
"columns": formula.columns,
|
||||||
|
"sql": formula.sql,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|||||||
@@ -55,7 +55,45 @@ def project_session(
|
|||||||
"esito": "accettata" if decision.type == "evidence_accepted" else "scartata",
|
"esito": "accettata" if decision.type == "evidence_accepted" else "scartata",
|
||||||
"decision_seq": decision.seq,
|
"decision_seq": decision.seq,
|
||||||
}
|
}
|
||||||
return list(entries.values())
|
return list(entries.values()) + _formula_proposals(decisions, linking)
|
||||||
|
|
||||||
|
|
||||||
|
def _formula_proposals(decisions: list["DecisionRecord"], linking: "SchemaLinking") -> list[dict]:
|
||||||
|
"""Project locally approved F4 formulas without representing them as Evidence.
|
||||||
|
|
||||||
|
A proposal remains in the persisted session artifact until a separate curator
|
||||||
|
imports, reviews, and publishes it in the workspace repository.
|
||||||
|
"""
|
||||||
|
approved = {
|
||||||
|
decision.detail: decision.seq
|
||||||
|
for decision in decisions
|
||||||
|
if decision.type == "concept_formula_approved" and decision.detail
|
||||||
|
}
|
||||||
|
proposals = []
|
||||||
|
for formula in linking.concept_formulas:
|
||||||
|
if not isinstance(formula, dict):
|
||||||
|
continue
|
||||||
|
concept = formula.get("concept")
|
||||||
|
sql = formula.get("sql")
|
||||||
|
columns = formula.get("columns")
|
||||||
|
if not isinstance(concept, str) or not isinstance(sql, str) or not isinstance(columns, list):
|
||||||
|
continue
|
||||||
|
# A referenced published Formula Evidence is already represented by its
|
||||||
|
# Evidence receipt/citation, so it must not be reintroduced as a proposal.
|
||||||
|
evidence_id = formula.get("evidence_id", formula.get("id", ""))
|
||||||
|
if isinstance(evidence_id, str) and evidence_id.startswith("evidence:"):
|
||||||
|
continue
|
||||||
|
proposals.append({
|
||||||
|
"schema_version": 1,
|
||||||
|
"kind": "formula_proposal",
|
||||||
|
"publication": "session_only",
|
||||||
|
"concept": concept,
|
||||||
|
"columns": columns,
|
||||||
|
"sql": sql,
|
||||||
|
"sources": formula.get("sources", []),
|
||||||
|
"decision_seq": approved.get(concept),
|
||||||
|
})
|
||||||
|
return proposals
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ FRAGMENT_ORDER = (
|
|||||||
("{{DISAMBIGUATION_REWRITING_INSTRUCTIONS}}", "disambiguation/phase-3.md"),
|
("{{DISAMBIGUATION_REWRITING_INSTRUCTIONS}}", "disambiguation/phase-3.md"),
|
||||||
("{{MEMORY_SOLVED_SEARCH_F4}}", "memory/solved-search-f4.md"),
|
("{{MEMORY_SOLVED_SEARCH_F4}}", "memory/solved-search-f4.md"),
|
||||||
("{{DISAMBIGUATION_SCHEMA_GROUNDING}}", "disambiguation/schema-grounding.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_F6}}", "memory/solved-search-f6.md"),
|
||||||
("{{MEMORY_SOLVED_SEARCH_F7}}", "memory/solved-search-f7.md"),
|
("{{MEMORY_SOLVED_SEARCH_F7}}", "memory/solved-search-f7.md"),
|
||||||
("{{MEMORY_PROMOTION_F8}}", "memory/phase-8-promotion.md"),
|
("{{MEMORY_PROMOTION_F8}}", "memory/phase-8-promotion.md"),
|
||||||
|
|||||||
Reference in New Issue
Block a user