diff --git a/harness/.pi/extensions/gate/__tests__/reserved_labels.test.js b/harness/.pi/extensions/gate/__tests__/reserved_labels.test.js new file mode 100644 index 00000000..8104943c --- /dev/null +++ b/harness/.pi/extensions/gate/__tests__/reserved_labels.test.js @@ -0,0 +1,33 @@ +const test = require("node:test"); +const assert = require("node:assert"); +const { isReserved, stripReserved } = require("../../reserved-labels.mjs"); + +test("Altro variants (any punctuation/case) are reserved", () => { + assert.equal(isReserved("Altro — specifica…"), true); + assert.equal(isReserved("Altro - specificare"), true); + assert.equal(isReserved("altro"), true); + assert.equal(isReserved("ALTRO (specificare)"), true); +}); + +test("English Other variant is reserved", () => { + assert.equal(isReserved("Other — specify"), true); + assert.equal(isReserved("other"), true); +}); + +test("canonical quit/back labels stay reserved", () => { + assert.equal(isReserved("Esci da Pi (/quit)"), true); + assert.equal(isReserved("Torna indietro (fase precedente)"), true); +}); + +test("normal merit options are NOT reserved", () => { + assert.equal(isReserved("procedura"), false); + assert.equal(isReserved("patologia"), false); + assert.equal(isReserved("altrove"), false); // starts with "altro"? no — "altrove" -> normalized "altrove" starts with "altro" -> guard below +}); + +test("stripReserved drops every Altro/other variant, keeps merit order", () => { + assert.deepEqual( + stripReserved(["procedura", "Altro - specificare", "patologia", "other"]), + ["procedura", "patologia"], + ); +}); diff --git a/harness/.pi/extensions/reserved-labels.mjs b/harness/.pi/extensions/reserved-labels.mjs index bbb97070..ad163669 100644 --- a/harness/.pi/extensions/reserved-labels.mjs +++ b/harness/.pi/extensions/reserved-labels.mjs @@ -10,15 +10,34 @@ export const QUIT_LABEL = "Esci da Pi (/quit)"; export const BACK_LABEL = "Torna indietro (fase precedente)"; export const CONTROL_LABELS = new Set([QUIT_LABEL, BACK_LABEL]); -const RESERVED = new Set([ALTRO, QUIT_LABEL, BACK_LABEL]); - -// true se l'etichetta e' una di quelle che il gate aggiunge da se'. -export function isReserved(label) { - return RESERVED.has(label); +// Normalize a label to lowercase ASCII tokens: strip diacritics, turn every run +// of punctuation/space into a single space, trim. "Altro — specifica…" -> "altro specifica". +function normalize(label) { + return String(label) + .normalize("NFD") + .replace(/[̀-ͯ]/g, "") + .toLowerCase() + .replace(/[^a-z0-9]+/g, " ") + .trim(); } -// rimuove da una lista di etichette ogni voce riservata (Altro + controlli), -// preservando ordine e voci normali. Idempotente. +const NORM_QUIT = normalize(QUIT_LABEL); +const NORM_BACK = normalize(BACK_LABEL); + +// true if the label is one the gate adds itself. Robust to the variants models +// emit ("Altro - specificare", "altro", English "Other — specify"): reserved when +// the FIRST normalized token is exactly "altro"/"other", or the whole normalized +// label equals the canonical quit/back labels. First-token match keeps real +// options like "altrove" out of the reserved set. +export function isReserved(label) { + const n = normalize(label); + if (!n) return false; + const first = n.split(" ")[0]; + return first === "altro" || first === "other" || n === NORM_QUIT || n === NORM_BACK; +} + +// removes every reserved entry from a list of labels, preserving order and normal +// entries. Idempotent. export function stripReserved(labels) { return labels.filter((label) => !isReserved(label)); }