From b043293baf5f9ce85d7d394bd2aab289f91542ab Mon Sep 17 00:00:00 2001 From: mptyl Date: Thu, 2 Jul 2026 19:10:01 +0200 Subject: [PATCH] fix(gate): stop injecting the redundant Altro option on select gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildSelectRequest injected an `altroOption` ({id:"other", opens:freetext}) into the select descriptor, but SelectWidget/MultiselectWidget never route option.opens — so it rendered as an inert "Altro — specifica…" button next to the working reserved "Other — specify" control (which now owns free-text since the reviewer-gate-ux fix). Remove the injection, the now-unused altroOption()/ALTRO_LABEL, and the unused allow_other flag from both builders (no frontend consumer). Free-text stays offered on every gate via the reserved "Other" control. The ArtifactGateWidget opens/LinkageHost linkage (reject-with-reason capability) is intentionally kept — it is a separate, tested feature, not the injected duplicate. Co-Authored-By: Claude Opus 4.8 --- .../gate/__tests__/builders.test.js | 14 +++++----- .../gate/__tests__/golden/multiselect_F4.json | 1 - .../gate/__tests__/golden/select_F1.json | 4 +-- harness/.pi/extensions/gate/builders.js | 26 +++++-------------- 4 files changed, 14 insertions(+), 31 deletions(-) diff --git a/harness/.pi/extensions/gate/__tests__/builders.test.js b/harness/.pi/extensions/gate/__tests__/builders.test.js index d3884314..72c80ef3 100644 --- a/harness/.pi/extensions/gate/__tests__/builders.test.js +++ b/harness/.pi/extensions/gate/__tests__/builders.test.js @@ -33,20 +33,17 @@ test("select F1 matches golden shape", () => { assert.equal(result.type, "ui_request"); assert.equal(result.schema_version, 1); assert.deepEqual(result.reserved, g.reserved); // back/exit/other framework hatches - assert.deepEqual( - result.options.filter((o) => o.id !== "other"), - g.options.filter((o) => o.id !== "other"), - ); + assert.deepEqual(result.options, g.options); }); -test("select always carries the Altro escape hatch with freetext linkage", () => { +test("select no longer injects an Altro option (free-text is the reserved 'Other' control)", () => { const result = buildSelectRequest({ id: "u", phase: "F1", title: "x", options: [{ id: "a", label: "A" }], }); - const other = result.options.find((o) => o.id === "other"); - assert.ok(other, "Altro option must always be present"); - assert.equal(other.opens.widget, "freetext"); + assert.equal(result.options.find((o) => o.id === "other"), undefined, "no injected Altro option"); + assert.ok(!("allow_other" in result), "the unused allow_other flag is gone"); + assert.deepEqual(result.reserved, ["back", "exit", "other"], "Altro still offered via the reserved control"); }); test("select surfaces a recommended marker when given", () => { @@ -70,6 +67,7 @@ test("multiselect F4 matches golden + carries content + allowEmpty", () => { const g = golden("multiselect_F4.json"); assert.equal(result.widget, "multiselect"); assert.equal(result.allow_empty, false); + assert.ok(!("allow_other" in result), "the unused allow_other flag is gone"); assert.ok("content" in result); assert.deepEqual(result.reserved, g.reserved); }); diff --git a/harness/.pi/extensions/gate/__tests__/golden/multiselect_F4.json b/harness/.pi/extensions/gate/__tests__/golden/multiselect_F4.json index 2d3fe773..fe7f2d8e 100644 --- a/harness/.pi/extensions/gate/__tests__/golden/multiselect_F4.json +++ b/harness/.pi/extensions/gate/__tests__/golden/multiselect_F4.json @@ -6,7 +6,6 @@ "widget": "multiselect", "title": "Tabelle", "allow_empty": false, - "allow_other": true, "options": [], "selected": [], "content": { "candidates": [] }, diff --git a/harness/.pi/extensions/gate/__tests__/golden/select_F1.json b/harness/.pi/extensions/gate/__tests__/golden/select_F1.json index bf5a40c3..9284cabf 100644 --- a/harness/.pi/extensions/gate/__tests__/golden/select_F1.json +++ b/harness/.pi/extensions/gate/__tests__/golden/select_F1.json @@ -6,12 +6,10 @@ "widget": "select", "title": "Disambigua 'ablazione'", "intro": null, - "allow_other": true, "recommended": null, "options": [ { "id": "o1", "label": "procedura" }, - { "id": "o2", "label": "patologia" }, - { "id": "other", "label": "Altro — specifica…", "opens": { "widget": "freetext", "title": "Specifica…" } } + { "id": "o2", "label": "patologia" } ], "reserved": ["back", "exit", "other"] } diff --git a/harness/.pi/extensions/gate/builders.js b/harness/.pi/extensions/gate/builders.js index 3cedfa8e..36f30a84 100644 --- a/harness/.pi/extensions/gate/builders.js +++ b/harness/.pi/extensions/gate/builders.js @@ -15,7 +15,6 @@ const SCHEMA_VERSION = 1; const RESERVED = ["back", "exit", "other"]; -const ALTRO_LABEL = "Altro — specifica…"; const VALID_ACTION_KINDS = new Set(["confirm", "approve_reject", "view_only"]); const VALID_INFO_LEVELS = new Set(["info", "warning", "error"]); @@ -33,21 +32,12 @@ function requireArray(value, name, ctx) { return value; } -// An "other" option that opens a freetext child widget. Always present on -// blocking pick widgets so the reviewer is never trapped in the listed options. -function altroOption() { - return { - id: "other", - label: ALTRO_LABEL, - opens: { widget: "freetext", title: "Specifica…" }, - }; -} - -// Build a `select` ui_request (single-pick). Reserved hatches (Altro/Back/Exit) -// are injected; Altro carries the freetext linkage (§4.2). `recommended` is the -// id of the option to highlight as "(consigliato)". +// Build a `select` ui_request (single-pick). The framework escape hatches +// (Back/Exit/Other) come from `reserved` (rendered by ReservedControls, which owns +// the free-text linkage on "Other"). `recommended` is the id of the option to +// highlight as "(consigliato)". function buildSelectRequest({ - id, phase, title, options, intro = null, allowOther = true, recommended = null, + id, phase, title, options, intro = null, recommended = null, }) { requireString(title, "title", "select"); const opts = requireArray(options, "options", "select"); @@ -59,9 +49,8 @@ function buildSelectRequest({ widget: "select", title, intro, - allow_other: allowOther, recommended, - options: allowOther ? [...opts, altroOption()] : [...opts], + options: [...opts], reserved: RESERVED, }; return out; @@ -71,7 +60,7 @@ function buildSelectRequest({ // context (e.g. schema candidates); `selected` the pre-checked option ids; // `allowEmpty:false` with zero options is a broken widget and throws. function buildMultiselectRequest({ - id, phase, title, options, content = null, selected = [], allowEmpty = false, allowOther = true, + id, phase, title, options, content = null, selected = [], allowEmpty = false, }) { requireString(title, "title", "multiselect"); const opts = requireArray(options, "options", "multiselect"); @@ -88,7 +77,6 @@ function buildMultiselectRequest({ widget: "multiselect", title, allow_empty: allowEmpty, - allow_other: allowOther, options: [...opts], selected: [...selected], content,