feat(tht): sync-schema-linking projects F4 ledger into schema_linking.json
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
committed by
Marco Pancotti
co-authored by
Claude Opus 4.8
parent
1df5d40d05
commit
00365cc6e6
@@ -0,0 +1,63 @@
|
|||||||
|
"""Tests for the F4 ledger -> schema_linking.json deterministic projection."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
from tht.config import DatabaseConfig
|
||||||
|
from tht.decisions import append_decision
|
||||||
|
from tht.session.store import create_session, set_schema_linking, sync_schema_linking
|
||||||
|
|
||||||
|
|
||||||
|
def _db():
|
||||||
|
return DatabaseConfig(
|
||||||
|
database="testdb", user="u", password="p", # noqa: S106
|
||||||
|
**{"schema": "public"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_projection_from_ledger(tmp_path):
|
||||||
|
m = create_session("domanda X", _db(), tmp_path)
|
||||||
|
sid = m.id
|
||||||
|
sdir = tmp_path / sid
|
||||||
|
# seed a prior schema_linking with a join to prove joins are preserved
|
||||||
|
set_schema_linking(sid, {"question": "domanda X", "candidates": [], "joins": [
|
||||||
|
{"from": "dim_patient.cod_paz", "to": "fact_x.cod_paz"}], "excluded": []}, tmp_path)
|
||||||
|
append_decision(sdir, type="table_promoted", subject="dim_patient")
|
||||||
|
append_decision(sdir, type="column_promoted", subject="dim_patient.cod_paz")
|
||||||
|
append_decision(sdir, type="column_promoted", subject="dim_patient.nome")
|
||||||
|
append_decision(sdir, type="table_excluded", subject="fact_sost")
|
||||||
|
|
||||||
|
sync_schema_linking(sid, tmp_path)
|
||||||
|
|
||||||
|
data = json.loads((sdir / "schema_linking.json").read_text())
|
||||||
|
tabs = {(c["kind"], c["name"], c["decision"]) for c in data["candidates"]}
|
||||||
|
assert ("table", "dim_patient", "promoted") in tabs
|
||||||
|
assert ("column", "dim_patient.cod_paz", "promoted") in tabs
|
||||||
|
assert ("column", "dim_patient.nome", "promoted") in tabs
|
||||||
|
assert {"kind": "table", "name": "fact_sost"} in [
|
||||||
|
{"kind": e["kind"], "name": e["name"]} for e in data["excluded"]]
|
||||||
|
assert data["joins"], "existing joins must be preserved"
|
||||||
|
|
||||||
|
|
||||||
|
def test_projection_last_decision_wins(tmp_path):
|
||||||
|
m = create_session("domanda Y", _db(), tmp_path)
|
||||||
|
sid = m.id
|
||||||
|
sdir = tmp_path / sid
|
||||||
|
append_decision(sdir, type="table_promoted", subject="dim_x")
|
||||||
|
append_decision(sdir, type="table_excluded", subject="dim_x")
|
||||||
|
|
||||||
|
sync_schema_linking(sid, tmp_path)
|
||||||
|
|
||||||
|
data = json.loads((sdir / "schema_linking.json").read_text())
|
||||||
|
assert data["candidates"] == []
|
||||||
|
assert [e["name"] for e in data["excluded"]] == ["dim_x"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_projection_defaults_question_from_manifest(tmp_path):
|
||||||
|
m = create_session("domanda Z", _db(), tmp_path)
|
||||||
|
sid = m.id
|
||||||
|
|
||||||
|
path = sync_schema_linking(sid, tmp_path)
|
||||||
|
|
||||||
|
data = json.loads(path.read_text())
|
||||||
|
assert data["question"] == "domanda Z"
|
||||||
|
assert data["candidates"] == []
|
||||||
|
assert data["excluded"] == []
|
||||||
@@ -141,6 +141,20 @@ def set_schema_linking_cmd(
|
|||||||
typer.secho(f"OK: schema_linking.json aggiornato ({path}).", fg=typer.colors.GREEN)
|
typer.secho(f"OK: schema_linking.json aggiornato ({path}).", fg=typer.colors.GREEN)
|
||||||
|
|
||||||
|
|
||||||
|
@session_app.command("sync-schema-linking")
|
||||||
|
def sync_schema_linking_cmd(
|
||||||
|
session_id: str = typer.Argument(...),
|
||||||
|
config: Path = CONFIG_OPT,
|
||||||
|
) -> None:
|
||||||
|
"""Riproietta schema_linking.json dalle decisioni F4 del ledger (deterministico)."""
|
||||||
|
from tht.session.store import sync_schema_linking
|
||||||
|
|
||||||
|
cfg = _load_config_or_exit(config)
|
||||||
|
load_session_or_exit(cfg, session_id)
|
||||||
|
path = sync_schema_linking(session_id, cfg.paths.sessions)
|
||||||
|
typer.secho(f"OK: schema_linking.json riproiettato ({path}).", fg=typer.colors.GREEN)
|
||||||
|
|
||||||
|
|
||||||
@session_app.command("show")
|
@session_app.command("show")
|
||||||
def show_cmd(
|
def show_cmd(
|
||||||
session_id: str = typer.Argument(...),
|
session_id: str = typer.Argument(...),
|
||||||
|
|||||||
@@ -181,6 +181,51 @@ def set_schema_linking(
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def sync_schema_linking(session_id: str, sessions_root: Path) -> Path:
|
||||||
|
"""Project the effective F4 ledger decisions into schema_linking.json.
|
||||||
|
|
||||||
|
candidates/excluded are rebuilt from table_promoted/table_excluded +
|
||||||
|
column_promoted/column_excluded (last decision per subject wins). question,
|
||||||
|
joins, concept_formulas and open_questions are preserved from the existing
|
||||||
|
file when present. The reviewer's curation is thus authoritative and
|
||||||
|
deterministic (no model transcription)."""
|
||||||
|
from tht.phase import effective_decisions
|
||||||
|
|
||||||
|
session_dir = sessions_root / session_id
|
||||||
|
manifest = load_session(session_id, sessions_root)
|
||||||
|
|
||||||
|
existing: dict = {}
|
||||||
|
sl_path = session_dir / "schema_linking.json"
|
||||||
|
if sl_path.exists():
|
||||||
|
existing = json.loads(sl_path.read_text())
|
||||||
|
|
||||||
|
# last decision per subject wins (handles a re-run of the gate).
|
||||||
|
latest: dict[str, str] = {}
|
||||||
|
for d in effective_decisions(session_dir):
|
||||||
|
if d.type in ("table_promoted", "table_excluded", "column_promoted", "column_excluded"):
|
||||||
|
latest[d.subject] = d.type
|
||||||
|
|
||||||
|
candidates: list[dict] = []
|
||||||
|
excluded: list[dict] = []
|
||||||
|
for subject, dtype in latest.items():
|
||||||
|
is_column = "." in subject
|
||||||
|
kind = "column" if is_column else "table"
|
||||||
|
if dtype in ("table_promoted", "column_promoted"):
|
||||||
|
candidates.append({"kind": kind, "name": subject, "decision": "promoted"})
|
||||||
|
else:
|
||||||
|
excluded.append({"kind": kind, "name": subject})
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"question": existing.get("question") or manifest.question,
|
||||||
|
"candidates": candidates,
|
||||||
|
"joins": existing.get("joins", []),
|
||||||
|
"excluded": excluded,
|
||||||
|
"open_questions": existing.get("open_questions", []),
|
||||||
|
"concept_formulas": existing.get("concept_formulas", []),
|
||||||
|
}
|
||||||
|
return set_schema_linking(session_id, data, sessions_root)
|
||||||
|
|
||||||
|
|
||||||
def load_session(session_id: str, sessions_root: Path) -> SessionManifest:
|
def load_session(session_id: str, sessions_root: Path) -> SessionManifest:
|
||||||
path = sessions_root / session_id / MANIFEST
|
path = sessions_root / session_id / MANIFEST
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
|
|||||||
Reference in New Issue
Block a user