refactor(harness): renaming prodotto tht (Onda -1)

Thoth (tht) è il prodotto, PSD è il cliente. Nessun riferimento al contesto
clinico nel codice.

Rinomine:
- comando+package nsp→tht (dir nsp/→tht/, 46 import, pyproject entry point)
- gate nsp-gate.js→tht-gate.js (+ rewrite token, relayIfNspFails→relayIfThtFails)
- workspace chirone.{example,test}.yaml→tht.{example,test}.yaml (generici)
- env THOTH_→THT_ (19 var) + NSP_ stragglers (NSP_HARNESS_ROOT, NSP_SESSION)
- commenti/docstring chirone/psdwp3/policlinico neutralizzati ('the reference
  implementation', 'the DWH')

Aggiunto [tool.setuptools.packages.find] include=['tht*'] (necessario: l'auto-
discovery rompeva con tht/ + workspaces/ come top-level multipli).

.env operatore aggiornato in-place (prefissi THT_, valori preservati, gitignored).

Verifica: pytest 109 passed, npm test 14 pass, tht phase meta --json OK, zero
residui nsp/THOTH_/NSP_/chirone nel package.
This commit is contained in:
2026-06-27 10:33:16 +02:00
parent 0dcc0246dc
commit fc5fbe6b65
72 changed files with 309 additions and 306 deletions
+19 -19
View File
@@ -1,38 +1,38 @@
# ThothII profile: server (full rebuild) | workstation (read REST + optional upsert) # ThothII profile: server (full rebuild) | workstation (read REST + optional upsert)
THOTH_PROFILE=server THT_PROFILE=server
# Workspace DB (relational, direct transport) — example: chirone # Workspace DB (relational, direct transport) — example: chirone
THOTH_DB_HOST= THT_DB_HOST=
THOTH_DB_PORT=5432 THT_DB_PORT=5432
THOTH_DB_NAME= THT_DB_NAME=
THOTH_DB_USER= THT_DB_USER=
THOTH_DB_PASSWORD= THT_DB_PASSWORD=
# DWH via REST (richiesto se database.transport = rest) # DWH via REST (richiesto se database.transport = rest)
THOTH_DWH_REST_URL=https://supabase-aritmolab.policlinicosandonato.it/dwh/ THT_DWH_REST_URL=https://supabase-aritmolab.policlinicosandonato.it/dwh/
THOTH_DWH_API_KEY= THT_DWH_API_KEY=
# Evidence source root (cartella curata a mano con le evidence) # Evidence source root (cartella curata a mano con le evidence)
THOTH_DOCS_ROOT= THT_DOCS_ROOT=
# Vector REST — LETTURA (rpc search_similar) sul Supabase remoto # Vector REST — LETTURA (rpc search_similar) sul Supabase remoto
THOTH_VEC_REST_URL=https://host/vector/v1/ THT_VEC_REST_URL=https://host/vector/v1/
THOTH_VEC_API_KEY= THT_VEC_API_KEY=
# Vector REST — SCRITTURA controllata (upsert only) da client remoti autorizzati # Vector REST — SCRITTURA controllata (upsert only) da client remoti autorizzati
# Path DEDICATO: /vector/write/v1/ (NON /vector/v1/). Le due chiavi valgono su path # Path DEDICATO: /vector/write/v1/ (NON /vector/v1/). Le due chiavi valgono su path
# separati: la writer key su /vector/v1/ -> 401, e viceversa. Solo postazioni che indicizzano. # separati: la writer key su /vector/v1/ -> 401, e viceversa. Solo postazioni che indicizzano.
THOTH_VEC_WRITE_REST_URL=https://host/vector/write/v1/ THT_VEC_WRITE_REST_URL=https://host/vector/write/v1/
THOTH_VEC_WRITE_API_KEY= THT_VEC_WRITE_API_KEY=
# Vector direct loading (server-only) # Vector direct loading (server-only)
THOTH_VEC_HOST=localhost THT_VEC_HOST=localhost
THOTH_VEC_PORT=5438 THT_VEC_PORT=5438
THOTH_VEC_USER=postgres THT_VEC_USER=postgres
THOTH_VEC_PASSWORD= THT_VEC_PASSWORD=
# Embeddings (Ollama) # Embeddings (Ollama)
THOTH_OLLAMA_URL=http://localhost:11434 THT_OLLAMA_URL=http://localhost:11434
# TLS (optional, internal CA) # TLS (optional, internal CA)
THOTH_SSL_CA= THT_SSL_CA=
+1 -1
View File
@@ -3,7 +3,7 @@
// Each function turns plain params into a ui_request descriptor object. No Pi // Each function turns plain params into a ui_request descriptor object. No Pi
// context, no I/O -- this is the part of the gate that is fully testable in L1 // context, no I/O -- this is the part of the gate that is fully testable in L1
// (in JS, in-language, no Python mirror). The glue (emission via ctx.sendRaw, // (in JS, in-language, no Python mirror). The glue (emission via ctx.sendRaw,
// anti-bypass, the no-limbo loop) is in nsp-gate.js and is verified at L2. // anti-bypass, the no-limbo loop) is in tht-gate.js and is verified at L2.
// //
// The 6 widget kinds: info, select, multiselect, freetext, artifact-gate, artifact. // The 6 widget kinds: info, select, multiselect, freetext, artifact-gate, artifact.
// `widget` is an open field (a new kind needs a renderer, not infra changes); // `widget` is an open field (a new kind needs a renderer, not infra changes);
+1 -1
View File
@@ -1,5 +1,5 @@
// Etichette riservate del gate HITL e helper di deduplica. Modulo PURO (nessuna // Etichette riservate del gate HITL e helper di deduplica. Modulo PURO (nessuna
// dipendenza da pi-tui): condiviso da nsp-gate.js e testabile in isolamento con // dipendenza da pi-tui): condiviso da tht-gate.js e testabile in isolamento con
// node --test .pi/extensions/test_reserved_labels.mjs // node --test .pi/extensions/test_reserved_labels.mjs
// `Altro — specifica…` e le due vie di fuga (torna indietro / esci) vengono // `Altro — specifica…` e le due vie di fuga (torna indietro / esci) vengono
// SEMPRE aggiunte dal gate alle opzioni in arrivo: se il modello ri-propone uno // SEMPRE aggiunte dal gate alle opzioni in arrivo: se il modello ri-propone uno
@@ -1,9 +1,9 @@
// nsp-gate.js -- Pi extension: the HITL gate for the ThothII NL->SQL workflow. // tht-gate.js -- Pi extension: the HITL gate for the ThothII NL->SQL workflow.
// //
// REWRITE of ChironeWp3's nsp-gate.js. Two changes vs the source: // REWRITE of the reference implementation's tht-gate.js. Two changes vs the source:
// (1) F2 single source: workflow facts (max_phase, phase names, the schema-linking // (1) F2 single source: workflow facts (max_phase, phase names, the schema-linking
// phase) come from `nsp phase meta --json`, NOT from JS-mirrored constants. // phase) come from `tht phase meta --json`, NOT from JS-mirrored constants.
// The ChironeWp3 PHASE_NAMES array (truncated to 7) is gone; F8/datamart can no // the reference implementation PHASE_NAMES array (truncated to 7) is gone; F8/datamart can no
// longer drift out of sync. // longer drift out of sync.
// (2) D2/D4 widget-descriptor: the reviewer interaction is emitted as a // (2) D2/D4 widget-descriptor: the reviewer interaction is emitted as a
// widget-descriptor JSON (built by ./gate/builders.js) and awaited by id, instead // widget-descriptor JSON (built by ./gate/builders.js) and awaited by id, instead
@@ -16,7 +16,7 @@
// - the agent_end prose safety net // - the agent_end prose safety net
// - the exit-code contracts with the CLI (5 = gate refusal, 6 = needs human, // - the exit-code contracts with the CLI (5 = gate refusal, 6 = needs human,
// 7 = not-ready silent no-op) // 7 = not-ready silent no-op)
// - textResult / nsp() / relayIfNspFails / advanceIfReady helpers // - textResult / tht() / relayIfThtFails / advanceIfReady helpers
// //
// TESTING: the pure builders are L1-tested (./gate/__tests__/). This file is the // TESTING: the pure builders are L1-tested (./gate/__tests__/). This file is the
// GLUE -- it depends on the Pi runtime (pi.on, pi.registerTool, ctx.sendRaw) and is // GLUE -- it depends on the Pi runtime (pi.on, pi.registerTool, ctx.sendRaw) and is
@@ -37,44 +37,44 @@ import { ALTRO, BACK_LABEL, QUIT_LABEL, CONTROL_LABELS, isReserved, stripReserve
// --- anti-bypass block lists (spec D4, verbatim from source L169-177) ----------- // --- anti-bypass block lists (spec D4, verbatim from source L169-177) -----------
const FORBIDDEN = [ const FORBIDDEN = [
/\bnsp\s+phase\s+(advance|reopen)\b/, /\btht\s+phase\s+(advance|reopen)\b/,
/\bnsp\s+decision\s+add\b/, /\btht\s+decision\s+add\b/,
/\bnsp\s+cte\s+plan\b/, /\btht\s+cte\s+plan\b/,
]; ];
const PROTECTED_FILES = /(review_decisions\.jsonl|session_manifest\.yaml|cte_plan\.json)/; const PROTECTED_FILES = /(review_decisions\.jsonl|session_manifest\.yaml|cte_plan\.json)/;
// --- kickoff payloads (verbatim from source L184-212, load-bearing model prose) - // --- kickoff payloads (verbatim from source L184-212, load-bearing model prose) -
const NUOVA_DOMANDA_KICKOFF = const NUOVA_DOMANDA_KICKOFF =
"Istruzioni operative — nuova sessione ThothII (workflow human-in-the-middle: tu " + "Istruzioni operative — nuova sessione ThothII (workflow human-in-the-middle: tu " +
"orchestri, il reviewer decide, la CLI `nsp` persiste; NON sei in modalita' autonoma).\n" + "orchestri, il reviewer decide, la CLI `tht` persiste; NON sei in modalita' autonoma).\n" +
'1. Esegui `nsp session new "<la domanda dell\'utente nel messaggio sopra>"` e annota ' + '1. Esegui `tht session new "<la domanda dell\'utente nel messaggio sopra>"` e annota ' +
"l'id stampato nell'ultima riga.\n" + "l'id stampato nell'ultima riga.\n" +
"2. Carica la skill leggendo il file con il tool `read`: `.pi/skills/nsp-sessione/SKILL.md` " + "2. Carica la skill leggendo il file con il tool `read`: `.pi/skills/tht-sessione/SKILL.md` " +
"(NON come comando di shell ne' come `/skill:...`). Poi segui il suo workflow dalla Fase 1 " + "(NON come comando di shell ne' come `/skill:...`). Poi segui il suo workflow dalla Fase 1 " +
"(Chiarimento), usando l'id di sessione in ogni comando `nsp`.\n" + "(Chiarimento), usando l'id di sessione in ogni comando `tht`.\n" +
"Se la skill non si carica (per qualsiasi motivo): FERMATI. Non proseguire da solo, non " + "Se la skill non si carica (per qualsiasi motivo): FERMATI. Non proseguire da solo, non " +
"improvvisare analisi o query. Comunica al reviewer che la skill nsp-sessione non e' " + "improvvisare analisi o query. Comunica al reviewer che la skill tht-sessione non e' " +
"disponibile e attendi istruzioni.\n" + "disponibile e attendi istruzioni.\n" +
"Regole non negoziabili (valgono SEMPRE, anche senza la skill):\n" + "Regole non negoziabili (valgono SEMPRE, anche senza la skill):\n" +
"- Una domanda al reviewer per volta; attendi la sua risposta prima di proseguire.\n" + "- Una domanda al reviewer per volta; attendi la sua risposta prima di proseguire.\n" +
"- MAI promuovere, escludere, correggere o applicare alcunche' senza conferma esplicita.\n" + "- MAI promuovere, escludere, correggere o applicare alcunche' senza conferma esplicita.\n" +
"- Le interazioni col reviewer passano dai tool reviewer_select/reviewer_decide/" + "- Le interazioni col reviewer passano dai tool reviewer_select/reviewer_decide/" +
"reviewer_confirm (widget-descriptor); il reviewer invia testo libero prefissando '!'. " + "reviewer_confirm (widget-descriptor); il reviewer invia testo libero prefissando '!'. " +
"NON eseguire mai `nsp phase advance|reopen` ne' `nsp decision add` da shell.\n" + "NON eseguire mai `tht phase advance|reopen` ne' `tht decision add` da shell.\n" +
"- Procedi una fase alla volta: a fine fase cedi il turno al reviewer, non incatenare le fasi."; "- Procedi una fase alla volta: a fine fase cedi il turno al reviewer, non incatenare le fasi.";
const RIPRENDI_KICKOFF = const RIPRENDI_KICKOFF =
"Istruzioni operative — ripresa di una sessione ThothII esistente (id nel messaggio sopra).\n" + "Istruzioni operative — ripresa di una sessione ThothII esistente (id nel messaggio sopra).\n" +
"1. Esegui `nsp session show <id>` e leggi: stato, domanda, decisioni registrate, presenza " + "1. Esegui `tht session show <id>` e leggi: stato, domanda, decisioni registrate, presenza " +
"di schema_linking.json.\n" + "di schema_linking.json.\n" +
"2. Carica la skill leggendo `.pi/skills/nsp-sessione/SKILL.md` con il tool `read` (non come " + "2. Carica la skill leggendo `.pi/skills/tht-sessione/SKILL.md` con il tool `read` (non come " +
"comando di shell ne' `/skill:...`).\n" + "comando di shell ne' `/skill:...`).\n" +
"3. Determina l'ultima fase completata dai fatti persistiti (le decisioni sono la verita': " + "3. Determina l'ultima fase completata dai fatti persistiti (le decisioni sono la verita': " +
"cio' che non e' registrato non e' avvenuto) e riprendi da li'.\n" + "cio' che non e' registrato non e' avvenuto) e riprendi da li'.\n" +
"Valgono le stesse regole non negoziabili: una domanda per volta, conferma esplicita, tool " + "Valgono le stesse regole non negoziabili: una domanda per volta, conferma esplicita, tool " +
"reviewer_*, niente phase advance/reopen o decision add da shell, una fase alla volta."; "reviewer_*, niente phase advance/reopen o decision add da shell, una fase alla volta.";
// Recovery hint shown when `nsp phase advance` refuses with exit 5 (gate not satisfied). // Recovery hint shown when `tht phase advance` refuses with exit 5 (gate not satisfied).
const PHASE_RECOVERY = "Completa i prerequisiti della fase (decisioni/artefatti) e riprova."; const PHASE_RECOVERY = "Completa i prerequisiti della fase (decisioni/artefatti) e riprova.";
// --- helpers (verbatim from source) ------------------------------------------- // --- helpers (verbatim from source) -------------------------------------------
@@ -84,15 +84,15 @@ function textResult(text) {
} }
// Single chokepoint for all CLI calls. cwd is the Pi project root (harness/). // Single chokepoint for all CLI calls. cwd is the Pi project root (harness/).
function nsp(ctx, args) { function tht(ctx, args) {
return execFileSync("nsp", args, { cwd: ctx.cwd, encoding: "utf8" }); return execFileSync("tht", args, { cwd: ctx.cwd, encoding: "utf8" });
} }
// Runs a privileged nsp call; converts any failure into an actionable textResult // Runs a privileged tht call; converts any failure into an actionable textResult
// (never propagates a raw "Command failed" to the model). // (never propagates a raw "Command failed" to the model).
function relayIfNspFails(ctx, args, recovery) { function relayIfThtFails(ctx, args, recovery) {
try { try {
nsp(ctx, args); tht(ctx, args);
return null; return null;
} catch (e) { } catch (e) {
const cliMsg = (e.stderr || e.message || String(e)).toString().trim(); const cliMsg = (e.stderr || e.message || String(e)).toString().trim();
@@ -100,13 +100,13 @@ function relayIfNspFails(ctx, args, recovery) {
} }
} }
// F2 single-source: workflow facts from `nsp phase meta --json`. Cached per session. // F2 single-source: workflow facts from `tht phase meta --json`. Cached per session.
// Replaces the source's mirrored PHASE_NAMES constant (which drifted to 7 entries) // Replaces the source's mirrored PHASE_NAMES constant (which drifted to 7 entries)
// and the regex-parse of `nsp phase show` text. // and the regex-parse of `tht phase show` text.
let _phaseMetaCache = null; let _phaseMetaCache = null;
function phaseMeta(ctx) { function phaseMeta(ctx) {
if (_phaseMetaCache) return _phaseMetaCache; if (_phaseMetaCache) return _phaseMetaCache;
const raw = nsp(ctx, ["phase", "meta", "--json"]); const raw = tht(ctx, ["phase", "meta", "--json"]);
const meta = JSON.parse(raw); const meta = JSON.parse(raw);
_phaseMetaCache = meta; _phaseMetaCache = meta;
return meta; return meta;
@@ -127,15 +127,15 @@ function schemaLinkingPhase(ctx) {
} }
function currentPhase(ctx, session) { function currentPhase(ctx, session) {
const out = nsp(ctx, ["phase", "show", "--session", session]); const out = tht(ctx, ["phase", "show", "--session", session]);
const m = out.match(/Fase corrente:\s*(\d+)/); const m = out.match(/Fase corrente:\s*(\d+)/);
return m ? parseInt(m[1], 10) : 1; return m ? parseInt(m[1], 10) : 1;
} }
// nsp phase advance --if-ready: exit 7 = not ready (silent no-op), others propagated. // tht phase advance --if-ready: exit 7 = not ready (silent no-op), others propagated.
function advanceIfReady(ctx, session) { function advanceIfReady(ctx, session) {
try { try {
nsp(ctx, ["phase", "advance", "--if-ready", "--session", session]); tht(ctx, ["phase", "advance", "--if-ready", "--session", session]);
return { advanced: true }; return { advanced: true };
} catch (e) { } catch (e) {
if (e.status === 7) return { advanced: false }; if (e.status === 7) return { advanced: false };
@@ -146,7 +146,7 @@ function advanceIfReady(ctx, session) {
// --- widget emission + wait (NEW: replaces ctx.ui.* blocking primitives) ------- // --- widget emission + wait (NEW: replaces ctx.ui.* blocking primitives) -------
// //
// emitAndWait(ctx, descriptor) sends a ui_request widget and awaits the correlated // emitAndWait(ctx, descriptor) sends a ui_request widget and awaits the correlated
// ui_response by id. This is the correlation-by-id layer ChironeWp3 never had (its // ui_response by id. This is the correlation-by-id layer the reference implementation never had (its
// native TUI primitives handled it implicitly). The descriptor is built by the pure // native TUI primitives handled it implicitly). The descriptor is built by the pure
// ./gate/builders.js (L1-tested). // ./gate/builders.js (L1-tested).
// //
@@ -199,8 +199,8 @@ export default function (pi) {
pi.on("tool_call", (event) => { pi.on("tool_call", (event) => {
if (event.toolName === "bash") { if (event.toolName === "bash") {
const cmd = event.input?.command ?? ""; const cmd = event.input?.command ?? "";
// nsp session finalize: legit session-close path -- unlock and let through. // tht session finalize: legit session-close path -- unlock and let through.
if (/\bnsp\s+session\s+finalize\b/.test(cmd)) { if (/\btht\s+session\s+finalize\b/.test(cmd)) {
lockActive = false; lockActive = false;
lastSteered = false; lastSteered = false;
return; return;
@@ -350,8 +350,8 @@ export default function (pi) {
label: "Decisione di merito (reviewer)", label: "Decisione di merito (reviewer)",
description: description:
"Pone una decisione di merito al reviewer via widget multiselect e PERSISTE le " + "Pone una decisione di merito al reviewer via widget multiselect e PERSISTE le " +
"scelte (nsp decision add). Ogni opzione porta un payload decision {type, subject, " + "scelte (tht decision add). Ogni opzione porta un payload decision {type, subject, " +
"detail, rationale}. Dopo la conferma, se p.advance e' vero tenta nsp phase advance --if-ready.", "detail, rationale}. Dopo la conferma, se p.advance e' vero tenta tht phase advance --if-ready.",
parameters: Type.Object({ parameters: Type.Object({
session: Type.String(), session: Type.String(),
title: Type.String(), title: Type.String(),
@@ -397,7 +397,7 @@ export default function (pi) {
"--subject", d.subject]; "--subject", d.subject];
if (d.detail) args.push("--detail", d.detail); if (d.detail) args.push("--detail", d.detail);
if (d.rationale) args.push("--rationale", d.rationale); if (d.rationale) args.push("--rationale", d.rationale);
const err = relayIfNspFails(ctx, args, ""); const err = relayIfThtFails(ctx, args, "");
if (err) return err; if (err) return err;
toAdd.push(d); toAdd.push(d);
} }
@@ -415,7 +415,7 @@ export default function (pi) {
label: "Gate di avanzamento (reviewer)", label: "Gate di avanzamento (reviewer)",
description: description:
"Checkpoint di fase/CTE/SQL: presenta un widget artifact-gate (artefatto + " + "Checkpoint di fase/CTE/SQL: presenta un widget artifact-gate (artefatto + " +
"Approva/Rifiuta/Altro) ed esegue l'azione privilegiata (nsp phase advance, cte plan, " + "Approva/Rifiuta/Altro) ed esegue l'azione privilegiata (tht phase advance, cte plan, " +
"decision add sql_approved) solo su approvazione. kind: phase | cte_plan | cte_result | sql.", "decision add sql_approved) solo su approvazione. kind: phase | cte_plan | cte_result | sql.",
parameters: Type.Object({ parameters: Type.Object({
session: Type.String(), session: Type.String(),
@@ -459,13 +459,13 @@ export default function (pi) {
return textResult(`Fase approvata (sessione ${session}).`); return textResult(`Fase approvata (sessione ${session}).`);
} }
if (kind === "cte_plan") { if (kind === "cte_plan") {
const err = relayIfNspFails(ctx, ["cte", "plan", "--session", session], ""); const err = relayIfThtFails(ctx, ["cte", "plan", "--session", session], "");
if (err) return err; if (err) return err;
return textResult(`CTE plan approvato (sessione ${session}).`); return textResult(`CTE plan approvato (sessione ${session}).`);
} }
if (kind === "cte_result" || kind === "sql") { if (kind === "cte_result" || kind === "sql") {
const dt = kind === "sql" ? "sql_approved" : "cte_approved"; const dt = kind === "sql" ? "sql_approved" : "cte_approved";
const err = relayIfNspFails( const err = relayIfThtFails(
ctx, ctx,
["decision", "add", "--session", session, "--type", dt, "--subject", `phase:${currentPhase(ctx, session)}`], ["decision", "add", "--session", session, "--type", dt, "--subject", `phase:${currentPhase(ctx, session)}`],
"", "",
@@ -481,7 +481,7 @@ export default function (pi) {
name: "rewrite_question", name: "rewrite_question",
label: "Riscrittura domanda (deterministica)", label: "Riscrittura domanda (deterministica)",
description: description:
"Scrive deterministicamente question.md via nsp session set-question (evita il tool " + "Scrive deterministicamente question.md via tht session set-question (evita il tool " +
"di edit unreliable). assumptions puo' essere array o stringa JSON.", "di edit unreliable). assumptions puo' essere array o stringa JSON.",
parameters: Type.Object({ parameters: Type.Object({
session: Type.String(), session: Type.String(),
@@ -503,7 +503,7 @@ export default function (pi) {
if (Array.isArray(assumps)) { if (Array.isArray(assumps)) {
for (const a of assumps) args.push("--assumption", String(a)); for (const a of assumps) args.push("--assumption", String(a));
} }
const err = relayIfNspFails(ctx, args, ""); const err = relayIfThtFails(ctx, args, "");
if (err) return err; if (err) return err;
return textResult(`Domanda riscritta per la sessione ${session}.`); return textResult(`Domanda riscritta per la sessione ${session}.`);
}, },
@@ -516,7 +516,7 @@ export default function (pi) {
const parts = args.trim().split(/\s+/).filter(Boolean); const parts = args.trim().split(/\s+/).filter(Boolean);
const sessionId = const sessionId =
parts.length >= 2 ? parts[0] : parts.length === 1 && /^\d/.test(parts[0]) ? undefined : parts[0]; parts.length >= 2 ? parts[0] : parts.length === 1 && /^\d/.test(parts[0]) ? undefined : parts[0];
const sid = sessionId ?? activeSessionId ?? process.env.NSP_SESSION; const sid = sessionId ?? activeSessionId ?? process.env.THT_SESSION;
if (!sid) { if (!sid) {
await ctx.ui.notify("Uso: /torna <session_id> [N]", "warning"); await ctx.ui.notify("Uso: /torna <session_id> [N]", "warning");
return; return;
@@ -528,7 +528,7 @@ export default function (pi) {
await ctx.ui.notify(`Target non valido (fase corrente ${cur}).`, "warning"); await ctx.ui.notify(`Target non valido (fase corrente ${cur}).`, "warning");
return; return;
} }
nsp(ctx, ["phase", "reopen", "--session", sid, "--phase", String(target)]); tht(ctx, ["phase", "reopen", "--session", sid, "--phase", String(target)]);
await pi.sendUserMessage( await pi.sendUserMessage(
`Ho riaperto la Fase ${target} della sessione ${sid}. Riprendi il protocollo da quella fase.`, `Ho riaperto la Fase ${target} della sessione ${sid}. Riprendi il protocollo da quella fase.`,
{ deliverAs: "followUp" }, { deliverAs: "followUp" },
+14 -14
View File
@@ -1,8 +1,8 @@
# ThothII harness — `nsp` # ThothII harness — `tht`
The self-contained Pi layer of ThothII: a deterministic Python CLI (`nsp`) + a Pi gate The self-contained Pi layer of ThothII: a deterministic Python CLI (`tht`) + a Pi gate
extension (`.pi/extensions/nsp-gate.js`) that runs the 8-phase NL→SQL workflow, extension (`.pi/extensions/tht-gate.js`) that runs the 8-phase NL→SQL workflow,
emitting and consuming widget-descriptor JSON. Derived from ChironeWp3 as a validated emitting and consuming widget-descriptor JSON. Derived from the reference implementation as a validated
starting point, **adapted** (not assumed reliable) to the ThothII contract. starting point, **adapted** (not assumed reliable) to the ThothII contract.
## Install ## Install
@@ -13,7 +13,7 @@ python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]" pip install -e ".[dev]"
``` ```
The `nsp` command is now on PATH. Node ≥ 20 is needed for the gate JS tests The `tht` command is now on PATH. Node ≥ 20 is needed for the gate JS tests
(`npm test`). (`npm test`).
## Configure ## Configure
@@ -22,8 +22,8 @@ The `nsp` command is now on PATH. Node ≥ 20 is needed for the gate JS tests
```bash ```bash
cp .env.example .env cp .env.example .env
# fill in: THOTH_PROFILE, THOTH_DB_*, THOTH_DWH_API_KEY, THOTH_VEC_API_KEY, # fill in: THT_PROFILE, THT_DB_*, THT_DWH_API_KEY, THT_VEC_API_KEY,
# THOTH_VEC_WRITE_API_KEY, THOTH_SSL_CA, THOTH_OLLAMA_URL, ... # THT_VEC_WRITE_API_KEY, THT_SSL_CA, THT_OLLAMA_URL, ...
``` ```
Keys are never logged; URLs are fine. Rotate any key that appeared in chat. Keys are never logged; URLs are fine. Rotate any key that appeared in chat.
@@ -31,7 +31,7 @@ Keys are never logged; URLs are fine. Rotate any key that appeared in chat.
### `workspaces/<name>.yaml` ### `workspaces/<name>.yaml`
A workspace wires the relational DWH + the pgvector (dual-key) + embeddings + evidence. A workspace wires the relational DWH + the pgvector (dual-key) + embeddings + evidence.
See `workspaces/chirone.example.yaml`. `${THOTH_*}` tokens expand from `.env`. See `workspaces/tht.example.yaml`. `${THT_*}}` tokens expand from `.env`.
## The workflow ## The workflow
@@ -40,8 +40,8 @@ See `workspaces/chirone.example.yaml`. `${THOTH_*}` tokens expand from `.env`.
and the skill all read from it — no mirrored constants to drift. and the skill all read from it — no mirrored constants to drift.
```bash ```bash
nsp phase meta --json # the gate reads workflow facts from here tht phase meta --json # the gate reads workflow facts from here
nsp phase meta # human-readable tht phase meta # human-readable
``` ```
## Run ## Run
@@ -53,8 +53,8 @@ pi --mode rpc
# in Pi: /nuova-domanda "<your question in natural language>" # in Pi: /nuova-domanda "<your question in natural language>"
``` ```
The gate (`nsp-gate.js`) presents reviewer widgets (widget-descriptor), the reviewer The gate (`tht-gate.js`) presents reviewer widgets (widget-descriptor), the reviewer
decides, and `nsp` persists decisions to the append-only ledger (`review_decisions.jsonl`). decides, and `tht` persists decisions to the append-only ledger (`review_decisions.jsonl`).
See `docs/testing.md` for what each interaction level validates. See `docs/testing.md` for what each interaction level validates.
## Test ## Test
@@ -72,9 +72,9 @@ level covers and — crucially — does **not** cover.
## Layout ## Layout
``` ```
nsp/ Python package (CLI + workflow + phase + decisions + db/rest/mschema/ tht/ Python package (CLI + workflow + phase + decisions + db/rest/mschema/
vectorstore/evidence/search/session + memory) vectorstore/evidence/search/session + memory)
.pi/ Pi project (settings, prompts, themes, extensions/nsp-gate.js + gate/) .pi/ Pi project (settings, prompts, themes, extensions/tht-gate.js + gate/)
workflow.yaml single source of workflow truth (F2) workflow.yaml single source of workflow truth (F2)
workspaces/ workspace YAML definitions (D3) workspaces/ workspace YAML definitions (D3)
scripts/ reader/writer RPC SQL for pgvector (D11) scripts/ reader/writer RPC SQL for pgvector (D11)
+4 -4
View File
@@ -1,8 +1,8 @@
# Editing the workflow # Editing the workflow
`harness/workflow.yaml` is the **single source of workflow truth** (spec F2, §5.3). `harness/workflow.yaml` is the **single source of workflow truth** (spec F2, §5.3).
`phase.py`, the gate (`nsp-gate.js`), and the skill all read from it. There are **no `phase.py`, the gate (`tht-gate.js`), and the skill all read from it. There are **no
mirrored constants** in JS or Python — that was the ChironeWp3 drift bug mirrored constants** in JS or Python — that was the the reference implementation drift bug
(`PHASE_NAMES` truncated to 7 entries in JS, F8/datamart silently dropped). Editing (`PHASE_NAMES` truncated to 7 entries in JS, F8/datamart silently dropped). Editing
this one file is the only place the workflow changes. this one file is the only place the workflow changes.
@@ -12,7 +12,7 @@ this one file is the only place the workflow changes.
schema_version: 1 schema_version: 1
phases: phases:
- id: F3 # stable id (referenced by the gate) - id: F3 # stable id (referenced by the gate)
name: riscrittura # human label (nsp phase show / gate UI) name: riscrittura # human label (tht phase show / gate UI)
advance: kind:phase # how the phase advances (see below) advance: kind:phase # how the phase advances (see below)
prerequisites: # what must hold before advancing (gate checks these) prerequisites: # what must hold before advancing (gate checks these)
- decision_exists: question_rewritten - decision_exists: question_rewritten
@@ -68,7 +68,7 @@ preserved. This is the D15 fix for the orphaned-CTE-blocks-finalize bug.
## After editing ## After editing
```bash ```bash
nsp phase meta --json # confirm the new shape (max_phase, phases, artifacts_out) tht phase meta --json # confirm the new shape (max_phase, phases, artifacts_out)
pytest # L1 coherence smoke re-derives from the new workflow.yaml pytest # L1 coherence smoke re-derives from the new workflow.yaml
``` ```
+5 -2
View File
@@ -1,5 +1,5 @@
[project] [project]
name = "nsp" name = "tht"
version = "0.1.0" version = "0.1.0"
description = "ThothII harness — deterministic CLI for the NL→SQL workflow" description = "ThothII harness — deterministic CLI for the NL→SQL workflow"
requires-python = ">=3.12" requires-python = ">=3.12"
@@ -18,7 +18,7 @@ dependencies = [
] ]
[project.scripts] [project.scripts]
nsp = "nsp.cli:app" tht = "tht.cli:app"
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
@@ -27,6 +27,9 @@ dev = [
"ruff>=0.5", "ruff>=0.5",
] ]
[tool.setuptools.packages.find]
include = ["tht*"]
[tool.ruff] [tool.ruff]
line-length = 100 line-length = 100
+5 -5
View File
@@ -7,14 +7,14 @@ from dotenv import load_dotenv
from sqlalchemy import create_engine from sqlalchemy import create_engine
from testcontainers.postgres import PostgresContainer from testcontainers.postgres import PostgresContainer
# Permetti `pytest` lanciato da qualsiasi directory di trovare il package `nsp` # Permetti `pytest` lanciato da qualsiasi directory di trovare il package `tht`
# (installato in modalità editable nella venv, ma utile anche senza attivazione). # (installato in modalità editable nella venv, ma utile anche senza attivazione).
_ROOT = Path(__file__).resolve().parent.parent _ROOT = Path(__file__).resolve().parent.parent
if str(_ROOT) not in sys.path: if str(_ROOT) not in sys.path:
sys.path.insert(0, str(_ROOT)) sys.path.insert(0, str(_ROOT))
# Directory sessions/ risolta relativamente alla root harness (per i test che creano sessioni) # Directory sessions/ risolta relativamente alla root harness (per i test che creano sessioni)
os.environ.setdefault("NSP_HARNESS_ROOT", str(_ROOT)) os.environ.setdefault("THT_HARNESS_ROOT", str(_ROOT))
# L2 tests need the remote credentials from harness/.env (gitignored). Loaded once, # L2 tests need the remote credentials from harness/.env (gitignored). Loaded once,
# autouse, before any test. L0/L1 tests don't read these vars; missing .env only # autouse, before any test. L0/L1 tests don't read these vars; missing .env only
@@ -23,10 +23,10 @@ load_dotenv(_ROOT / ".env")
# L2 connection prerequisites (spec Testing Strategy). If any is missing/empty, L2 # L2 connection prerequisites (spec Testing Strategy). If any is missing/empty, L2
# tests are SKIPPED (not failed) so the default run (pytest = L0+L1) stays green. # tests are SKIPPED (not failed) so the default run (pytest = L0+L1) stays green.
# NOTE: THOTH_SSL_CA is NOT required -- the DWH endpoint presents a public cert # NOTE: THT_SSL_CA is NOT required -- the DWH endpoint presents a public cert
# (*.policlinicosandonato.it signed by GoDaddy), already in the certifi bundle, so # signed by a public CA (GoDaddy), already in the certifi bundle, so
# the clients fall back to verify=True and TLS validates without a CA file. # the clients fall back to verify=True and TLS validates without a CA file.
REQUIRED_L2 = ["THOTH_DWH_API_KEY", "THOTH_VEC_API_KEY", "THOTH_VEC_WRITE_API_KEY"] REQUIRED_L2 = ["THT_DWH_API_KEY", "THT_VEC_API_KEY", "THT_VEC_WRITE_API_KEY"]
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
+1 -1
View File
@@ -7,7 +7,7 @@ is not assumed reliable' gains real teeth for the data layer.
import pytest import pytest
from sqlalchemy import create_engine, text from sqlalchemy import create_engine, text
from nsp.db.connection import can_create_in_schema, make_engine, ping, writable_tables from tht.db.connection import can_create_in_schema, make_engine, ping, writable_tables
pytestmark = [pytest.mark.l0] pytestmark = [pytest.mark.l0]
+1 -1
View File
@@ -4,7 +4,7 @@ FKs, and indexes from a real Postgres catalog.
""" """
import pytest import pytest
from nsp.db.introspect import IntrospectionError, introspect from tht.db.introspect import IntrospectionError, introspect
pytestmark = [pytest.mark.l0] pytestmark = [pytest.mark.l0]
+3 -3
View File
@@ -4,9 +4,9 @@ columns, and that wide_text / non-text columns are excluded.
""" """
import pytest import pytest
from nsp.config import LshConfig from tht.config import LshConfig
from nsp.db.introspect import introspect from tht.db.introspect import introspect
from nsp.db.sampling import is_text_type, unique_values_for_lsh from tht.db.sampling import is_text_type, unique_values_for_lsh
pytestmark = [pytest.mark.l0] pytestmark = [pytest.mark.l0]
@@ -1,4 +1,4 @@
"""L2: nsp memory save-one against real pgvector (spec D11, L2). """L2: tht memory save-one against real pgvector (spec D11, L2).
Validates D11 end-to-end: a single promoted decision is upserted to the real Validates D11 end-to-end: a single promoted decision is upserted to the real
pgvector via the WRITER key (not a full resync), and a subsequent search_similar pgvector via the WRITER key (not a full resync), and a subsequent search_similar
@@ -13,18 +13,18 @@ from unittest.mock import MagicMock
import pytest import pytest
from nsp.memory import MemoryRecord, save_one_memory from tht.memory import MemoryRecord, save_one_memory
from nsp.workspace import load_workspace from tht.workspace import load_workspace
pytestmark = [pytest.mark.l2] pytestmark = [pytest.mark.l2]
WORKSPACE = Path(__file__).resolve().parents[2] / "workspaces" / "chirone-test.yaml" WORKSPACE = Path(__file__).resolve().parents[2] / "workspaces" / "tht-test.yaml"
def test_save_one_upserts_to_real_pgvector(l2_env): def test_save_one_upserts_to_real_pgvector(l2_env):
"""save_one_memory pushes one row to the real pgvector via the writer key, and """save_one_memory pushes one row to the real pgvector via the writer key, and
a subsequent search_similar retrieves it. Idempotent (re-running upserts >= 0).""" a subsequent search_similar retrieves it. Idempotent (re-running upserts >= 0)."""
from nsp.vectorstore.embeddings import OllamaEmbeddings from tht.vectorstore.embeddings import OllamaEmbeddings
from nsp.vectorstore.rest_client import VectorRestClient from tht.vectorstore.rest_client import VectorRestClient
ws = load_workspace(WORKSPACE) ws = load_workspace(WORKSPACE)
if not ws.vector_write_rest or not ws.vector_write_rest.api_key.strip(): if not ws.vector_write_rest or not ws.vector_write_rest.api_key.strip():
+6 -6
View File
@@ -11,8 +11,8 @@ MODES (both manual, non-deterministic, pre-release -- NOT a regression gate):
Altro/value-grounding/rollback paths so specific behaviors assert deterministically. Altro/value-grounding/rollback paths so specific behaviors assert deterministically.
HOW TO RUN (operator, before release): HOW TO RUN (operator, before release):
1. Populate harness/.env (THOTH_DWH_API_KEY, THOTH_VEC_API_KEY, 1. Populate harness/.env (THT_DWH_API_KEY, THT_VEC_API_KEY,
THOTH_VEC_WRITE_API_KEY, THOTH_SSL_CA, THOTH_*_REST_URL, THOTH_OLLAMA_URL). THT_VEC_WRITE_API_KEY, THT_SSL_CA, THT_*_REST_URL, THT_OLLAMA_URL).
2. Connect VPN. Ensure Pi is configured locally with GLM 5.2. 2. Connect VPN. Ensure Pi is configured locally with GLM 5.2.
3. Run: pytest -m l2 tests/l2/test_session_ablazione.py -s 3. Run: pytest -m l2 tests/l2/test_session_ablazione.py -s
@@ -30,13 +30,13 @@ import pytest
pytestmark = [pytest.mark.l2] pytestmark = [pytest.mark.l2]
WORKSPACE = Path(__file__).resolve().parents[2] / "workspaces" / "chirone-test.yaml" WORKSPACE = Path(__file__).resolve().parents[2] / "workspaces" / "tht-test.yaml"
QUESTION = "dammi la lista dei pazienti che hanno fatto un'ablazione nel 2025" QUESTION = "dammi la lista dei pazienti che hanno fatto un'ablazione nel 2025"
def test_workspace_chirone_test_loads(l2_env): def test_workspace_chirone_test_loads(l2_env):
"""The L2 workspace YAML loads and expands ${THOTH_*} from .env.""" """The L2 workspace YAML loads and expands ${THT_*} from .env."""
from nsp.workspace import load_workspace from tht.workspace import load_workspace
ws = load_workspace(WORKSPACE) ws = load_workspace(WORKSPACE)
# secrets must be expanded (not the literal ${...} token) # secrets must be expanded (not the literal ${...} token)
@@ -68,7 +68,7 @@ def test_ablazione_session_manual(l2_env, tmp_path):
# assertion. We surface the run instructions and assert only that the launch # assertion. We surface the run instructions and assert only that the launch
# context is ready; the operator drives the conversation and inspects the outcome. # context is ready; the operator drives the conversation and inspects the outcome.
env_ok = all(os.environ.get(v, "").strip() for v in env_ok = all(os.environ.get(v, "").strip() for v in
["THOTH_DWH_API_KEY", "THOTH_VEC_API_KEY", "THOTH_VEC_WRITE_API_KEY"]) ["THT_DWH_API_KEY", "THT_VEC_API_KEY", "THT_VEC_WRITE_API_KEY"])
assert env_ok assert env_ok
assert WORKSPACE.exists() assert WORKSPACE.exists()
print(f"\n[L2 manual] launch: pi --mode rpc (cwd=harness/)") print(f"\n[L2 manual] launch: pi --mode rpc (cwd=harness/)")
+10 -10
View File
@@ -10,31 +10,31 @@ from pathlib import Path
import pytest import pytest
from nsp.workspace import load_workspace from tht.workspace import load_workspace
pytestmark = [pytest.mark.l2] pytestmark = [pytest.mark.l2]
WORKSPACE = Path(__file__).resolve().parents[2] / "workspaces" / "chirone-test.yaml" WORKSPACE = Path(__file__).resolve().parents[2] / "workspaces" / "tht-test.yaml"
def test_ablazione_returns_multiple_columns(l2_env): def test_ablazione_returns_multiple_columns(l2_env):
"""On the real schema, 'ablazione' should ground to more than one column (e.g. """On the real schema, 'ablazione' should ground to more than one column (e.g.
a flag and a free-text patologia field) -- the whole point of D14a's a flag and a free-text patologia field) -- the whole point of D14a's
non-collapsing aggregation. Requires a built LSH index (nsp lsh build).""" non-collapsing aggregation. Requires a built LSH index (tht lsh build)."""
from nsp.config import LshConfig from tht.config import LshConfig
try: try:
from nsp.lshindex import load_index, query_index # ported with the lsh build path from tht.lshindex import load_index, query_index # ported with the lsh build path
except ModuleNotFoundError: except ModuleNotFoundError:
pytest.skip("nsp.lshindex not yet ported (deferred from B3; lands with nsp lsh build)") pytest.skip("tht.lshindex not yet ported (deferred from B3; lands with tht lsh build)")
from nsp.search import aggregate_lsh_multi from tht.search import aggregate_lsh_multi
# NOTE: this test assumes the LSH index was built (nsp lsh build --workspace # NOTE: this test assumes the LSH index was built (tht lsh build --workspace
# chirone-test). If absent, build it first. The index path comes from the config. # tht-test). If absent, build it first. The index path comes from the config.
ws = load_workspace(WORKSPACE) ws = load_workspace(WORKSPACE)
index_dir = ws.paths.indexes index_dir = ws.paths.indexes
try: try:
lsh, minhashes, meta = load_index(index_dir, "datawarehouse") lsh, minhashes, meta = load_index(index_dir, "datawarehouse")
except Exception as e: except Exception as e:
pytest.skip(f"LSH index not built yet (run nsp lsh build): {e}") pytest.skip(f"LSH index not built yet (run tht lsh build): {e}")
hits = query_index(lsh, minhashes, "ablazione", meta, top_n=20) hits = query_index(lsh, minhashes, "ablazione", meta, top_n=20)
grouped = aggregate_lsh_multi( grouped = aggregate_lsh_multi(
+2 -2
View File
@@ -1,11 +1,11 @@
"""L1: nsp phase meta --json -- the gate reads workflow facts from here (F2). """L1: tht phase meta --json -- the gate reads workflow facts from here (F2).
Kills the JS/Python drift (no mirrored constants): workflow.yaml is the single source. Kills the JS/Python drift (no mirrored constants): workflow.yaml is the single source.
""" """
import json import json
from typer.testing import CliRunner from typer.testing import CliRunner
from nsp.cli import app from tht.cli import app
runner = CliRunner() runner = CliRunner()
+2 -2
View File
@@ -1,4 +1,4 @@
from nsp.decisions import append_decision, list_decisions from tht.decisions import append_decision, list_decisions
def test_decision_retracted_in_audit_but_marked(tmp_path): def test_decision_retracted_in_audit_but_marked(tmp_path):
@@ -57,7 +57,7 @@ def test_empty_session_returns_empty_list(tmp_path):
def test_decision_type_literal_includes_retracted(): def test_decision_type_literal_includes_retracted():
"""decision_retracted e' un tipo valido (pydantic lo accetta).""" """decision_retracted e' un tipo valido (pydantic lo accetta)."""
from nsp.decisions import DecisionRecord from tht.decisions import DecisionRecord
from datetime import datetime, UTC from datetime import datetime, UTC
d = DecisionRecord( d = DecisionRecord(
seq=1, ts=datetime.now(UTC), type="decision_retracted", seq=1, ts=datetime.now(UTC), type="decision_retracted",
+2 -2
View File
@@ -10,7 +10,7 @@ from pathlib import Path
import pytest import pytest
from nsp.evidence.formula_store import ConceptFormula, retrieve_formula, save_formula from tht.evidence.formula_store import ConceptFormula, retrieve_formula, save_formula
def test_formula_retrieval_by_concept(tmp_path): def test_formula_retrieval_by_concept(tmp_path):
@@ -72,7 +72,7 @@ def test_retrieve_empty_on_missing_dir(tmp_path):
def test_concept_formula_decision_types_exist(): def test_concept_formula_decision_types_exist():
import typing import typing
from nsp.decisions import DecisionType from tht.decisions import DecisionType
args = typing.get_args(DecisionType) args = typing.get_args(DecisionType)
assert "concept_formula_approved" in args assert "concept_formula_approved" in args
assert "concept_formula_rejected" in args assert "concept_formula_rejected" in args
@@ -9,7 +9,7 @@ decision rationale and survive persistence, never silently discarded.
""" """
from pathlib import Path from pathlib import Path
from nsp.decisions import append_decision, list_decisions from tht.decisions import append_decision, list_decisions
def test_altro_freetext_recorded_in_rationale(tmp_path): def test_altro_freetext_recorded_in_rationale(tmp_path):
+3 -3
View File
@@ -1,6 +1,6 @@
"""L1: nsp memory save-one -- targeted upsert via the writer key (spec D11). """L1: tht memory save-one -- targeted upsert via the writer key (spec D11).
The D11 deviation: instead of a full vectorstore resync (nsp memory index / sync), The D11 deviation: instead of a full vectorstore resync (tht memory index / sync),
a remote workstation with a writer key can push a SINGLE promoted decision to a remote workstation with a writer key can push a SINGLE promoted decision to
pgvector as a one-row upsert. This test pins the pure core of that behavior: pgvector as a one-row upsert. This test pins the pure core of that behavior:
- exactly one VectorRecord is built for the chosen decision_seq - exactly one VectorRecord is built for the chosen decision_seq
@@ -13,7 +13,7 @@ from unittest.mock import MagicMock
import pytest import pytest
from nsp.memory import MemoryRecord, memory_vector_record_for_decision, save_one_memory from tht.memory import MemoryRecord, memory_vector_record_for_decision, save_one_memory
def _record(seq: int = 7, **kw) -> MemoryRecord: def _record(seq: int = 7, **kw) -> MemoryRecord:
+3 -3
View File
@@ -6,9 +6,9 @@ override wins over the physical classification.
""" """
from datetime import datetime from datetime import datetime
from nsp.config import EligibilityConfig from tht.config import EligibilityConfig
from nsp.mschema.eligibility import classify_all, classify_column, effective_eligibility from tht.mschema.eligibility import classify_all, classify_column, effective_eligibility
from nsp.mschema.models import ( from tht.mschema.models import (
Annotations, Annotations,
ColumnAnnotation, ColumnAnnotation,
ColumnPhysical, ColumnPhysical,
+3 -3
View File
@@ -5,7 +5,7 @@ ThothAI style, schema-dict for AV-SQL). Pure logic, no I/O.
""" """
from datetime import datetime from datetime import datetime
from nsp.mschema.models import ( from tht.mschema.models import (
Annotations, Annotations,
ColumnAnnotation, ColumnAnnotation,
ColumnPhysical, ColumnPhysical,
@@ -13,7 +13,7 @@ from nsp.mschema.models import (
PhysicalSchema, PhysicalSchema,
TablePhysical, TablePhysical,
) )
from nsp.mschema.render import to_markdown, to_mschema_text, to_schema_dict from tht.mschema.render import to_markdown, to_mschema_text, to_schema_dict
def _fake_schema() -> PhysicalSchema: def _fake_schema() -> PhysicalSchema:
@@ -84,7 +84,7 @@ def test_to_schema_dict_avsql_shape():
def test_annotations_override_description_in_render(): def test_annotations_override_description_in_render():
schema = _fake_schema() schema = _fake_schema()
from nsp.mschema.models import TableAnnotation from tht.mschema.models import TableAnnotation
ann = Annotations(tables={ ann = Annotations(tables={
"dim_pazienti": TableAnnotation(columns={ "dim_pazienti": TableAnnotation(columns={
"citta": ColumnAnnotation(description="Comune di residenza"), "citta": ColumnAnnotation(description="Comune di residenza"),
+2 -2
View File
@@ -1,5 +1,5 @@
from nsp.decisions import append_decision, DecisionRecord from tht.decisions import append_decision, DecisionRecord
from nsp.phase import current_phase, effective_decisions from tht.phase import current_phase, effective_decisions
def _d(session, dtype, subject, **kw) -> DecisionRecord: def _d(session, dtype, subject, **kw) -> DecisionRecord:
+5 -5
View File
@@ -2,13 +2,13 @@
Verifies the ported RestClient: each rpc carries the X-API-Key header, the right Verifies the ported RestClient: each rpc carries the X-API-Key header, the right
payload, base_url slash handling, and HTTP/network errors surface as RestError. payload, base_url slash handling, and HTTP/network errors surface as RestError.
Ported (renamed psdwp3->nsp) from ChironeWp3/tests/test_rest_client.py. Ported (renamed psdwp3->tht) from ChironeWp3/tests/test_rest_client.py.
""" """
import pytest import pytest
import requests import requests
from nsp.config import RestConfig from tht.config import RestConfig
from nsp.rest.client import RestClient, RestError from tht.rest.client import RestClient, RestError
class FakeResponse: class FakeResponse:
@@ -39,7 +39,7 @@ def _capture(monkeypatch, response):
"timeout": timeout, "verify": verify}) "timeout": timeout, "verify": verify})
return response return response
monkeypatch.setattr("nsp.rest.client.requests.post", fake_post) monkeypatch.setattr("tht.rest.client.requests.post", fake_post)
return calls return calls
@@ -104,7 +104,7 @@ def test_network_error_actionable(monkeypatch):
def boom(*a, **k): def boom(*a, **k):
raise requests.ConnectionError("refused") raise requests.ConnectionError("refused")
monkeypatch.setattr("nsp.rest.client.requests.post", boom) monkeypatch.setattr("tht.rest.client.requests.post", boom)
with pytest.raises(RestError, match="raggiungibile"): with pytest.raises(RestError, match="raggiungibile"):
_client().ping() _client().ping()
@@ -8,11 +8,11 @@ effective view, teardown, taskdoc byte budget).
""" """
from pathlib import Path from pathlib import Path
from nsp.decisions import append_decision from tht.decisions import append_decision
from nsp.phase import current_phase, effective_decisions from tht.phase import current_phase, effective_decisions
from nsp.taskdoc import generate_task_doc from tht.taskdoc import generate_task_doc
from nsp.teardown import teardown_to_phase from tht.teardown import teardown_to_phase
from nsp.workflow import load_workflow from tht.workflow import load_workflow
def _approve(session: Path, phase: int) -> None: def _approve(session: Path, phase: int) -> None:
+3 -3
View File
@@ -1,6 +1,6 @@
from pathlib import Path from pathlib import Path
from nsp.taskdoc import generate_task_doc from tht.taskdoc import generate_task_doc
def test_task_doc_includes_question_and_schema_scope(tmp_path): def test_task_doc_includes_question_and_schema_scope(tmp_path):
@@ -52,8 +52,8 @@ def test_task_doc_carries_phase_header(tmp_path):
def test_task_doc_excludes_stale_decisions_post_rollback(tmp_path): def test_task_doc_excludes_stale_decisions_post_rollback(tmp_path):
"""D15+D16: il task doc riflette lo stato effective, non quello stale. """D15+D16: il task doc riflette lo stato effective, non quello stale.
Dopo rollback a F4, una sql_approved:7 stale non appare nel brief delle decisioni.""" Dopo rollback a F4, una sql_approved:7 stale non appare nel brief delle decisioni."""
from nsp.decisions import append_decision from tht.decisions import append_decision
from nsp.phase import current_phase from tht.phase import current_phase
s = tmp_path / "sess"; s.mkdir() s = tmp_path / "sess"; s.mkdir()
(s / "question.md").write_text("q") (s / "question.md").write_text("q")
# simula: lavoro fino a F7, poi rollback a F4 # simula: lavoro fino a F7, poi rollback a F4
+1 -1
View File
@@ -1,4 +1,4 @@
from nsp.teardown import teardown_to_phase from tht.teardown import teardown_to_phase
def test_teardown_to_phase_4_deletes_phase5plus_artifacts(tmp_path): def test_teardown_to_phase_4_deletes_phase5plus_artifacts(tmp_path):
+2 -2
View File
@@ -7,7 +7,7 @@ aggregate_lsh_multi exposes every column where the value appears, grouped by tab
so the value-grounding widget can let the reviewer choose which column(s) anchor so the value-grounding widget can let the reviewer choose which column(s) anchor
the value. the value.
""" """
from nsp.search import aggregate_lsh_multi from tht.search import aggregate_lsh_multi
def test_value_in_multiple_columns_returns_all(): def test_value_in_multiple_columns_returns_all():
@@ -64,7 +64,7 @@ def test_empty_hits_returns_empty():
def test_value_grounded_decision_type_exists(): def test_value_grounded_decision_type_exists():
# D14a adds the value_grounded decision type so the gate can record the # D14a adds the value_grounded decision type so the gate can record the
# reviewer's choice of which column(s) anchor a cited value. # reviewer's choice of which column(s) anchor a cited value.
from nsp.decisions import DecisionType from tht.decisions import DecisionType
import typing import typing
args = typing.get_args(DecisionType) args = typing.get_args(DecisionType)
assert "value_grounded" in args assert "value_grounded" in args
+3 -3
View File
@@ -5,9 +5,9 @@ API keys against the same pgvector REST endpoint, with distinct roles
(vector_reader / vector_writer). This test pins the dual-key construction and (vector_reader / vector_writer). This test pins the dual-key construction and
the workstation write-guard. the workstation write-guard.
""" """
from nsp.cli._guards import has_vector_write_rest, require_vector_write_allowed from tht.cli._guards import has_vector_write_rest, require_vector_write_allowed
from nsp.config import Config, DatabaseConfig, RestConfig from tht.config import Config, DatabaseConfig, RestConfig
from nsp.vectorstore.rest_client import VectorRestClient from tht.vectorstore.rest_client import VectorRestClient
def _minimal_config(**kw) -> Config: def _minimal_config(**kw) -> Config:
+1 -1
View File
@@ -1,4 +1,4 @@
from nsp.workflow import load_workflow from tht.workflow import load_workflow
def test_workflow_loads_8_phases(): def test_workflow_loads_8_phases():
+36 -36
View File
@@ -1,59 +1,59 @@
from pathlib import Path from pathlib import Path
from nsp.workspace import load_workspace, WorkspaceError from tht.workspace import load_workspace, WorkspaceError
def test_load_workspace_expands_env_vars(monkeypatch, tmp_path): def test_load_workspace_expands_env_vars(monkeypatch, tmp_path):
# Isolate profile: load_dotenv (conftest D3) injects THOTH_PROFILE from the real # Isolate profile: load_dotenv (conftest D3) injects THT_PROFILE from the real
# harness/.env into os.environ; this test asserts the *default* (server), so it # harness/.env into os.environ; this test asserts the *default* (server), so it
# must del THOTH_PROFILE rather than inherit whatever the operator set. # must del THT_PROFILE rather than inherit whatever the operator set.
monkeypatch.delenv("THOTH_PROFILE", raising=False) monkeypatch.delenv("THT_PROFILE", raising=False)
monkeypatch.setenv("THOTH_VEC_API_KEY", "secret-reader") monkeypatch.setenv("THT_VEC_API_KEY", "secret-reader")
monkeypatch.setenv("THOTH_VEC_WRITE_API_KEY", "secret-writer") monkeypatch.setenv("THT_VEC_WRITE_API_KEY", "secret-writer")
monkeypatch.setenv("THOTH_VEC_REST_URL", "https://example/vector/v1/") monkeypatch.setenv("THT_VEC_REST_URL", "https://example/vector/v1/")
monkeypatch.setenv("THOTH_DWH_REST_URL", "https://example/dwh/") monkeypatch.setenv("THT_DWH_REST_URL", "https://example/dwh/")
monkeypatch.setenv("THOTH_DWH_API_KEY", "dwh-key") monkeypatch.setenv("THT_DWH_API_KEY", "dwh-key")
monkeypatch.setenv("THOTH_DB_HOST", "h") monkeypatch.setenv("THT_DB_HOST", "h")
monkeypatch.setenv("THOTH_DB_NAME", "db") monkeypatch.setenv("THT_DB_NAME", "db")
monkeypatch.setenv("THOTH_DB_USER", "u") monkeypatch.setenv("THT_DB_USER", "u")
monkeypatch.setenv("THOTH_DB_PASSWORD", "p") monkeypatch.setenv("THT_DB_PASSWORD", "p")
monkeypatch.setenv("THOTH_VEC_HOST", "vh") monkeypatch.setenv("THT_VEC_HOST", "vh")
monkeypatch.setenv("THOTH_VEC_USER", "vu") monkeypatch.setenv("THT_VEC_USER", "vu")
monkeypatch.setenv("THOTH_VEC_PASSWORD", "vp") monkeypatch.setenv("THT_VEC_PASSWORD", "vp")
monkeypatch.setenv("THOTH_OLLAMA_URL", "http://ollama") monkeypatch.setenv("THT_OLLAMA_URL", "http://ollama")
monkeypatch.setenv("THOTH_DOCS_ROOT", str(tmp_path / "docs")) monkeypatch.setenv("THT_DOCS_ROOT", str(tmp_path / "docs"))
yaml = tmp_path / "w.yaml" yaml = tmp_path / "w.yaml"
yaml.write_text( yaml.write_text(
"database:\n" "database:\n"
" host: ${THOTH_DB_HOST}\n" " host: ${THT_DB_HOST}\n"
" port: 5432\n" " port: 5432\n"
" database: ${THOTH_DB_NAME}\n" " database: ${THT_DB_NAME}\n"
" schema: datawarehouse\n" " schema: datawarehouse\n"
" user: ${THOTH_DB_USER}\n" " user: ${THT_DB_USER}\n"
" password: ${THOTH_DB_PASSWORD}\n" " password: ${THT_DB_PASSWORD}\n"
" transport: rest\n" " transport: rest\n"
"rest:\n" "rest:\n"
" base_url: ${THOTH_DWH_REST_URL}\n" " base_url: ${THT_DWH_REST_URL}\n"
" api_key: ${THOTH_DWH_API_KEY}\n" " api_key: ${THT_DWH_API_KEY}\n"
"vector_db:\n" "vector_db:\n"
" host: ${THOTH_VEC_HOST}\n" " host: ${THT_VEC_HOST}\n"
" port: 5438\n" " port: 5438\n"
" database: postgres\n" " database: postgres\n"
" schema: vectors\n" " schema: vectors\n"
" user: ${THOTH_VEC_USER}\n" " user: ${THT_VEC_USER}\n"
" password: ${THOTH_VEC_PASSWORD}\n" " password: ${THT_VEC_PASSWORD}\n"
"vector_rest:\n" "vector_rest:\n"
" base_url: ${THOTH_VEC_REST_URL}\n" " base_url: ${THT_VEC_REST_URL}\n"
" api_key: ${THOTH_VEC_API_KEY}\n" " api_key: ${THT_VEC_API_KEY}\n"
"vector_write_rest:\n" "vector_write_rest:\n"
" base_url: ${THOTH_VEC_REST_URL}\n" " base_url: ${THT_VEC_REST_URL}\n"
" api_key: ${THOTH_VEC_WRITE_API_KEY}\n" " api_key: ${THT_VEC_WRITE_API_KEY}\n"
"embeddings:\n" "embeddings:\n"
" base_url: ${THOTH_OLLAMA_URL}\n" " base_url: ${THT_OLLAMA_URL}\n"
" model: nomic-embed-text-v2-moe\n" " model: nomic-embed-text-v2-moe\n"
" dim: 768\n" " dim: 768\n"
"evidence:\n" "evidence:\n"
" source_root: ${THOTH_DOCS_ROOT}\n" " source_root: ${THT_DOCS_ROOT}\n"
) )
ws = load_workspace(yaml) ws = load_workspace(yaml)
# Dual vector key (top-level, per Config reale): reader and writer separate # Dual vector key (top-level, per Config reale): reader and writer separate
@@ -66,7 +66,7 @@ def test_load_workspace_expands_env_vars(monkeypatch, tmp_path):
def test_load_workspace_missing_env_raises(monkeypatch, tmp_path): def test_load_workspace_missing_env_raises(monkeypatch, tmp_path):
monkeypatch.delenv("THOTH_VEC_API_KEY", raising=False) monkeypatch.delenv("THT_VEC_API_KEY", raising=False)
yaml = tmp_path / "w.yaml" yaml = tmp_path / "w.yaml"
yaml.write_text( yaml.write_text(
"database:\n" "database:\n"
@@ -74,10 +74,10 @@ def test_load_workspace_missing_env_raises(monkeypatch, tmp_path):
" user: u\n password: p\n transport: direct\n" " user: u\n password: p\n transport: direct\n"
"vector_rest:\n" "vector_rest:\n"
" base_url: 'https://v/'\n" " base_url: 'https://v/'\n"
" api_key: '${THOTH_VEC_API_KEY}'\n" " api_key: '${THT_VEC_API_KEY}'\n"
) )
try: try:
load_workspace(yaml) load_workspace(yaml)
assert False, "should have raised" assert False, "should have raised"
except WorkspaceError as e: except WorkspaceError as e:
assert "THOTH_VEC_API_KEY" in str(e) assert "THT_VEC_API_KEY" in str(e)
@@ -1,6 +1,6 @@
"""nsp CLI -- Typer app `nsp`. """tht CLI -- Typer app `tht`.
Minimal skeleton for now: registers `phase_app` (the gate needs `nsp phase meta --json`). Minimal skeleton for now: registers `phase_app` (the gate needs `tht phase meta --json`).
Other command groups (db, schema, lsh, vector, memory, session, decision, sql, cte, Other command groups (db, schema, lsh, vector, memory, session, decision, sql, cte,
evidence, search) are wired in their porting tasks, once their backend deps land evidence, search) are wired in their porting tasks, once their backend deps land
(A9 ports db/mschema/_guards; B1 ports vectorstore; B3 ports search/lshindex/sampling). (A9 ports db/mschema/_guards; B1 ports vectorstore; B3 ports search/lshindex/sampling).
@@ -9,10 +9,10 @@ from __future__ import annotations
import typer import typer
from nsp import __version__ from tht import __version__
app = typer.Typer( app = typer.Typer(
help="nsp (Natural Semantic Platform) -- kit operativo per generazione SQL assistita da Pi", help="tht (Thoth) -- kit operativo per generazione SQL assistito da Pi",
no_args_is_help=True, no_args_is_help=True,
) )
@@ -36,6 +36,6 @@ def main(
pass pass
from nsp.cli.phase_cmd import phase_app # noqa: E402 from tht.cli.phase_cmd import phase_app # noqa: E402
app.add_typer(phase_app, name="phase") app.add_typer(phase_app, name="phase")
@@ -1,6 +1,6 @@
"""nsp phase -- workflow HITL gate commands. """tht phase -- workflow HITL gate commands.
`meta` is the single source the gate (nsp-gate.js) reads workflow facts from, `meta` is the single source the gate (tht-gate.js) reads workflow facts from,
killing the JS/Python PHASE_NAMES drift (F2). advance/reopen/show land with the killing the JS/Python PHASE_NAMES drift (F2). advance/reopen/show land with the
session/db ports (A9) -- they need current_phase + load_session_or_exit which session/db ports (A9) -- they need current_phase + load_session_or_exit which
depend on the not-yet-ported session store helpers. depend on the not-yet-ported session store helpers.
@@ -11,7 +11,7 @@ import json
import typer import typer
from nsp.workflow import load_workflow from tht.workflow import load_workflow
phase_app = typer.Typer(help="Fase del workflow HITL (gate di avanzamento/ritorno)") phase_app = typer.Typer(help="Fase del workflow HITL (gate di avanzamento/ritorno)")
@@ -68,7 +68,7 @@ class ExamplesConfig(BaseModel):
class LshSkipConfig(BaseModel): class LshSkipConfig(BaseModel):
# DEPRECATO: l'euristica di lunghezza (skip_column vendored) non è più usata. La # DEPRECATO: l'euristica di lunghezza (skip_column vendored) non è più usata. La
# selezione delle colonne da indicizzare segue il principio di column eligibility # selezione delle colonne da indicizzare segue il principio di column eligibility
# (sezione `eligibility`). Mantenuto solo per compatibilità con nsp.yaml esistenti. # (sezione `eligibility`). Mantenuto solo per compatibilità con tht.yaml esistenti.
max_total_chars: int = 50000 max_total_chars: int = 50000
max_avg_length: int = 20 max_avg_length: int = 20
@@ -114,7 +114,7 @@ class VectorConfig(BaseModel):
class SearchConfig(BaseModel): class SearchConfig(BaseModel):
rrf_k: int = 60 rrf_k: int = 60
top_schema_tables: int = 12 # default `--top` per `nsp search --kind schema` (n. tabelle) top_schema_tables: int = 12 # default `--top` per `tht search --kind schema` (n. tabelle)
schema_chunk_pool: int = 150 # chunk tabella/colonna fusi prima dell'aggregazione a tabella schema_chunk_pool: int = 150 # chunk tabella/colonna fusi prima dell'aggregazione a tabella
@@ -135,7 +135,7 @@ class ExecutionConfig(BaseModel):
class Config(BaseModel): class Config(BaseModel):
database: DatabaseConfig database: DatabaseConfig
# Profilo dell'installazione, letto da THOTH_PROFILE (.env), non dallo yaml versionato. # Profilo dell'installazione, letto da THT_PROFILE (.env), non dallo yaml versionato.
# server: ricostruisce i derivati (artefatti, LSH, vettori schema nel vectordb). # server: ricostruisce i derivati (artefatti, LSH, vettori schema nel vectordb).
# workstation: postazione locale che legge il vectordb via REST; gli upsert remoti # workstation: postazione locale che legge il vectordb via REST; gli upsert remoti
# richiedono vector_write_rest, mentre init/clear/rebuild restano solo-server. # richiedono vector_write_rest, mentre init/clear/rebuild restano solo-server.
@@ -152,7 +152,7 @@ class Config(BaseModel):
execution: ExecutionConfig = ExecutionConfig() execution: ExecutionConfig = ExecutionConfig()
rest: RestConfig | None = None rest: RestConfig | None = None
# Endpoint REST dedicato per la similarity search del pgvector (rpc search_similar). # Endpoint REST dedicato per la similarity search del pgvector (rpc search_similar).
# Se presente, `nsp search` legge via REST; altrimenti legge in diretto (dev/test). # Se presente, `tht search` legge via REST; altrimenti legge in diretto (dev/test).
vector_rest: RestConfig | None = None vector_rest: RestConfig | None = None
# Endpoint REST dedicato alle scritture controllate del pgvector. E' opzionale e usa # Endpoint REST dedicato alle scritture controllate del pgvector. E' opzionale e usa
# una API key separata dalla lettura; espone solo upsert/hash via RPC allowlist. # una API key separata dalla lettura; espone solo upsert/hash via RPC allowlist.
@@ -169,11 +169,11 @@ def load_config(path: Path) -> Config:
cfg = Config.model_validate(_expand_env(raw)) cfg = Config.model_validate(_expand_env(raw))
except ValidationError as e: except ValidationError as e:
raise ConfigError(f"Configurazione non valida in {path}:\n{e}") from e raise ConfigError(f"Configurazione non valida in {path}:\n{e}") from e
env_profile = os.environ.get("THOTH_PROFILE") env_profile = os.environ.get("THT_PROFILE")
if env_profile is not None: if env_profile is not None:
if env_profile not in ("server", "workstation"): if env_profile not in ("server", "workstation"):
raise ConfigError( raise ConfigError(
f"THOTH_PROFILE non valido: {env_profile!r} (atteso 'server' o 'workstation')." f"THT_PROFILE non valido: {env_profile!r} (atteso 'server' o 'workstation')."
) )
cfg = cfg.model_copy(update={"profile": env_profile}) cfg = cfg.model_copy(update={"profile": env_profile})
if cfg.database.transport == "rest" and cfg.rest is None: if cfg.database.transport == "rest" and cfg.rest is None:
@@ -1,6 +1,6 @@
from sqlalchemy import Engine, create_engine, text from sqlalchemy import Engine, create_engine, text
from nsp.config import DatabaseConfig from tht.config import DatabaseConfig
def make_engine(cfg: DatabaseConfig) -> Engine: def make_engine(cfg: DatabaseConfig) -> Engine:
@@ -1,7 +1,7 @@
"""Recupero della catena di certificati presentata da un endpoint HTTPS. """Recupero della catena di certificati presentata da un endpoint HTTPS.
Serve al setup di una postazione *workstation* dietro una CA interna: scarica la Serve al setup di una postazione *workstation* dietro una CA interna: scarica la
catena TLS del server REST e la salva in un bundle PEM da puntare con `PSD_SSL_CA` catena TLS del server REST e la salva in un bundle PEM da puntare con `THT_SSL_CA`
(consumato da `requests` via `verify=`). NON installa nulla nel trust store dell'OS. (consumato da `requests` via `verify=`). NON installa nulla nel trust store dell'OS.
""" """
@@ -39,7 +39,7 @@ def fetch_chain_pem(host: str, port: int = 443, timeout: int = 30) -> list[str]:
L'handshake è volutamente *non verificato* (CERT_NONE): stiamo recuperando la catena L'handshake è volutamente *non verificato* (CERT_NONE): stiamo recuperando la catena
per poter poi *stabilire* la fiducia, non per fidarci adesso. La verifica vera avviene per poter poi *stabilire* la fiducia, non per fidarci adesso. La verifica vera avviene
in seguito quando `PSD_SSL_CA` punta al bundle salvato (es. `nsp db ping`). in seguito quando `THT_SSL_CA` punta al bundle salvato (es. `tht db ping`).
""" """
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.check_hostname = False ctx.check_hostname = False
@@ -2,7 +2,7 @@ from datetime import UTC, datetime
from sqlalchemy import Engine, text from sqlalchemy import Engine, text
from nsp.mschema.models import ( from tht.mschema.models import (
ColumnPhysical, ColumnPhysical,
ForeignKey, ForeignKey,
Index, Index,
@@ -10,7 +10,7 @@ from nsp.mschema.models import (
TablePhysical, TablePhysical,
) )
# Query adattate da thoth_sqldb2 (Apache 2.0) — vedi src/nsp/vendor/VENDORED.md. # Query adattate da thoth_sqldb2 (Apache 2.0) — vedi src/tht/vendor/VENDORED.md.
_TABLES_Q = text(""" _TABLES_Q = text("""
SELECT c.relname AS table_name, SELECT c.relname AS table_name,
@@ -3,8 +3,8 @@ from dataclasses import dataclass
from sqlalchemy import Engine, text from sqlalchemy import Engine, text
from nsp.config import ExamplesConfig, LshConfig from tht.config import ExamplesConfig, LshConfig
from nsp.mschema.models import Annotations, PhysicalSchema from tht.mschema.models import Annotations, PhysicalSchema
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -79,7 +79,7 @@ def unique_values_for_lsh(
(ORDER BY frequenza); se superano `max_values_per_column` la colonna è troncata e (ORDER BY frequenza); se superano `max_values_per_column` la colonna è troncata e
segnalata (mai tagliata in silenzio). segnalata (mai tagliata in silenzio).
""" """
from nsp.mschema.eligibility import effective_eligibility from tht.mschema.eligibility import effective_eligibility
annotations = annotations or Annotations() annotations = annotations or Annotations()
schema = physical.db_schema schema = physical.db_schema
@@ -129,7 +129,7 @@ def unique_values_for_lsh_rest(
Stessa logica di eligibility e di segnalazione del troncamento del transport diretto. Stessa logica di eligibility e di segnalazione del troncamento del transport diretto.
""" """
from nsp.mschema.eligibility import effective_eligibility from tht.mschema.eligibility import effective_eligibility
annotations = annotations or Annotations() annotations = annotations or Annotations()
schema = physical.db_schema schema = physical.db_schema
@@ -6,7 +6,7 @@ from pydantic import BaseModel
DECISIONS_FILE = "review_decisions.jsonl" DECISIONS_FILE = "review_decisions.jsonl"
# 22 tipi di ChironeWp3 (verified leggendo session/decisions.py) + 1 nuovo (D15): # 22 tipi di the reference implementation (verified leggendo session/decisions.py) + 1 nuovo (D15):
# `decision_retracted` per il rollback a granularità step (ritira una decisione # `decision_retracted` per il rollback a granularità step (ritira una decisione
# senza cancellarne la riga dal log di audit; effective_decisions la onora). # senza cancellarne la riga dal log di audit; effective_decisions la onora).
DecisionType = Literal[ DecisionType = Literal[
@@ -6,9 +6,9 @@ from pathlib import Path
from pydantic import BaseModel from pydantic import BaseModel
from nsp.decisions import DecisionRecord, DecisionType, list_decisions from tht.decisions import DecisionRecord, DecisionType, list_decisions
from nsp.session.models import SessionManifest from tht.session.models import SessionManifest
from nsp.vectorstore.records import VectorRecord from tht.vectorstore.records import VectorRecord
class MemoryRecord(BaseModel): class MemoryRecord(BaseModel):
@@ -32,7 +32,7 @@ def decided_memory_ids(decisions: list[DecisionRecord]) -> set[str]:
"""Id delle memorie gia' DECISE nella sessione: rifiutate (decisione """Id delle memorie gia' DECISE nella sessione: rifiutate (decisione
`memory_rejected`, id nel subject) o applicate (citate come `mem-XXXX` nel `memory_rejected`, id nel subject) o applicate (citate come `mem-XXXX` nel
rationale, per convenzione della checklist memorie). Servono a rationale, per convenzione della checklist memorie). Servono a
`nsp memory search --session` per non riproporre cio' che il reviewer ha `tht memory search --session` per non riproporre cio' che il reviewer ha
gia' deciso (fix: memorie scartate riproposte).""" gia' deciso (fix: memorie scartate riproposte)."""
out: set[str] = set() out: set[str] = set()
for d in decisions: for d in decisions:
@@ -239,8 +239,8 @@ def save_one_memory(
The destructive cleanup (sync's delete-stale step) is intentionally absent: it The destructive cleanup (sync's delete-stale step) is intentionally absent: it
remains a server-side-only operation via the direct vectordb connection. remains a server-side-only operation via the direct vectordb connection.
""" """
from nsp.vectorstore.rest_writer import pack_metadata from tht.vectorstore.rest_writer import pack_metadata
from nsp.vectorstore.store import content_hash from tht.vectorstore.store import content_hash
record = memory_vector_record_for_decision(records, decision_seq) record = memory_vector_record_for_decision(records, decision_seq)
if record is None: if record is None:
@@ -1,15 +1,15 @@
"""Classificazione di column eligibility (principio trasversale PsdWp3). """Classificazione di column eligibility (principio trasversale Thoth).
Vedi docs/superpowers/specs/2026-06-13-nsp-column-eligibility-principle.md. Vedi docs/superpowers/specs/2026-06-13-tht-column-eligibility-principle.md.
Il testo ampio (lettere di dimissione, note, anamnesi) è ignorato ovunque; i dati Il testo ampio (lettere di dimissione, note, anamnesi) è ignorato ovunque; i dati
provengono solo da numerici, enum, temporali, booleani e testo breve. provengono solo da numerici, enum, temporali, booleani e testo breve.
""" """
import re import re
from nsp.config import EligibilityConfig from tht.config import EligibilityConfig
from nsp.db.sampling import is_text_type from tht.db.sampling import is_text_type
from nsp.mschema.models import ColumnAnnotation, ColumnPhysical, PhysicalSchema from tht.mschema.models import ColumnAnnotation, ColumnPhysical, PhysicalSchema
_NUMERIC_PREFIXES = ( _NUMERIC_PREFIXES = (
"smallint", "integer", "bigint", "numeric", "decimal", "real", "double", "money", "smallint", "integer", "bigint", "numeric", "decimal", "real", "double", "money",
@@ -1,4 +1,4 @@
from nsp.mschema.models import Annotations, PhysicalSchema from tht.mschema.models import Annotations, PhysicalSchema
def find_orphans(physical: PhysicalSchema, annotations: Annotations) -> list[str]: def find_orphans(physical: PhysicalSchema, annotations: Annotations) -> list[str]:
@@ -1,7 +1,7 @@
from typing import Any from typing import Any
from nsp.mschema.eligibility import effective_eligibility from tht.mschema.eligibility import effective_eligibility
from nsp.mschema.models import Annotations, ColumnAnnotation, PhysicalSchema from tht.mschema.models import Annotations, ColumnAnnotation, PhysicalSchema
MAX_EXAMPLES_IN_PROMPT = 5 MAX_EXAMPLES_IN_PROMPT = 5
@@ -1,6 +1,6 @@
"""Phase machinery -- data-driven + effective_decisions (spec D15, F2, §4.8). """Phase machinery -- data-driven + effective_decisions (spec D15, F2, §4.8).
This is the single most important architectural fix vs ChironeWp3: ALL helpers This is the single most important architectural fix vs the reference implementation: ALL helpers
consult effective_decisions() instead of raw list_decisions(), so the reopen-aware consult effective_decisions() instead of raw list_decisions(), so the reopen-aware
view is consistent everywhere (fixes the bug where approved_ctes / advance_problems / view is consistent everywhere (fixes the bug where approved_ctes / advance_problems /
build_evidence conflated stale pre-reopen decisions with new ones). build_evidence conflated stale pre-reopen decisions with new ones).
@@ -16,9 +16,9 @@ from pathlib import Path
from pydantic import ValidationError from pydantic import ValidationError
from nsp.decisions import DecisionRecord, list_decisions from tht.decisions import DecisionRecord, list_decisions
from nsp.session.models import SchemaLinking from tht.session.models import SchemaLinking
from nsp.workflow import load_workflow from tht.workflow import load_workflow
def _phase_num(subject: str) -> int | None: def _phase_num(subject: str) -> int | None:
@@ -1,4 +1,4 @@
"""Client per il DWH Chirone esposto via Supabase/PostgREST. """Client per il the DWH esposto via Supabase/PostgREST.
Incapsula i 10 rpc verificati contro produzione. Errori sempre in italiano e azionabili Incapsula i 10 rpc verificati contro produzione. Errori sempre in italiano e azionabili
(stile `vectorstore/embeddings.py`). Lettura sola: l'API ammette solo SELECT/WITH. (stile `vectorstore/embeddings.py`). Lettura sola: l'API ammette solo SELECT/WITH.
@@ -6,7 +6,7 @@ Incapsula i 10 rpc verificati contro produzione. Errori sempre in italiano e azi
import requests import requests
from nsp.config import RestConfig from tht.config import RestConfig
class RestError(Exception): class RestError(Exception):
@@ -1,6 +1,6 @@
from pydantic import BaseModel from pydantic import BaseModel
from nsp.vectorstore.store import VectorStore from tht.vectorstore.store import VectorStore
class SearchResult(BaseModel): class SearchResult(BaseModel):
@@ -1,7 +1,7 @@
from pathlib import Path from pathlib import Path
from nsp.decisions import DecisionRecord from tht.decisions import DecisionRecord
from nsp.session.models import SchemaLinking from tht.session.models import SchemaLinking
def _find_evidence_file(evidence_root: Path, evidence_id: str) -> str: def _find_evidence_file(evidence_root: Path, evidence_id: str) -> str:
@@ -4,7 +4,7 @@ from typing import Literal
from pydantic import BaseModel, Field, ConfigDict from pydantic import BaseModel, Field, ConfigDict
# Stub locale di _YamlModel (in ChironeWp3 vive in mschema/models.py). # Stub locale di _YamlModel (in the reference implementation vive in mschema/models.py).
# Qui serve solo come base con populate_by_name; mschema/ sara' portato nel Task A9. # Qui serve solo come base con populate_by_name; mschema/ sara' portato nel Task A9.
class _YamlModel(BaseModel): class _YamlModel(BaseModel):
model_config = ConfigDict(populate_by_name=True) model_config = ConfigDict(populate_by_name=True)
@@ -1,9 +1,9 @@
from datetime import UTC, datetime from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from nsp.config import DatabaseConfig from tht.config import DatabaseConfig
from nsp.session.models import SessionManifest from tht.session.models import SessionManifest
from nsp.textutil import slugify from tht.textutil import slugify
MANIFEST = "session_manifest.yaml" MANIFEST = "session_manifest.yaml"
MAX_SLUG_CHARS = 40 MAX_SLUG_CHARS = 40
@@ -13,8 +13,8 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from nsp.phase import effective_decisions from tht.phase import effective_decisions
from nsp.workflow import load_workflow from tht.workflow import load_workflow
MAX_BODY_BYTES = 80_000 # ~20k token (target per task document di una fase) MAX_BODY_BYTES = 80_000 # ~20k token (target per task document di una fase)
@@ -1,7 +1,7 @@
"""Artifact teardown on rollback (spec D15, §4.8). """Artifact teardown on rollback (spec D15, §4.8).
teardown_to_phase cancella ogni artefatto la cui fase produttrice > target, usando la teardown_to_phase cancella ogni artefatto la cui fase produttrice > target, usando la
mappa artifacts_out di workflow.yaml. Risolve il bug latente di ChironeWp3: dopo un mappa artifacts_out di workflow.yaml. Risolve il bug latente di the reference implementation: dopo un
re-derive con piano CTE diverso, i vecchi ctes/*.sql orfani restavano su disco e re-derive con piano CTE diverso, i vecchi ctes/*.sql orfani restavano su disco e
bloccavano finalize (che itera glob('*.sql') esigendo che ognuno sia testato). bloccavano finalize (che itera glob('*.sql') esigendo che ognuno sia testato).
@@ -14,7 +14,7 @@ import shutil
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from nsp.workflow import load_workflow from tht.workflow import load_workflow
@dataclass @dataclass
@@ -1,6 +1,6 @@
import requests import requests
from nsp.config import EmbeddingsConfig from tht.config import EmbeddingsConfig
DOC_PREFIX = "search_document: " DOC_PREFIX = "search_document: "
QUERY_PREFIX = "search_query: " QUERY_PREFIX = "search_query: "
@@ -9,10 +9,10 @@ Entrambe mappano i `kind` sulle tabelle per-dominio dello schema `vectors`.
from sqlalchemy import Engine from sqlalchemy import Engine
from nsp.vectorstore.rest_client import VectorRestClient from tht.vectorstore.rest_client import VectorRestClient
from nsp.vectorstore.store import VectorHit, VectorStore, hit_from_metadata from tht.vectorstore.store import VectorHit, VectorStore, hit_from_metadata
# kind PsdWp3 → tabella dello schema `vectors`. # kind Thoth → tabella dello schema `vectors`.
KIND_TO_TABLE = { KIND_TO_TABLE = {
"schema_table": "schema_records", "schema_table": "schema_records",
"schema_column": "schema_records", "schema_column": "schema_records",
@@ -2,8 +2,8 @@ import re
from pydantic import BaseModel from pydantic import BaseModel
from nsp.evidence.model import EvidenceDoc from tht.evidence.model import EvidenceDoc
from nsp.mschema.models import Annotations, PhysicalSchema from tht.mschema.models import Annotations, PhysicalSchema
MAX_EXAMPLES_IN_RECORD = 5 MAX_EXAMPLES_IN_RECORD = 5
@@ -7,7 +7,7 @@ Errori in italiano e azionabili, stile `rest/client.py`.
import requests import requests
from nsp.config import RestConfig from tht.config import RestConfig
class VectorRestError(Exception): class VectorRestError(Exception):
@@ -5,9 +5,9 @@ Mantiene l'upsert incrementale del VectorStore diretto, ma non esegue delete/cle
operazioni distruttive restano solo-server via connessione Postgres diretta. operazioni distruttive restano solo-server via connessione Postgres diretta.
""" """
from nsp.vectorstore.records import VectorRecord from tht.vectorstore.records import VectorRecord
from nsp.vectorstore.rest_client import VectorRestClient from tht.vectorstore.rest_client import VectorRestClient
from nsp.vectorstore.store import SyncStats, content_hash from tht.vectorstore.store import SyncStats, content_hash
KIND_TO_TABLE = { KIND_TO_TABLE = {
@@ -4,7 +4,7 @@ from dataclasses import dataclass
from sqlalchemy import Engine, text from sqlalchemy import Engine, text
from nsp.vectorstore.records import VectorRecord from tht.vectorstore.records import VectorRecord
def content_hash(content: str) -> str: def content_hash(content: str) -> str:
@@ -180,7 +180,7 @@ class VectorStore:
def _pack_metadata(r: VectorRecord) -> dict: def _pack_metadata(r: VectorRecord) -> dict:
"""Impacchetta nel `metadata` (unica colonna letta via REST) tutta la semantica PsdWp3.""" """Impacchetta nel `metadata` (unica colonna letta via REST) tutta la semantica Thoth."""
return { return {
"kind": r.kind, "kind": r.kind,
"ref": r.ref, "ref": r.ref,
@@ -1,7 +1,7 @@
"""Reads workflow.yaml -- the SINGLE source of workflow truth (spec F2, §5.3). """Reads workflow.yaml -- the SINGLE source of workflow truth (spec F2, §5.3).
phase.py, the gate (nsp-gate.js), and the skill all read from here. phase.py, the gate (tht-gate.js), and the skill all read from here.
No more duplicated constants (the JS/Python drift bug in ChironeWp3 -- PHASE_NAMES No more duplicated constants (the JS/Python drift bug in the reference implementation -- PHASE_NAMES
truncated to 7 in JS -- is structurally impossible because there is one source). truncated to 7 in JS -- is structurally impossible because there is one source).
Edit workflow.yaml to change the workflow: add/reorder/merge/skip phases. Edit workflow.yaml to change the workflow: add/reorder/merge/skip phases.
@@ -1,9 +1,9 @@
"""Workspace YAML loading — the single boundary for workspace configuration (spec D3). """Workspace YAML loading — the single boundary for workspace configuration (spec D3).
Reads workspaces/<name>.yaml, expands ${VAR} from env, validates via the Config model Reads workspaces/<name>.yaml, expands ${VAR} from env, validates via the Config model
(ported from ChironeWp3). Future migration to a DB store would replace only this module. (ported from the reference implementation). Future migration to a DB store would replace only this module.
La struttura YAML rispecchia esattamente nsp/config.py: La struttura YAML rispecchia esattamente tht/config.py:
database + rest (DWH), vector_rest + vector_write_rest (pgvector, doppia key top-level), database + rest (DWH), vector_rest + vector_write_rest (pgvector, doppia key top-level),
vector_db (loading diretto, server-only), embeddings, evidence, execution. vector_db (loading diretto, server-only), embeddings, evidence, execution.
""" """
@@ -11,7 +11,7 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
from nsp.config import Config, ConfigError, load_config from tht.config import Config, ConfigError, load_config
class WorkspaceError(Exception): class WorkspaceError(Exception):
@@ -22,7 +22,7 @@ def load_workspace(path: str | Path) -> Config:
"""Carica e valida un workspace YAML. """Carica e valida un workspace YAML.
Espande ${VAR} dall'ambiente; se una variabile referenziata manca, raises WorkspaceError. Espande ${VAR} dall'ambiente; se una variabile referenziata manca, raises WorkspaceError.
Delega a load_config (portato da ChironeWp3) per la validazione del modello Config. Delega a load_config (portato da the reference implementation) per la validazione del modello Config.
""" """
path = Path(path) path = Path(path)
try: try:
@@ -1,20 +1,20 @@
# Workspace ThothII — Chirone (DWH remoto) per i test L2. # Workspace ThothII — test (DWH remoto) per i test L2.
# Struttura allineata a workspaces/chirone.example.yaml e nsp/config.py. # Struttura allineata a workspaces/tht.example.yaml e tht/config.py.
# I segreti vivono SOLO in .env (${THOTH_*}). # I segreti vivono SOLO in .env (${THT_*}).
database: database:
host: ${THOTH_DB_HOST} host: ${THT_DB_HOST}
port: ${THOTH_DB_PORT} port: ${THT_DB_PORT}
database: ${THOTH_DB_NAME} database: ${THT_DB_NAME}
schema: datawarehouse schema: datawarehouse
user: ${THOTH_DB_USER} user: ${THT_DB_USER}
password: ${THOTH_DB_PASSWORD} password: ${THT_DB_PASSWORD}
transport: rest transport: rest
rest: rest:
base_url: ${THOTH_DWH_REST_URL} base_url: ${THT_DWH_REST_URL}
api_key: ${THOTH_DWH_API_KEY} api_key: ${THT_DWH_API_KEY}
ssl_ca: ${THOTH_SSL_CA} ssl_ca: ${THT_SSL_CA}
paths: paths:
artifacts: artifacts artifacts: artifacts
@@ -22,7 +22,7 @@ paths:
sessions: sessions sessions: sessions
embeddings: embeddings:
base_url: ${THOTH_OLLAMA_URL} base_url: ${THT_OLLAMA_URL}
model: nomic-embed-text-v2-moe model: nomic-embed-text-v2-moe
dim: 768 dim: 768
batch_size: 64 batch_size: 64
@@ -32,26 +32,26 @@ embeddings:
# li richiede, per cui si appoggiano alle stesse variabili del server (popolate nel # li richiede, per cui si appoggiano alle stesse variabili del server (popolate nel
# .env del server; sul workstation restano dummy/inediti finche' transport=rest). # .env del server; sul workstation restano dummy/inediti finche' transport=rest).
vector_db: vector_db:
host: ${THOTH_VEC_HOST} host: ${THT_VEC_HOST}
port: ${THOTH_VEC_PORT} port: ${THT_VEC_PORT}
database: postgres database: postgres
schema: vectors schema: vectors
user: ${THOTH_VEC_USER} user: ${THT_VEC_USER}
password: ${THOTH_VEC_PASSWORD} password: ${THT_VEC_PASSWORD}
# LETTURA del pgvector via REST remota (rpc search_similar), ruolo vector_reader. # LETTURA del pgvector via REST remota (rpc search_similar), ruolo vector_reader.
vector_rest: vector_rest:
base_url: ${THOTH_VEC_REST_URL} base_url: ${THT_VEC_REST_URL}
api_key: ${THOTH_VEC_API_KEY} api_key: ${THT_VEC_API_KEY}
ssl_ca: ${THOTH_SSL_CA} ssl_ca: ${THT_SSL_CA}
# SCRITTURA controllata del pgvector via REST remota (upsert/hash via RPC allowlist), # SCRITTURA controllata del pgvector via REST remota (upsert/hash via RPC allowlist),
# ruolo vector_writer. Path DEDICATO /vector/write/v1/ (diverso dal reader): la chiave # ruolo vector_writer. Path DEDICATO /vector/write/v1/ (diverso dal reader): la chiave
# writer vale solo qui, la reader solo su /vector/v1/. Key SEPARATA dalla lettura. # writer vale solo qui, la reader solo su /vector/v1/. Key SEPARATA dalla lettura.
vector_write_rest: vector_write_rest:
base_url: ${THOTH_VEC_WRITE_REST_URL} base_url: ${THT_VEC_WRITE_REST_URL}
api_key: ${THOTH_VEC_WRITE_API_KEY} api_key: ${THT_VEC_WRITE_API_KEY}
ssl_ca: ${THOTH_SSL_CA} ssl_ca: ${THT_SSL_CA}
vector: vector:
max_chunk_chars: 4000 max_chunk_chars: 4000
@@ -1,22 +1,22 @@
# Workspace ThothII — Chirone (esempio). I segreti vivono SOLO in .env (${THOTH_*}). # Workspace ThothII (esempio). I segreti vivono SOLO in .env (${THT_*}).
# La struttura rispecchia esattamente nsp/config.py (portato da ChironeWp3): # La struttura rispecchia esattamente tht/config.py:
# database + rest per il DWH; vector_rest/vector_write_rest per il pgvector (doppia key); # database + rest per il DWH; vector_rest/vector_write_rest per il pgvector (doppia key);
# vector_db per il loading diretto (server-only); embeddings + evidence + execution. # vector_db per il loading diretto (server-only); embeddings + evidence + execution.
database: database:
host: ${THOTH_DB_HOST} host: ${THT_DB_HOST}
port: ${THOTH_DB_PORT} # es. 5437 (Postgres diretto Supabase; 5432 = pooler) port: ${THT_DB_PORT} # es. 5437 (Postgres diretto Supabase; 5432 = pooler)
database: ${THOTH_DB_NAME} # es. postgres (lo schema a stella vive in `datawarehouse`) database: ${THT_DB_NAME} # es. postgres (lo schema a stella vive in `datawarehouse`)
schema: datawarehouse schema: datawarehouse
user: ${THOTH_DB_USER} user: ${THT_DB_USER}
password: ${THOTH_DB_PASSWORD} password: ${THT_DB_PASSWORD}
transport: rest # direct (Postgres) | rest (Supabase/PostgREST) transport: rest # direct (Postgres) | rest (Supabase/PostgREST)
# Accesso al DWH via REST (richiesto se database.transport = rest). # Accesso al DWH via REST (richiesto se database.transport = rest).
rest: rest:
base_url: ${THOTH_DWH_REST_URL} # es. https://supabase-aritmolab.policlinicosandonato.it/dwh/ base_url: ${THT_DWH_REST_URL} # es. https://supabase-aritmolab.policlinicosandonato.it/dwh/
api_key: ${THOTH_DWH_API_KEY} # header X-API-Key, ruolo dwh_reader (read-only) api_key: ${THT_DWH_API_KEY} # header X-API-Key, ruolo dwh_reader (read-only)
ssl_ca: ${THOTH_SSL_CA} # path al certificato CA (per server con CA interna) ssl_ca: ${THT_SSL_CA} # path al certificato CA (per server con CA interna)
paths: paths:
artifacts: artifacts artifacts: artifacts
@@ -39,11 +39,11 @@ eligibility:
ignore_columns: [etl_last_update] ignore_columns: [etl_last_update]
evidence: evidence:
source_root: ${THOTH_DOCS_ROOT} # es. /Users/mp/Chirone/chirone/etl/docs source_root: ${THT_DOCS_ROOT} # es. harness/evidence (cartella curata dentro ThothII)
evidence_dir: evidence # cartella curata a mano: unica fonte delle evidence evidence_dir: evidence # cartella curata a mano: unica fonte delle evidence
embeddings: embeddings:
base_url: ${THOTH_OLLAMA_URL} # es. http://localhost:11434 base_url: ${THT_OLLAMA_URL} # es. http://localhost:11434
model: nomic-embed-text-v2-moe model: nomic-embed-text-v2-moe
dim: 768 dim: 768
batch_size: 32 batch_size: 32
@@ -51,27 +51,27 @@ embeddings:
# LOADING del pgvector: connessione diretta, eseguita sul server (profilo server). # LOADING del pgvector: connessione diretta, eseguita sul server (profilo server).
# Opzionale su postazione remota (lì la lettura passa da vector_rest). # Opzionale su postazione remota (lì la lettura passa da vector_rest).
vector_db: vector_db:
host: ${THOTH_VEC_HOST} # Postgres locale del server host: ${THT_VEC_HOST} # Postgres locale del server
port: ${THOTH_VEC_PORT} # es. 5437 port: ${THT_VEC_PORT} # es. 5437
database: postgres database: postgres
schema: vectors schema: vectors
user: ${THOTH_VEC_USER} user: ${THT_VEC_USER}
password: ${THOTH_VEC_PASSWORD} password: ${THT_VEC_PASSWORD}
# LETTURA (similarity search) del pgvector via REST remota: rpc search_similar. # LETTURA (similarity search) del pgvector via REST remota: rpc search_similar.
vector_rest: vector_rest:
base_url: ${THOTH_VEC_REST_URL} # es. https://host/vector/v1/ base_url: ${THT_VEC_REST_URL} # es. https://host/vector/v1/
api_key: ${THOTH_VEC_API_KEY} # header X-API-Key, ruolo vector_reader (read-only) api_key: ${THT_VEC_API_KEY} # header X-API-Key, ruolo vector_reader (read-only)
ssl_ca: ${THOTH_SSL_CA} ssl_ca: ${THT_SSL_CA}
# SCRITTURA controllata del pgvector via REST remota: upsert/hash via RPC allowlist, # SCRITTURA controllata del pgvector via REST remota: upsert/hash via RPC allowlist,
# niente delete/clear. Usa una API key SEPARATA dalla lettura (ruolo vector_writer). # niente delete/clear. Usa una API key SEPARATA dalla lettura (ruolo vector_writer).
# OPZIONALE: assente o key vuota = scrittura non abilitata (solo lettura). # OPZIONALE: assente o key vuota = scrittura non abilitata (solo lettura).
# Abilita nsp memory save-one / vector index-schema da postazione remota. # Abilita tht memory save-one / vector index-schema da postazione remota.
vector_write_rest: vector_write_rest:
base_url: ${THOTH_VEC_REST_URL} base_url: ${THT_VEC_REST_URL}
api_key: ${THOTH_VEC_WRITE_API_KEY} api_key: ${THT_VEC_WRITE_API_KEY}
ssl_ca: ${THOTH_SSL_CA} ssl_ca: ${THT_SSL_CA}
vector: vector:
max_chunk_chars: 4000 max_chunk_chars: 4000