fix(f4): harden schema-columns enrichment + ValidationError guard + staged-helper note
This commit is contained in:
@@ -120,3 +120,51 @@ test("reviewer_schema_linking records table/column decisions and syncs schema_li
|
|||||||
cp.execFileSync = origExecFileSync;
|
cp.execFileSync = origExecFileSync;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("reviewer_schema_linking returns a textResult (not a throw) when a proposed table is not in the catalog", async () => {
|
||||||
|
const origExecFileSync = cp.execFileSync;
|
||||||
|
cp.execFileSync = (file, args) => {
|
||||||
|
if (args[0] === "phase" && args[1] === "meta")
|
||||||
|
return JSON.stringify({ phases: [{ num: 4, id: "F4" }] });
|
||||||
|
if (args[0] === "phase" && args[1] === "show") return "Fase corrente: 4\n";
|
||||||
|
if (args[0] === "schema" && args[1] === "columns" && args[2] === "badtable") {
|
||||||
|
const err = new Error("Command failed");
|
||||||
|
err.stderr = "ERRORE: tabella 'badtable' non trovata nel catalogo";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const gate = require(GATE);
|
||||||
|
const { createFakePi } = require("./fake_pi_runtime.js");
|
||||||
|
const { pi, ctx, tools } = createFakePi();
|
||||||
|
ctx.cwd = "/nonexistent-thothii-test-cwd";
|
||||||
|
gate.default(pi);
|
||||||
|
|
||||||
|
const tool = tools.get("reviewer_schema_linking");
|
||||||
|
assert.ok(tool, "reviewer_schema_linking must be registered");
|
||||||
|
|
||||||
|
const result = await tool.def.execute(
|
||||||
|
"call-1",
|
||||||
|
{
|
||||||
|
session: "s1",
|
||||||
|
title: "Schema linking",
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
id: "t-bad",
|
||||||
|
name: "badtable",
|
||||||
|
kind: "promote",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(result.content[0].text, /badtable/);
|
||||||
|
} finally {
|
||||||
|
cp.execFileSync = origExecFileSync;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -651,10 +651,19 @@ export default function (pi) {
|
|||||||
const phase = phaseId(ctx, currentPhase(ctx, session));
|
const phase = phaseId(ctx, currentPhase(ctx, session));
|
||||||
|
|
||||||
// Enrich each table with its full catalog columns (deterministic source).
|
// Enrich each table with its full catalog columns (deterministic source).
|
||||||
const enriched = tables.map((t) => {
|
const enriched = [];
|
||||||
const cat = JSON.parse(tht(ctx, ["schema", "columns", t.name, "--json"]));
|
for (const t of tables) {
|
||||||
|
let cat;
|
||||||
|
try {
|
||||||
|
cat = JSON.parse(tht(ctx, ["schema", "columns", t.name, "--json"]));
|
||||||
|
} catch (e) {
|
||||||
|
const msg = (e.stderr || e.message || String(e)).toString().trim();
|
||||||
|
return textResult(
|
||||||
|
`Tabella '${t.name}' non caricabile dal catalogo (${msg}). Proponi solo tabelle presenti nel catalogo (usa 'tht schema render' / 'tht search' per verificarne i nomi).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
const suggested = new Set(t.suggested_columns ?? []);
|
const suggested = new Set(t.suggested_columns ?? []);
|
||||||
return {
|
enriched.push({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
name: t.name,
|
name: t.name,
|
||||||
kind: t.kind,
|
kind: t.kind,
|
||||||
@@ -662,8 +671,8 @@ export default function (pi) {
|
|||||||
description: cat.description ?? "",
|
description: cat.description ?? "",
|
||||||
rationale: t.rationale ?? "",
|
rationale: t.rationale ?? "",
|
||||||
columns: cat.columns.map((c) => ({ ...c, suggested: suggested.has(c.name) })),
|
columns: cat.columns.map((c) => ({ ...c, suggested: suggested.has(c.name) })),
|
||||||
};
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
const widget = buildSchemaLinkingRequest({
|
const widget = buildSchemaLinkingRequest({
|
||||||
id: `u${Date.now()}`,
|
id: `u${Date.now()}`,
|
||||||
|
|||||||
@@ -147,11 +147,17 @@ def sync_schema_linking_cmd(
|
|||||||
config: Path = CONFIG_OPT,
|
config: Path = CONFIG_OPT,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Riproietta schema_linking.json dalle decisioni F4 del ledger (deterministico)."""
|
"""Riproietta schema_linking.json dalle decisioni F4 del ledger (deterministico)."""
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from tht.session.store import sync_schema_linking
|
from tht.session.store import sync_schema_linking
|
||||||
|
|
||||||
cfg = _load_config_or_exit(config)
|
cfg = _load_config_or_exit(config)
|
||||||
load_session_or_exit(cfg, session_id)
|
load_session_or_exit(cfg, session_id)
|
||||||
path = sync_schema_linking(session_id, cfg.paths.sessions)
|
try:
|
||||||
|
path = sync_schema_linking(session_id, cfg.paths.sessions)
|
||||||
|
except ValidationError as e:
|
||||||
|
typer.secho(f"ERRORE: schema_linking non valido:\n{e}", fg=typer.colors.RED, err=True)
|
||||||
|
raise typer.Exit(code=5)
|
||||||
typer.secho(f"OK: schema_linking.json riproiettato ({path}).", fg=typer.colors.GREEN)
|
typer.secho(f"OK: schema_linking.json riproiettato ({path}).", fg=typer.colors.GREEN)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ def promoted_tables_for(cfg, session_id: str | None) -> set[str] | None:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Staged for a future SQL-projection consumer; hard column enforcement is a
|
||||||
|
# documented non-goal (Option-1 soft) — not yet wired into validate_or_exit.
|
||||||
def promoted_columns_for(cfg, session_id: str | None) -> set[str] | None:
|
def promoted_columns_for(cfg, session_id: str | None) -> set[str] | None:
|
||||||
if session_id is None:
|
if session_id is None:
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user