refactor(memory): extract F2 recall path (#23)

This commit is contained in:
2026-08-24 01:08:15 +02:00
parent beac2e80f4
commit 93fe0d733b
8 changed files with 585 additions and 236 deletions
@@ -1,16 +0,0 @@
const test = require("node:test");
const assert = require("node:assert");
const { shouldSkipEmptyDecide } = require("../../tht-gate.js");
test("skips the widget only when empty AND allow_empty AND advance", () => {
assert.equal(shouldSkipEmptyDecide({ meritCount: 0, allowEmpty: true, advance: true }), true);
});
test("does not skip when there are merito options", () => {
assert.equal(shouldSkipEmptyDecide({ meritCount: 2, allowEmpty: true, advance: true }), false);
});
test("does not skip when allow_empty is false or advance is false", () => {
assert.equal(shouldSkipEmptyDecide({ meritCount: 0, allowEmpty: false, advance: true }), false);
assert.equal(shouldSkipEmptyDecide({ meritCount: 0, allowEmpty: true, advance: false }), false);
});
@@ -1,6 +1,6 @@
const test = require("node:test");
const assert = require("node:assert");
const { installMemoryGate } = require("../memory/index.js");
const { createMemoryGate } = require("../memory/index.js");
const { createFakePi } = require("./fake_pi_runtime.js");
@@ -44,7 +44,7 @@ test("the public Memory facade owns F8 policy and mutation ordering", async () =
return JSON.stringify({ id: descriptor.id, choices: ["seq-5"] });
};
installMemoryGate(pi, {
const memoryGate = createMemoryGate({
workflow: {
activate: () => calls.push(["activate"]),
phase: () => ({ number: 8, id: "F8" }),
@@ -80,6 +80,7 @@ test("the public Memory facade owns F8 policy and mutation ordering", async () =
},
toTextResult: (text) => ({ content: [{ type: "text", text }] }),
});
memoryGate.install(pi);
const result = await tools.get("reviewer_memory_promote").def.execute(
"promote-via-facade",
@@ -1,94 +1,180 @@
const test = require("node:test");
const assert = require("node:assert");
const path = require("node:path");
const {
memorySelectionWidgetProps,
normalizeMemoryOptions,
} = require("../../tht-gate.js");
const { createMemoryGate } = require("../memory/index.js");
const { createFakePi } = require("./fake_pi_runtime.js");
test("reviewer_decide accepts the exact memory content in option.description", () => {
const { createFakePi } = require("./fake_pi_runtime.js");
const { pi, tools } = createFakePi();
require(path.join(__dirname, "..", "..", "tht-gate.js")).default(pi);
const optionProperties = tools.get("reviewer_decide")
.def.parameters.properties.options.items.properties;
assert.ok(optionProperties.description);
});
test("F2 preseleziona solo le memory raccomandate e parla di applicazione", () => {
assert.deepEqual(
memorySelectionWidgetProps([
{ id: "recommended", label: "Memory A", recommended: true },
{ id: "optional", label: "Memory B" },
]),
{
title: "Seleziona le memory da applicare alla domanda",
selected: ["recommended"],
selectionLabel: "memory da applicare",
confirmLabel: "Applica le memory selezionate",
const MEMORY_OPTIONS = [
{
id: "first",
label: "Regola paziente attivo",
description: "Decisione concept_clarified: paziente attivo\nflag_attivo = TRUE",
decision: {
type: "concept_clarified",
subject: "paziente attivo",
detail: "flag_attivo = TRUE",
rationale: "Riusa mem-0042 per la stessa definizione",
},
);
});
recommended: true,
},
{
id: "duplicate",
label: "La stessa regola",
description: "Decisione concept_clarified: paziente attivo\nflag_attivo = TRUE",
decision: {
type: "concept_clarified",
subject: "paziente attivo",
detail: "flag_attivo = TRUE",
rationale: "Memory mem-0042",
},
},
{
id: "mem-0043",
label: "Tabella promossa",
description: "fact_pazienti",
decision: {
type: "table_promoted",
subject: "fact_pazienti",
detail: "tabella principale",
},
},
];
test("F2 preserves the exact memory content and proposes each memory id once", () => {
const options = normalizeMemoryOptions([
function setupRecall(choices) {
const { pi, tools, ctx } = createFakePi();
const calls = [];
let descriptor;
ctx.ui.input = async (title) => {
descriptor = JSON.parse(title);
calls.push(["review", descriptor.options.map((option) => option.id)]);
return JSON.stringify({ id: descriptor.id, choices });
};
const memoryGate = createMemoryGate({
workflow: {
activate: () => calls.push(["activate"]),
phase: () => ({ number: 8, id: "F8" }),
advance: () => {
calls.push(["advance"]);
return { advanced: choices.length === 0 };
},
close: () => null,
},
memory: {
execute: () => "[]",
mutate: () => null,
},
ledger: {
validate: () => null,
record: (_ctx, session, decision, recovery) => {
calls.push(["ledger", session, decision, recovery]);
return null;
},
},
waitForReviewer: async (runtimeContext, widget) => {
const response = await runtimeContext.ui.input(JSON.stringify(widget), "");
return JSON.parse(response);
},
toTextResult: (text) => ({ content: [{ type: "text", text }] }),
});
memoryGate.install(pi);
return { ctx, tools, calls, memoryGate, descriptor: () => descriptor };
}
test("the Memory facade normalizes search hits and applies only the selected F2 memory", async () => {
const { ctx, calls, memoryGate, descriptor } = setupRecall(["first"]);
const result = await memoryGate.reviewRecall(ctx, {
session: "s1",
title: "Memorie candidate",
options: MEMORY_OPTIONS,
allow_empty: true,
advance: true,
}, "F2");
assert.deepEqual(descriptor().options, [
{
id: "first",
label: "Regola paziente attivo",
description: "Decisione concept_clarified: paziente attivo\nflag_attivo = TRUE",
decision: {
type: "concept_clarified",
subject: "paziente attivo",
detail: "flag_attivo = TRUE",
rationale: "Riusa mem-0042 per la stessa definizione",
},
recommended: true,
},
{
id: "duplicate",
label: "La stessa regola",
description: "Decisione concept_clarified: paziente attivo\nflag_attivo = TRUE",
decision: {
type: "concept_clarified",
subject: "paziente attivo",
detail: "flag_attivo = TRUE",
rationale: "Memory mem-0042",
},
detail: "Decisione concept_clarified: paziente attivo\nflag_attivo = TRUE",
rationale: "Riusa mem-0042 per la stessa definizione",
meta: { memory_id: "mem-0042" },
selected: true,
},
]);
assert.equal(options.length, 1);
assert.equal(
options[0].detail,
"Decisione concept_clarified: paziente attivo\nflag_attivo = TRUE",
);
assert.equal(options[0].recommended, true);
});
test("F2 accepts only concept_clarified memory options", () => {
const options = normalizeMemoryOptions([
{
id: "mem-0042",
label: "Concetto paziente attivo",
description: "Definizione riusabile",
decision: {
type: "concept_clarified",
subject: "paziente attivo",
detail: "flag_attivo = TRUE",
},
},
{
id: "mem-0043",
label: "Tabella promossa",
description: "fact_pazienti",
decision: {
type: "table_promoted",
subject: "fact_pazienti",
detail: "tabella principale",
},
},
assert.deepEqual(calls, [
["review", ["first"]],
["ledger", "s1", MEMORY_OPTIONS[0].decision, ""],
["advance"],
]);
assert.match(result.content[0].text, /1 decisioni.*La fase resta aperta/s);
});
assert.deepEqual(options.map((option) => option.id), ["mem-0042"]);
test("the Memory facade accepts a deselected F2 result without a rejection", async () => {
const { ctx, calls, memoryGate } = setupRecall([]);
const result = await memoryGate.reviewRecall(ctx, {
session: "s1",
title: "Memorie candidate",
options: [MEMORY_OPTIONS[0]],
allow_empty: true,
advance: true,
}, "F2");
assert.deepEqual(calls, [["review", ["first"]], ["advance"]]);
assert.match(result.content[0].text, /Nessuna decisione registrata.*Fase avanzata/s);
});
test("the Memory facade skips an absent F2 result and advances without a widget", async () => {
const { ctx, calls, memoryGate, descriptor } = setupRecall([]);
const result = await memoryGate.reviewRecall(ctx, {
session: "s1",
title: "Memorie candidate",
options: [],
allow_empty: true,
advance: true,
}, "F2");
assert.equal(descriptor(), undefined);
assert.deepEqual(calls, [["advance"]]);
assert.deepEqual(ctx.notifications.map(({ level }) => level), ["info"]);
assert.match(result.content[0].text, /Fase memoria vuota/);
});
test("the Memory facade does not skip an absent F2 result when empty is forbidden", async () => {
const { ctx, calls, memoryGate, descriptor } = setupRecall([]);
const result = await memoryGate.reviewRecall(ctx, {
session: "s1",
title: "Memorie candidate",
options: [],
allow_empty: false,
advance: true,
}, "F2");
assert.equal(descriptor(), undefined);
assert.deepEqual(calls, []);
assert.match(result.content[0].text, /ERRORE INTERNO/);
});
test("the Memory facade keeps an absent F2 result open when advance is disabled", async () => {
const { ctx, calls, memoryGate, descriptor } = setupRecall([]);
const result = await memoryGate.reviewRecall(ctx, {
session: "s1",
title: "Memorie candidate",
options: [],
allow_empty: true,
advance: false,
}, "F2");
assert.deepEqual(descriptor().options, []);
assert.deepEqual(calls, [["review", []]]);
assert.match(result.content[0].text, /Nessuna decisione registrata/);
});