// 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 tht-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 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; } // 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, 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, recommended, options: [...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, selectionLabel, confirmLabel, }) { 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, options: opts.map((option) => ({ ...option, selected: selected.includes(option.id) })), selected: [...selected], content, reserved: RESERVED, ...(selectionLabel ? { selection_label: selectionLabel } : {}), ...(confirmLabel ? { confirm_label: confirmLabel } : {}), }; } // 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(", ")}`, ); } const ACTION_OPTIONS = { approve_reject: [ { id: "approve", label: "Salva e procedi", recommended: true }, { id: "reject", label: "Rifiuta" }, ], confirm: [{ id: "approve", label: "Salva e procedi", recommended: true }], view_only: [], }; return { type: "ui_request", id, phase, schema_version: SCHEMA_VERSION, widget: "artifact-gate", title, artifact, action, options: ACTION_OPTIONS[action.kind], 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, }; } // Build a `schema-linking` ui_request: table rows (promote/exclude) each carrying // their full catalog columns; the frontend renders rows + a per-table columns // modal (suggested pre-selected). `tables` is validated shallowly here. function buildSchemaLinkingRequest({ id, phase, title, tables }) { requireString(title, "title", "schema-linking"); const tabs = requireArray(tables, "tables", "schema-linking"); return { type: "ui_request", id, phase, schema_version: SCHEMA_VERSION, widget: "schema-linking", title, tables: [...tabs], reserved: RESERVED, }; } // Build a blocking, read-only join review. The reviewer can accept the complete // proposal or use Other to request a textual correction; individual joins are // deliberately not selectable because omitting one could create a Cartesian product. function buildJoinReviewRequest({ id, phase, title, options }) { requireString(title, "title", "join-review"); const opts = requireArray(options, "options", "join-review"); if (opts.length === 0) { throw new Error("builders: join-review requires at least one option"); } return { type: "ui_request", id, phase, schema_version: SCHEMA_VERSION, widget: "join-review", title, options: [...opts], confirm_label: "Continue", 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, buildSchemaLinkingRequest, buildJoinReviewRequest, withChildLinkage, SCHEMA_VERSION, RESERVED, };