refactor(memory): extract F8 promotion gate (#22)

This commit is contained in:
2026-08-24 00:51:53 +02:00
parent d15bb59c3d
commit beac2e80f4
3 changed files with 379 additions and 217 deletions
@@ -1,67 +1,156 @@
const test = require("node:test");
const assert = require("node:assert");
const {
dedupePromotionCandidates,
promotionOptions,
promotionContent,
splitPromotionChoices,
} = require("../../tht-gate.js");
const { installMemoryGate } = require("../memory/index.js");
const { createFakePi } = require("./fake_pi_runtime.js");
// F8 memory-promotion gate: the candidates are DETERMINISTIC (computed by
// `tht memory promote --preview --json`), the model only names the session.
// These tests pin the pure candidate->widget mapping and the choice partition.
const CANDIDATES = [
{ decision_seq: 3, type: "table_promoted", subject: "fact_seeablazione",
detail: "tabella principale ablazioni", rationale: "scelta dal reviewer",
question_context: "quante ablazioni nel 2023", tables: ["fact_seeablazione"], concepts: [] },
{ decision_seq: 5, type: "concept_clarified", subject: "paziente attivo",
detail: "flag_attivo = TRUE", rationale: "",
question_context: "quante ablazioni nel 2023", tables: [], concepts: ["paziente attivo"] },
];
const TABLE_CANDIDATE = {
decision_seq: 3,
type: "table_promoted",
subject: "fact_seeablazione",
detail: "tabella principale ablazioni",
rationale: "scelta dal reviewer",
question_context: "quante ablazioni nel 2023",
tables: ["fact_seeablazione"],
concepts: [],
};
test("promotionOptions maps candidates to seq-keyed options", () => {
assert.deepEqual(promotionOptions(dedupePromotionCandidates(CANDIDATES)), [
const MEMORY_CANDIDATE = {
decision_seq: 5,
type: "concept_clarified",
subject: "paziente attivo",
detail: "flag_attivo = TRUE",
rationale: "scelta dal reviewer",
question_context: "quante ablazioni nel 2023",
tables: [],
concepts: ["paziente attivo"],
};
test("the public Memory facade owns F8 policy and mutation ordering", async () => {
const { pi, tools, ctx } = createFakePi();
const calls = [];
let descriptor;
const duplicateMemory = { ...MEMORY_CANDIDATE, decision_seq: 6 };
const declinedMemory = {
...MEMORY_CANDIDATE,
decision_seq: 7,
subject: "ricovero indice",
detail: "first_event",
};
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: ["seq-5"] });
};
installMemoryGate(pi, {
workflow: {
activate: () => calls.push(["activate"]),
phase: () => ({ number: 8, id: "F8" }),
close: (_ctx, _session, phaseNumber, summary) => {
calls.push(["close", phaseNumber, summary]);
return null;
},
},
memory: {
execute: (_ctx, args) => {
calls.push(["memory-execute", args]);
return JSON.stringify([
TABLE_CANDIDATE,
MEMORY_CANDIDATE,
duplicateMemory,
declinedMemory,
]);
},
mutate: (_ctx, args, recovery) => {
calls.push(["memory-mutate", args, recovery]);
return null;
},
},
ledger: {
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 }] }),
});
const result = await tools.get("reviewer_memory_promote").def.execute(
"promote-via-facade",
{ session: "s1" },
null,
null,
ctx,
);
assert.deepEqual(descriptor.options, [
{
id: "seq-5",
label: "concept_clarified: paziente attivo",
detail: "flag_attivo = TRUE",
rationale: "",
rationale: "scelta dal reviewer",
meta: { question_context: "quante ablazioni nel 2023" },
selected: true,
},
{
id: "seq-7",
label: "concept_clarified: ricovero indice",
detail: "first_event",
rationale: "scelta dal reviewer",
meta: { question_context: "quante ablazioni nel 2023" },
selected: true,
},
]);
});
test("F8 proposes semantically identical memory content only once", () => {
const duplicate = { ...CANDIDATES[0], decision_seq: 9 };
assert.deepEqual(
dedupePromotionCandidates([CANDIDATES[0], duplicate, CANDIDATES[1]])
.map((candidate) => candidate.decision_seq),
[5],
);
});
test("F8 never proposes promoted tables as memory", () => {
const candidates = dedupePromotionCandidates(CANDIDATES);
const options = promotionOptions(candidates);
const c = promotionContent(candidates);
assert.deepEqual(candidates.map((candidate) => candidate.type), ["concept_clarified"]);
assert.deepEqual(options.map((option) => option.id), ["seq-5"]);
assert.ok(!c.includes("fact_seeablazione"));
assert.ok(c.includes("flag_attivo = TRUE"));
assert.ok(c.includes("quante ablazioni nel 2023"));
});
test("splitPromotionChoices partitions by selection", () => {
const candidates = dedupePromotionCandidates(CANDIDATES);
const { promote, decline } = splitPromotionChoices(candidates, ["seq-5"]);
assert.deepEqual(promote.map((c) => c.decision_seq), [5]);
assert.deepEqual(decline.map((c) => c.decision_seq), []);
});
test("empty or missing choices declines everything", () => {
const candidates = dedupePromotionCandidates(CANDIDATES);
assert.equal(splitPromotionChoices(candidates, []).decline.length, 1);
assert.equal(splitPromotionChoices(candidates, undefined).decline.length, 1);
assert.deepEqual(descriptor.selected, ["seq-5", "seq-7"]);
assert.doesNotMatch(descriptor.content, /fact_seeablazione/);
assert.match(descriptor.content, /flag_attivo = TRUE/);
assert.deepEqual(calls.slice(0, 7), [
["activate"],
[
"memory-execute",
["promote", "--session", "s1", "--preview", "--json"],
],
["review", ["seq-5", "seq-7"]],
[
"memory-mutate",
["save-one", "--session", "s1", "--decision", "5", "--json"],
"Recupero manuale (umano): tht memory save-one --session s1 " +
"--decision 5. Finora salvate: 0.",
],
[
"ledger",
"s1",
{
type: "memory_promoted",
subject: "paziente attivo",
detail: "seq:5",
rationale: "scelta dal reviewer",
},
"Memoria salvata nel vectordb ma decisione memory_promoted NON registrata: " +
"recupero manuale (umano) con tht decision add --session s1 " +
"--type memory_promoted --subject \"paziente attivo\" --detail seq:5.",
],
[
"ledger",
"s1",
{
type: "memory_promotion_declined",
subject: "ricovero indice",
detail: "seq:7",
},
"",
],
[
"close",
8,
"Promozione registrata: 1 memorie salvate nel vectordb, 1 candidati scartati.",
],
]);
assert.match(result.content[0].text, /1 memorie salvate.*1 candidati scartati/);
});