220 lines
6.9 KiB
JavaScript
220 lines
6.9 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 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,
|
|
};
|