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
@@ -0,0 +1,203 @@
const test = require("node:test");
const assert = require("node:assert");
const { createDisambiguationGate } = require("../disambiguation/index.js");
function setupClarification({
phase = 1,
knownTypes = new Set(["concept_clarified", "ambiguity_open", "value_grounded"]),
minimumPhases = {
concept_clarified: 1,
ambiguity_open: 1,
value_grounded: 3,
},
} = {}) {
const calls = [];
const gate = createDisambiguationGate({
workflow: {
phase: (_ctx, session) => {
calls.push(["phase", session]);
return { number: phase };
},
describe: (_ctx, session) => {
calls.push(["describe", session]);
return "F1";
},
decisionTypes: () => {
calls.push(["decisionTypes"]);
return knownTypes;
},
decisionMinimumPhases: () => {
calls.push(["decisionMinimumPhases"]);
return minimumPhases;
},
advance: () => ({ advanced: false }),
},
session: { mutate: () => null },
ledger: {
record: () => null,
},
reviewer: {
buildSelect: (input) => {
calls.push(["build", input]);
return { type: "ui_request", widget: "select", ...input };
},
isReserved: (label) => ["Altro", "Other"].includes(label),
},
toTextResult: (text) => ({ content: [{ type: "text", text }] }),
});
return { gate, calls };
}
const OPTIONS = [
{
id: "procedure",
label: "Procedura clinica",
recommended: true,
decision: {
type: "concept_clarified",
subject: "ablazione",
detail: "procedura clinica",
rationale: "scelta dal reviewer",
},
},
{ id: "ask", label: "Serve un altro chiarimento" },
{ id: "other", label: "Altro" },
];
test("Disambiguation normalizes stringified F1 options", () => {
const { gate } = setupClarification();
assert.deepEqual(
gate.prepareClarificationArguments({ session: "s1", options: JSON.stringify(OPTIONS) }),
{ session: "s1", options: OPTIONS },
);
assert.deepEqual(
gate.prepareClarificationArguments({ session: "s1", options: "not-json" }),
{ session: "s1", options: "not-json" },
);
});
test("Disambiguation validates and presents F1 clarification options", () => {
const { gate, calls } = setupClarification();
const prepared = gate.prepareClarification({}, {
session: "s1",
title: "Che cosa significa ablazione?",
intro: "Una sola interpretazione può essere corretta.",
options: OPTIONS,
advance: true,
}, "u1");
assert.deepEqual(calls.map(([kind]) => kind), [
"decisionTypes", "phase", "decisionMinimumPhases", "describe", "build",
]);
assert.deepEqual(prepared, {
session: "s1",
options: OPTIONS,
advance: true,
widget: {
type: "ui_request",
widget: "select",
id: "u1",
phase: "F1",
title: "Che cosa significa ablazione?",
intro: "Una sola interpretazione può essere corretta.",
recommended: "procedure",
options: [
{ id: "procedure", label: "Procedura clinica" },
{ id: "ask", label: "Serve un altro chiarimento" },
],
},
});
});
test("Disambiguation rejects an unknown F1 decision type before presentation", () => {
const { gate, calls } = setupClarification();
const invalidOptions = [{
id: "invalid",
label: "Invalida",
decision: { type: "not_a_decision", subject: "x" },
}];
const prepared = gate.prepareClarification({}, {
session: "s1",
title: "Domanda",
options: invalidOptions,
}, "u1");
assert.deepEqual(calls.map(([kind]) => kind), ["decisionTypes"]);
assert.equal(
prepared.result.content[0].text,
"Tipo di decisione 'not_a_decision' non valido. Tipi ammessi: " +
"concept_clarified, ambiguity_open, value_grounded. Correggi e riprova.",
);
});
test("Disambiguation rejects a decision that belongs to a later phase", () => {
const { gate, calls } = setupClarification();
const futureOptions = [{
id: "grounded",
label: "Valore verificato",
decision: { type: "value_grounded", subject: "stato" },
}];
const prepared = gate.prepareClarification({}, {
session: "s1",
title: "Domanda",
options: futureOptions,
}, "u1");
assert.deepEqual(calls.map(([kind]) => kind), [
"decisionTypes", "phase", "decisionMinimumPhases",
]);
assert.equal(
prepared.result.content[0].text,
"Tipo 'value_grounded' ammesso dalla Fase 3, sessione alla Fase 1. " +
"Chiudi prima la fase corrente.",
);
});
test("Disambiguation translates F1 choices into domain decisions or clarification requests", () => {
const { gate } = setupClarification();
const prepared = { options: OPTIONS };
const decision = gate.resolveClarification(prepared, { choices: ["procedure"] });
assert.deepEqual(decision.decision, OPTIONS[0].decision);
assert.match(decision.text, /Decisione registrata \(concept_clarified\): Procedura clinica\./);
assert.match(decision.text, /tht session show/);
assert.deepEqual(
gate.resolveClarification(prepared, { choices: ["ask"] }),
{ text: "Scelta del reviewer: Serve un altro chiarimento" },
);
assert.deepEqual(
gate.resolveClarification(prepared, { choices: ["missing"] }),
{ text: "Scelta del reviewer: missing" },
);
});
test("Disambiguation never translates F1 control responses into decisions", () => {
const { gate } = setupClarification();
const prepared = { options: OPTIONS };
assert.deepEqual(
gate.resolveClarification(prepared, { control: "freetext", text: "un altro significato" }),
{ text: "Altro (reviewer): un altro significato" },
);
assert.deepEqual(
gate.resolveClarification(prepared, { control: "back" }),
{ text: "Il reviewer vuole tornare indietro." },
);
assert.deepEqual(
gate.resolveClarification(prepared, { control: "exit" }),
{ text: "Il reviewer vuole uscire." },
);
});