feat(replay): fixtures + augment script for v2 artifact-gate viewers
Add cte-plan-fixture.json, cte-result-fixture.json, and phase-summary-fixture.json — realistic Italian-language payloads for the cardiology DWH replay session, conforming exactly to contracts.md (A/B/C). Add augment-review-gates.mjs (pattern of augment-schema-linking.mjs): finds the F6 cte_plan gate, first CTE 1 cte_result gate, and Fase 5 phase-summary gate by title regex and swaps in the v2 artifact.data, so CtePlanViewer, CteResultViewer, and PhaseSummaryViewer render in the offline replay. Idempotent; exits 1 if an expected gate is missing. Document the augment scripts and re-extract note in tools/replay/README.md.
This commit is contained in:
+11
-1
@@ -109,7 +109,17 @@ I 20 gate della sessione 17:07: 4 `select` + 11 `confirm` + 5 `decide`.
|
||||
## File
|
||||
|
||||
- `extract.mjs` — estrattore fixture dal transcript Pi JSONL.
|
||||
- `augment-schema-linking.mjs` — riscrive il gate F4 tabelle/colonne come
|
||||
descrittore `schema-linking` (vedi `schema-linking-fixture.json`).
|
||||
- `augment-review-gates.mjs` — riscrive i gate F6 piano CTE, CTE 1 approvato e
|
||||
Fase 5 completata come descrittori v2 (`cte_plan`/`cte_result`/`phase`, vedi
|
||||
`cte-plan-fixture.json`, `cte-result-fixture.json`,
|
||||
`phase-summary-fixture.json`). **Nota:** questi tre gate sono ancora
|
||||
fixture sintetiche coerenti coi contratti v2 — dopo la prima sessione live
|
||||
che passa per un gate `cte_plan`/`cte_result`/`phase` reale, rilancia
|
||||
`extract.mjs` per catturare i descrittori v2 reali e sostituire le fixture.
|
||||
- `server.mjs` — server HTTP+SSE standalone (node:http, zero deps).
|
||||
- `replay.json` — fixture generata (committata per comodità).
|
||||
- `replay.json` — fixture generata (committata per comodità; rigenerata con
|
||||
`augment-schema-linking.mjs` poi `augment-review-gates.mjs`, in quest'ordine).
|
||||
- `web/` — bundle SPA costruito da `frontend/` con
|
||||
`VITE_BACKEND_URL=http://localhost:5333` (gitignorato; prodotto da `build`).
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Rewrite three replay gates as v2 artifact descriptors, so their new
|
||||
// viewers (CtePlanViewer, CteResultViewer, PhaseSummaryViewer) render in
|
||||
// replay. Pattern of augment-schema-linking.mjs: load replay.json, find each
|
||||
// gate by regex on its title, replace artifact.data with the matching v2
|
||||
// fixture. Run after editing a fixture: node tools/replay/augment-review-gates.mjs
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const replayPath = join(HERE, "replay.json");
|
||||
const replay = JSON.parse(readFileSync(replayPath, "utf8"));
|
||||
|
||||
const ctePlan = JSON.parse(readFileSync(join(HERE, "cte-plan-fixture.json"), "utf8"));
|
||||
const cteResult = JSON.parse(readFileSync(join(HERE, "cte-result-fixture.json"), "utf8"));
|
||||
const phaseSummary = JSON.parse(readFileSync(join(HERE, "phase-summary-fixture.json"), "utf8"));
|
||||
|
||||
function findGate(re, widget) {
|
||||
return replay.gates.findIndex(
|
||||
(g) => re.test(g.descriptor?.title ?? "") && (!widget || g.descriptor?.widget === widget)
|
||||
);
|
||||
}
|
||||
|
||||
function replaceArtifactData(idx, kind, data) {
|
||||
const d = replay.gates[idx].descriptor;
|
||||
d.artifact = { kind, data };
|
||||
}
|
||||
|
||||
const targets = [
|
||||
{ name: "F6 cte_plan", re: /Piano CTE \(persisti cte_plan\.json\)/, widget: "artifact-gate", kind: "cte_plan", data: ctePlan },
|
||||
{ name: "F6 CTE 1 approvato (cte_result)", re: /CTE 1 approvato/, widget: "artifact-gate", kind: "cte_result", data: cteResult },
|
||||
{ name: "Fase 5 completata (phase)", re: /Fase 5 completata/, widget: "artifact-gate", kind: "phase", data: phaseSummary },
|
||||
];
|
||||
|
||||
for (const t of targets) {
|
||||
const idx = findGate(t.re, t.widget);
|
||||
if (idx < 0) {
|
||||
console.error(`✗ ${t.name} gate not found in replay.json (title /${t.re.source}/)`);
|
||||
process.exit(1);
|
||||
}
|
||||
replaceArtifactData(idx, t.kind, t.data);
|
||||
console.log(`✓ Rewrote gate ${idx} ("${replay.gates[idx].descriptor.title}") -> kind:"${t.kind}"`);
|
||||
}
|
||||
|
||||
writeFileSync(replayPath, JSON.stringify(replay, null, 2) + "\n");
|
||||
console.log(`Wrote ${replayPath}`);
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"question": "Lista dei pazienti che, negli ultimi 15 anni, hanno avuto un'ablazione transcatetere seguita da un impianto di defibrillatore (ICD) entro 10 anni.",
|
||||
"strategy": "Catena di 4 CTE: prima isolo le ablazioni transcatetere e gli impianti ICD separatamente (con le rispettive finestre temporali), poi li unisco per paziente mantenendo solo le coppie in cui l'impianto segue l'ablazione entro 10 anni, infine riduco a una riga per paziente scegliendo la prima coppia qualificante.",
|
||||
"ctes": [
|
||||
{
|
||||
"name": "ablazioni_recenti",
|
||||
"index": 1,
|
||||
"purpose": "Ablazioni transcatetere (ablazione_transcatetere IS TRUE) con data ∈ [2011-07-03, 2026-07-03].",
|
||||
"rationale": "Punto di partenza della catena: isola l'evento indice (ablazione) prima di cercare l'impianto conseguente.",
|
||||
"depends_on": [],
|
||||
"tables": [
|
||||
{
|
||||
"name": "fact_studio_elettrofisiologico_endocavitario_ablazione",
|
||||
"description": "Fatto clinico derivato da cr_seeablazione relativo a eventi, procedure o rilevazioni del paziente. Esclude record riclassificati come cardioversione o chiusura auricola."
|
||||
}
|
||||
],
|
||||
"keys": ["cod_paz", "num"],
|
||||
"filters": [
|
||||
{
|
||||
"column": "fact_studio_elettrofisiologico_endocavitario_ablazione.ablazione_transcatetere",
|
||||
"op": "IS",
|
||||
"value": "TRUE",
|
||||
"description": "Indicazione di ablazione transcatetere derivata dal text analysis sul campo conclusioni (sovrascrive il flag Cardioref).",
|
||||
"rationale": "Filtra solo le procedure di ablazione transcatetere richieste dalla domanda, escludendo altre tipologie di intervento."
|
||||
},
|
||||
{
|
||||
"column": "fact_studio_elettrofisiologico_endocavitario_ablazione.data_ablazione",
|
||||
"op": "BETWEEN",
|
||||
"value": "2011-07-03 AND 2026-07-03",
|
||||
"description": "Data di esecuzione della procedura di ablazione.",
|
||||
"rationale": "Finestra degli «ultimi 15 anni» calcolata rispetto alla data corrente della domanda."
|
||||
}
|
||||
],
|
||||
"output_columns": ["cod_paz", "num", "data_ablazione"]
|
||||
},
|
||||
{
|
||||
"name": "impianti_icd",
|
||||
"index": 2,
|
||||
"purpose": "Impianti ICD iniziali con data.",
|
||||
"rationale": "Secondo evento della catena: individua gli impianti candidati che potrebbero seguire un'ablazione già isolata in ablazioni_recenti.",
|
||||
"depends_on": [],
|
||||
"tables": [
|
||||
{
|
||||
"name": "fact_impianto_defibrillatore_impiantabile",
|
||||
"description": "Fact table degli impianti di defibrillatori (ICD). Esclude record riclassificati come CRT-P/CRT-D/S-ICD dal classificatore CIED."
|
||||
}
|
||||
],
|
||||
"keys": ["cod_paz", "num"],
|
||||
"filters": [],
|
||||
"output_columns": ["cod_paz", "num", "data_impianto"]
|
||||
},
|
||||
{
|
||||
"name": "sequenze_abl_imp",
|
||||
"index": 3,
|
||||
"purpose": "Join su cod_paz tra ablazioni_recenti e impianti_icd, mantiene le coppie con data_impianto > data_ablazione E <= data_ablazione + 10 anni.",
|
||||
"rationale": "Applica il vincolo temporale «entro 10 anni» richiesto dalla domanda, collegando ablazione e impianto per lo stesso paziente.",
|
||||
"depends_on": ["ablazioni_recenti", "impianti_icd"],
|
||||
"tables": [
|
||||
{
|
||||
"name": "dim_patient",
|
||||
"description": "Dimensione paziente derivata dall'anagrafica Cardioref normalizzata nello schema di integrazione. Popolata solo con pazienti che hanno almeno un record in una tabella flaggata is_activity_table:true in integration-map.yaml (gate di attivita clinica del reparto di aritmologia)."
|
||||
}
|
||||
],
|
||||
"keys": ["cod_paz"],
|
||||
"filters": [
|
||||
{
|
||||
"column": "sequenze_abl_imp.data_impianto",
|
||||
"op": ">",
|
||||
"value": "data_ablazione",
|
||||
"description": "",
|
||||
"rationale": "L'impianto deve seguire cronologicamente l'ablazione (non precederla né coincidere)."
|
||||
},
|
||||
{
|
||||
"column": "sequenze_abl_imp.data_impianto",
|
||||
"op": "<=",
|
||||
"value": "data_ablazione + INTERVAL '10 years'",
|
||||
"description": "",
|
||||
"rationale": "Vincolo «entro 10 anni» dalla domanda originale."
|
||||
}
|
||||
],
|
||||
"output_columns": ["cod_paz", "num_ablazione", "data_ablazione", "num_impianto", "data_impianto"]
|
||||
},
|
||||
{
|
||||
"name": "pazienti_qualificati",
|
||||
"index": 4,
|
||||
"purpose": "Una riga per paziente (prima coppia qualificante: ablazione più recente) + gap in anni.",
|
||||
"rationale": "Riduce sequenze_abl_imp a una riga per paziente come richiesto dalla «lista dei pazienti», scegliendo la coppia più recente in caso di ablazioni multiple.",
|
||||
"depends_on": ["sequenze_abl_imp"],
|
||||
"tables": [],
|
||||
"keys": ["cod_paz"],
|
||||
"filters": [],
|
||||
"output_columns": ["cod_paz", "data_ablazione", "data_impianto", "gap_anni"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "ablazioni_recenti",
|
||||
"index": 1,
|
||||
"total": 4,
|
||||
"purpose": "Ablazioni transcatetere (ablazione_transcatetere IS TRUE) con data ∈ [2011-07-03, 2026-07-03].",
|
||||
"rationale": "Punto di partenza della catena: isola l'evento indice (ablazione) prima di cercare l'impianto conseguente.",
|
||||
"depends_on": [],
|
||||
"sql": "WITH ablazioni_recenti AS (\n SELECT\n cod_paz,\n num,\n data_ablazione\n FROM fact_studio_elettrofisiologico_endocavitario_ablazione\n WHERE ablazione_transcatetere IS TRUE\n AND data_ablazione BETWEEN DATE '2011-07-03' AND DATE '2026-07-03'\n)\nSELECT * FROM ablazioni_recenti\nLIMIT 10;",
|
||||
"status": "ok",
|
||||
"execution_ms": 184,
|
||||
"row_sample": 10,
|
||||
"warnings": ["righe = limit: risultato troncato dalla preview"],
|
||||
"sql_hash": "a3f6c9d1e8b2047f",
|
||||
"columns": [
|
||||
{ "name": "cod_paz", "description": "Codice paziente (FK a dim_patient)." },
|
||||
{ "name": "num", "description": "Numero progressivo dell'episodio clinico per il paziente." },
|
||||
{ "name": "data_ablazione", "description": "Data di esecuzione della procedura di ablazione." }
|
||||
],
|
||||
"preview": {
|
||||
"columns": ["cod_paz", "num", "data_ablazione"],
|
||||
"rows": [
|
||||
[3758, 5, "2018-09-24"],
|
||||
[8175, 473, "2025-05-19"],
|
||||
[152185, 3, "2026-05-28"],
|
||||
[20446, 61, "2018-05-14"],
|
||||
[25492, 3, "2013-11-28"],
|
||||
[4664, 94, "2016-05-31"],
|
||||
[39149, 2, "2018-04-05"],
|
||||
[7243, 38, "2012-12-12"],
|
||||
[22310, 9, "2013-07-15"],
|
||||
[4848, 408, "2011-10-05"]
|
||||
]
|
||||
},
|
||||
"note": "Tutte le data_ablazione ricadono nel range [2011-07-03, 2026-07-03]; preview troncata a 10 righe. Mantiene le chiavi cod_paz e num."
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"phase": { "id": "F5", "num": 5, "name": "sintesi" },
|
||||
"summary": "Sintesi completa per il Blocco 3: ablazione transcatetere e impianto ICD sono ancorati rispettivamente a `fact_studio_elettrofisiologico_endocavitario_ablazione` e `fact_impianto_defibrillatore_impiantabile`, incrociati su `cod_paz` (entrambe FK a `dim_patient`). Escluse sostituzioni e controlli, fuori perimetro della domanda.",
|
||||
"checks": [
|
||||
{ "label": "Copertura tabelle promosse", "status": "ok", "detail": "Tutte le tabelle usate sono nel perimetro promosso in F4." },
|
||||
{ "label": "Finestre temporali coerenti", "status": "ok", "detail": "Ablazione in [2011-07-03, 2026-07-03]; impianto dopo l'ablazione e <= ablazione + 10 anni." },
|
||||
{ "label": "Chiavi di join verificate", "status": "ok", "detail": "cod_paz presente e non nullo in entrambe le fact table candidate." },
|
||||
{ "label": "Ambiguità residue", "status": "warn", "detail": "Pazienti con più ablazioni: da chiarire in F6 se contare la prima o l'ultima coppia qualificante." }
|
||||
],
|
||||
"sections": [
|
||||
{
|
||||
"title": "Criteri di selezione",
|
||||
"items": [
|
||||
{
|
||||
"label": "Ablazione transcatetere",
|
||||
"table": "fact_studio_elettrofisiologico_endocavitario_ablazione",
|
||||
"column": "ablazione_transcatetere",
|
||||
"value": "IS TRUE",
|
||||
"kind": "filter",
|
||||
"description": "Indicazione di ablazione transcatetere derivata dal text analysis sul campo conclusioni (sovrascrive il flag Cardioref).",
|
||||
"rationale": "Isola le sole procedure transcatetere richieste dalla domanda."
|
||||
},
|
||||
{
|
||||
"label": "Finestra ablazione",
|
||||
"table": "fact_studio_elettrofisiologico_endocavitario_ablazione",
|
||||
"column": "data_ablazione",
|
||||
"value": "[2011-07-03, 2026-07-03]",
|
||||
"kind": "filter",
|
||||
"description": "Data di esecuzione della procedura di ablazione.",
|
||||
"rationale": "Finestra degli «ultimi 15 anni» calcolata rispetto alla data corrente della domanda."
|
||||
},
|
||||
{
|
||||
"label": "Impianto successivo all'ablazione",
|
||||
"table": "fact_impianto_defibrillatore_impiantabile",
|
||||
"column": "data_impianto",
|
||||
"value": "> data_ablazione E <= data_ablazione + 10 anni",
|
||||
"kind": "filter",
|
||||
"description": "Data di esecuzione dell'impianto del defibrillatore.",
|
||||
"rationale": "Vincolo «entro 10 anni» richiesto dalla domanda."
|
||||
},
|
||||
{
|
||||
"label": "Join paziente",
|
||||
"table": "dim_patient",
|
||||
"column": "cod_paz",
|
||||
"value": "cross-fact join",
|
||||
"kind": "join",
|
||||
"description": "Codice identificativo del paziente nel sistema sorgente Cardioref.",
|
||||
"rationale": "Chiave comune tra le due fact table (entrambe FK a dim_patient)."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tables": [
|
||||
{
|
||||
"name": "fact_studio_elettrofisiologico_endocavitario_ablazione",
|
||||
"role": "promoted",
|
||||
"description": "Fatto clinico derivato da cr_seeablazione relativo a eventi, procedure o rilevazioni del paziente. Esclude record riclassificati come cardioversione o chiusura auricola.",
|
||||
"columns": [
|
||||
{ "name": "cod_paz", "description": "Codice paziente (FK a dim_patient)." },
|
||||
{ "name": "num", "description": "Numero progressivo dell'episodio clinico per il paziente." },
|
||||
{ "name": "ablazione_transcatetere", "description": "Indicazione di ablazione transcatetere derivata dal text analysis sul campo conclusioni.", "value_filter": "IS TRUE" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "fact_impianto_defibrillatore_impiantabile",
|
||||
"role": "promoted",
|
||||
"description": "Fact table degli impianti di defibrillatori (ICD). Esclude record riclassificati come CRT-P/CRT-D/S-ICD dal classificatore CIED.",
|
||||
"columns": [
|
||||
{ "name": "cod_paz", "description": "Codice paziente (FK a dim_patient)." },
|
||||
{ "name": "num", "description": "Numero progressivo dell'episodio clinico per il paziente." }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dim_patient",
|
||||
"role": "promoted",
|
||||
"description": "Dimensione paziente derivata dall'anagrafica Cardioref normalizzata nello schema di integrazione. Popolata solo con pazienti che hanno almeno un record in una tabella flaggata is_activity_table:true in integration-map.yaml (gate di attivita clinica del reparto di aritmologia).",
|
||||
"columns": [
|
||||
{ "name": "cod_paz", "description": "Codice identificativo del paziente nel sistema sorgente Cardioref." }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "fact_sostituzione_impianto_defibrillatore_impiantabile",
|
||||
"role": "excluded",
|
||||
"description": "Fact table delle sostituzioni/revisioni ICD. Esclude record riclassificati come CRT-P/CRT-D/S-ICD dal classificatore CIED.",
|
||||
"columns": []
|
||||
},
|
||||
{
|
||||
"name": "fact_controllo_defibrillatore_impiantabile",
|
||||
"role": "excluded",
|
||||
"description": "Fact table dei controlli periodici dei dispositivi ICD.",
|
||||
"columns": []
|
||||
}
|
||||
],
|
||||
"open_questions": [
|
||||
"Se un paziente ha più ablazioni transcatetere seguite da impianto, contare la prima o l'ultima coppia qualificante?"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user