refactor(disambiguation): own F1 clarification policy (#27)

This commit is contained in:
2026-08-24 01:55:42 +02:00
parent 2ed55ef131
commit 1e459b073e
3 changed files with 327 additions and 34 deletions
@@ -14,6 +14,108 @@ function normalizedAssumptions(assumptions) {
}
function prepareClarificationArguments(input) {
if (!input || typeof input !== "object") return input;
const args = { ...input };
if (typeof args.options === "string") {
try {
const parsed = JSON.parse(args.options);
if (Array.isArray(parsed)) args.options = parsed;
} catch {
// Preserve non-JSON input for the TypeBox/tool validation path.
}
}
return args;
}
function decisionRecordedText(decision, option) {
return (
`Decisione registrata (${decision.type}): ${option.label}. ` +
"Ora rileggi lo stato persistito con `tht session show` e invoca immediatamente " +
"il prossimo tool reviewer_ richiesto dal workflow. " +
"Non scrivere analisi o spiegazioni visibili."
);
}
function validateClarificationOptions(ctx, options, session, workflow) {
const known = workflow.decisionTypes(ctx);
if (!known) return null;
for (const option of options) {
if (option.decision && !known.has(option.decision.type)) {
return (
`Tipo di decisione '${option.decision.type}' non valido. Tipi ammessi: ` +
`${[...known].join(", ")}. Correggi e riprova.`
);
}
}
const current = workflow.phase(ctx, session).number;
const minimumPhases = workflow.decisionMinimumPhases(ctx);
for (const option of options) {
const type = option.decision?.type;
if (type && minimumPhases[type] && minimumPhases[type] > current) {
return (
`Tipo '${type}' ammesso dalla Fase ${minimumPhases[type]}, ` +
`sessione alla Fase ${current}. Chiudi prima la fase corrente.`
);
}
}
return null;
}
function prepareClarification(
ctx,
params,
id,
{ workflow, reviewer, toTextResult },
) {
const { session, title, options, intro, advance } = params;
const validationError = validateClarificationOptions(ctx, options, session, workflow);
if (validationError) return { result: toTextResult(validationError) };
const phase = workflow.describe(ctx, session);
const recommended = options.find((option) => option.recommended)?.id ?? null;
return {
session,
options,
advance,
widget: reviewer.buildSelect({
id,
phase,
title,
intro: intro ?? null,
recommended,
options: options
.filter((option) => !reviewer.isReserved(option.label))
.map((option) => ({ id: option.id, label: option.label })),
}),
};
}
function resolveClarification(prepared, response) {
if (response?.control === "freetext") {
return { text: `Altro (reviewer): ${response.text}` };
}
if (response?.control === "back") {
return { text: "Il reviewer vuole tornare indietro." };
}
if (response?.control === "exit") {
return { text: "Il reviewer vuole uscire." };
}
const choice = Array.isArray(response?.choices) ? response.choices[0] : response?.choice;
const option = (prepared.options || []).find((candidate) => candidate.id === choice) || null;
if (option?.decision) {
return {
decision: option.decision,
text: decisionRecordedText(option.decision, option),
};
}
return { text: `Scelta del reviewer: ${option ? option.label : choice}` };
}
function installDisambiguationGate(
pi,
{ workflow, session, ledger, toTextResult },
@@ -73,5 +175,9 @@ function installDisambiguationGate(
export function createDisambiguationGate(dependencies) {
return {
install: (pi) => installDisambiguationGate(pi, dependencies),
prepareClarificationArguments,
prepareClarification: (ctx, params, id) =>
prepareClarification(ctx, params, id, dependencies),
resolveClarification,
};
}