Files
ThothII/harness/.pi/extensions/gate/builders.js
T
marcopan 7971d73628 feat(harness): pure widget-builder functions + JS golden/fuzzy tests (D2, L1)
The gate's widget-descriptor CONSTRUCTION, extracted into pure testable functions.
Each builder turns plain params into a ui_request descriptor (spec §4.1 taxonomy):
buildSelectRequest, buildMultiselectRequest, buildArtifactGate, buildInfoRequest,
buildFreetextRequest, withChildLinkage. No Pi context, no I/O -- the part of the
gate fully testable in L1 (in JS, in-language, no Python mirror).

Validation in the builders (not just happy-path): select requires title + array
options; multiselect allow_empty:false with zero options throws (a broken widget);
artifact-gate requires an artifact with a kind + a valid action.kind
(confirm/approve_reject/view_only); info level must be info/warning/error. The
Altro escape hatch with freetext linkage is always injected on blocking pick
widgets (no-limbo invariant).

L1: 14 node:test cases -- 3 golden files (select_F1, multiselect_F4,
artifact_gate_F5) pin the exact descriptor shape; fuzzy tests assert bad params
throw clearly rather than silently producing a broken widget.

package.json wires 'npm test' -> node --test (runs alongside pytest). The gate
GLUE (emission, anti-bypass, no-limbo loop) is C2, verified at L2.
2026-06-26 23:07:56 +02:00

178 lines
5.4 KiB
JavaScript

// Pure widget-descriptor builders (spec D2/D4, §4.1).
//
// Each function turns plain params into a ui_request descriptor object. No Pi
// context, no I/O -- this is the part of the gate that is fully testable in L1
// (in JS, in-language, no Python mirror). The glue (emission via ctx.sendRaw,
// anti-bypass, the no-limbo loop) is in nsp-gate.js and is verified at L2.
//
// The 6 widget kinds: info, select, multiselect, freetext, artifact-gate, artifact.
// `widget` is an open field (a new kind needs a renderer, not infra changes);
// these are the initial 6. reserved = framework escape hatches always present on
// blocking widgets: back / exit / other.
"use strict";
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"]);
function requireString(value, name, ctx) {
if (typeof value !== "string" || value.length === 0) {
throw new Error(`builders: ${ctx} requires a non-empty ${name}`);
}
return value;
}
function requireArray(value, name, ctx) {
if (!Array.isArray(value)) {
throw new Error(`builders: ${ctx} requires ${name} to be an array`);
}
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)".
function buildSelectRequest({
id, phase, title, options, intro = null, allowOther = true, recommended = null,
}) {
requireString(title, "title", "select");
const opts = requireArray(options, "options", "select");
const out = {
type: "ui_request",
id,
phase,
schema_version: SCHEMA_VERSION,
widget: "select",
title,
intro,
allow_other: allowOther,
recommended,
options: allowOther ? [...opts, altroOption()] : [...opts],
reserved: RESERVED,
};
return out;
}
// Build a `multiselect` ui_request (multi-pick). `content` is the scrollable
// 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,
}) {
requireString(title, "title", "multiselect");
const opts = requireArray(options, "options", "multiselect");
if (!allowEmpty && opts.length === 0) {
throw new Error(
"builders: multiselect with allow_empty:false requires at least one option",
);
}
return {
type: "ui_request",
id,
phase,
schema_version: SCHEMA_VERSION,
widget: "multiselect",
title,
allow_empty: allowEmpty,
allow_other: allowOther,
options: [...opts],
selected: [...selected],
content,
reserved: RESERVED,
};
}
// Build an `artifact-gate` ui_request: a rendered artifact + a disposition list
// (confirm / approve_reject / view_only). The decision is load-bearing on the
// document, so the artifact is never shown alone (spec §4.1).
function buildArtifactGate({ id, phase, title, artifact, action }) {
requireString(title, "title", "artifact-gate");
if (!artifact || typeof artifact !== "object") {
throw new Error("builders: artifact-gate requires an artifact object");
}
if (!artifact.kind) {
throw new Error("builders: artifact-gate artifact requires a kind");
}
if (!action || !action.kind || !VALID_ACTION_KINDS.has(action.kind)) {
throw new Error(
`builders: artifact-gate action.kind must be one of ${[...VALID_ACTION_KINDS].join(", ")}`,
);
}
return {
type: "ui_request",
id,
phase,
schema_version: SCHEMA_VERSION,
widget: "artifact-gate",
title,
artifact,
action,
reserved: RESERVED,
};
}
// Build an `info` ui_request: fire-and-forget notification (no response expected,
// no reserved hatches, no correlated id).
function buildInfoRequest({ phase, level, text }) {
if (!level || !VALID_INFO_LEVELS.has(level)) {
throw new Error(
`builders: info level must be one of ${[...VALID_INFO_LEVELS].join(", ")}`,
);
}
requireString(text, "text", "info");
return {
type: "ui_request",
phase,
schema_version: SCHEMA_VERSION,
widget: "info",
level,
text,
};
}
// Build a `freetext` ui_request: free-text input, always a child of
// select/artifact-gate (via Altro/Rifiuta) or via the ambient steering channel.
function buildFreetextRequest({ id, phase, title }) {
requireString(title, "title", "freetext");
return {
type: "ui_request",
id,
phase,
schema_version: SCHEMA_VERSION,
widget: "freetext",
title,
reserved: RESERVED,
};
}
// Attach a child-widget spec to an option (linkage, §4.2). Returns the option.
function withChildLinkage(option, widgetSpec) {
option.opens = widgetSpec;
return option;
}
module.exports = {
buildSelectRequest,
buildMultiselectRequest,
buildArtifactGate,
buildInfoRequest,
buildFreetextRequest,
withChildLinkage,
SCHEMA_VERSION,
RESERVED,
};