feat(gate): reviewer_schema_linking tool with per-column curation + deterministic sync
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
committed by
Marco Pancotti
co-authored by
Claude Opus 4.8
parent
559f52e26e
commit
ad788278dd
@@ -29,6 +29,7 @@ import {
|
||||
buildSelectRequest,
|
||||
buildMultiselectRequest,
|
||||
buildArtifactGate,
|
||||
buildSchemaLinkingRequest,
|
||||
} from "./gate/builders.js";
|
||||
import { isReserved } from "./reserved-labels.mjs";
|
||||
|
||||
@@ -67,6 +68,14 @@ export function prepareReviewerArguments(input) {
|
||||
/* not JSON */
|
||||
}
|
||||
}
|
||||
if (typeof args.tables === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(args.tables);
|
||||
if (Array.isArray(parsed)) args.tables = parsed;
|
||||
} catch {
|
||||
/* not JSON */
|
||||
}
|
||||
}
|
||||
// reviewer_confirm's `artifact` is a Type.Object; some models send it as a
|
||||
// stringified JSON object, which fails validation BEFORE execute() and makes the
|
||||
// model loop. Coerce it back to an object so the gate proceeds.
|
||||
@@ -610,6 +619,104 @@ export default function (pi) {
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: "reviewer_schema_linking",
|
||||
label: "Schema linking: tabelle/colonne/esclusioni (reviewer)",
|
||||
description:
|
||||
"F4: presenta al reviewer le tabelle da promuovere/escludere con la loro descrizione " +
|
||||
"e, per ogni tabella, le colonne dal catalogo (le suggerite pre-selezionate). Il reviewer " +
|
||||
"cura le colonne di ogni tabella promossa. PERSISTE table_promoted/table_excluded e " +
|
||||
"column_promoted/column_excluded, poi riproietta schema_linking.json in modo deterministico " +
|
||||
"(tht session sync-schema-linking). `tables[]`: {id, name, kind: 'promote'|'exclude', " +
|
||||
"rationale?, suggested_columns?: string[]}. Le colonne complete arrivano dal catalogo, non dal modello.",
|
||||
parameters: Type.Object({
|
||||
session: Type.String(),
|
||||
title: Type.String(),
|
||||
tables: Type.Array(
|
||||
Type.Object({
|
||||
id: Type.String(),
|
||||
name: Type.String(),
|
||||
kind: Type.Union([Type.Literal("promote"), Type.Literal("exclude")]),
|
||||
rationale: Type.Optional(Type.String()),
|
||||
suggested_columns: Type.Optional(Type.Array(Type.String())),
|
||||
recommended: Type.Optional(Type.Boolean()),
|
||||
}),
|
||||
),
|
||||
advance: Type.Optional(Type.Boolean()),
|
||||
}),
|
||||
prepareArguments: prepareReviewerArguments,
|
||||
async execute(_id, params, _signal, _onUpdate, ctx) {
|
||||
lockActive = true;
|
||||
const { session, title, tables, advance } = params;
|
||||
const phase = phaseId(ctx, currentPhase(ctx, session));
|
||||
|
||||
// Enrich each table with its full catalog columns (deterministic source).
|
||||
const enriched = tables.map((t) => {
|
||||
const cat = JSON.parse(tht(ctx, ["schema", "columns", t.name, "--json"]));
|
||||
const suggested = new Set(t.suggested_columns ?? []);
|
||||
return {
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
kind: t.kind,
|
||||
recommended: t.recommended ?? true,
|
||||
description: cat.description ?? "",
|
||||
rationale: t.rationale ?? "",
|
||||
columns: cat.columns.map((c) => ({ ...c, suggested: suggested.has(c.name) })),
|
||||
};
|
||||
});
|
||||
|
||||
const widget = buildSchemaLinkingRequest({
|
||||
id: `u${Date.now()}`,
|
||||
phase,
|
||||
title,
|
||||
tables: enriched,
|
||||
});
|
||||
const resp = await emitAndWait(ctx, widget);
|
||||
if (resp.control === "back") return textResult("Il reviewer vuole tornare indietro.");
|
||||
if (resp.control === "exit") return textResult("Il reviewer vuole uscire.");
|
||||
if (resp.control === "freetext")
|
||||
return textResult(`Altro (reviewer): ${resp.text}. Riformula tenendone conto.`);
|
||||
|
||||
const byId = new Map(enriched.map((t) => [t.id, t]));
|
||||
let n = 0;
|
||||
for (const rt of resp.tables ?? []) {
|
||||
const t = byId.get(rt.id);
|
||||
if (!t || !rt.enacted) continue;
|
||||
if (t.kind === "promote") {
|
||||
const e1 = relayIfThtFails(ctx, decisionAddArgs(session, {
|
||||
type: "table_promoted", subject: t.name, detail: t.description, rationale: t.rationale,
|
||||
}), "");
|
||||
if (e1) return e1;
|
||||
n++;
|
||||
const sel = new Set(rt.columns ?? []);
|
||||
for (const c of t.columns) {
|
||||
const type = sel.has(c.name)
|
||||
? "column_promoted"
|
||||
: (c.suggested ? "column_excluded" : null);
|
||||
if (!type) continue;
|
||||
const e2 = relayIfThtFails(ctx, decisionAddArgs(session, {
|
||||
type, subject: `${t.name}.${c.name}`, detail: c.description ?? "",
|
||||
}), "");
|
||||
if (e2) return e2;
|
||||
}
|
||||
} else {
|
||||
const e3 = relayIfThtFails(ctx, decisionAddArgs(session, {
|
||||
type: "table_excluded", subject: t.name, detail: t.description, rationale: t.rationale,
|
||||
}), "");
|
||||
if (e3) return e3;
|
||||
n++;
|
||||
}
|
||||
}
|
||||
|
||||
// Deterministic projection of the ledger into schema_linking.json.
|
||||
const eSync = relayIfThtFails(ctx, ["session", "sync-schema-linking", session], "");
|
||||
if (eSync) return eSync;
|
||||
|
||||
if (advance) advanceIfReady(ctx, session);
|
||||
return textResult(`Schema linking registrato dal reviewer (${n} tabelle + colonne curate).`);
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: "reviewer_confirm",
|
||||
label: "Gate di avanzamento (reviewer)",
|
||||
|
||||
Reference in New Issue
Block a user