Add full shell, replaceable Omics adapter and bilingual interaction

Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
This commit is contained in:
Codex
2026-09-13 14:26:39 +02:00
parent 2d1b714ebe
commit d8a29bfbdd
207 changed files with 8570 additions and 2164 deletions
+14 -7
View File
@@ -14,6 +14,7 @@
const SCHEMA_VERSION = 1;
const RESERVED = ["back", "exit", "other"];
const { gateText } = require("./locale.js");
const VALID_ACTION_KINDS = new Set(["confirm", "approve_reject", "view_only"]);
const VALID_INFO_LEVELS = new Set(["info", "warning", "error"]);
@@ -38,6 +39,7 @@ function requireArray(value, name, ctx) {
// highlight as "(consigliato)".
function buildSelectRequest({
id, phase, title, options, intro = null, recommended = null,
titleI18n,
}) {
requireString(title, "title", "select");
const opts = requireArray(options, "options", "select");
@@ -48,6 +50,7 @@ function buildSelectRequest({
schema_version: SCHEMA_VERSION,
widget: "select",
title,
...(titleI18n ? { title_i18n: titleI18n } : {}),
intro,
recommended,
options: [...opts],
@@ -61,7 +64,7 @@ function buildSelectRequest({
// `allowEmpty:false` with zero options is a broken widget and throws.
function buildMultiselectRequest({
id, phase, title, options, content = null, selected = [], allowEmpty = false,
selectionLabel, confirmLabel,
selectionLabel, confirmLabel, titleI18n, selectionLabelI18n, confirmLabelI18n,
}) {
requireString(title, "title", "multiselect");
const opts = requireArray(options, "options", "multiselect");
@@ -77,6 +80,7 @@ function buildMultiselectRequest({
schema_version: SCHEMA_VERSION,
widget: "multiselect",
title,
...(titleI18n ? { title_i18n: titleI18n } : {}),
allow_empty: allowEmpty,
options: opts.map((option) => ({ ...option, selected: selected.includes(option.id) })),
selected: [...selected],
@@ -84,13 +88,15 @@ function buildMultiselectRequest({
reserved: RESERVED,
...(selectionLabel ? { selection_label: selectionLabel } : {}),
...(confirmLabel ? { confirm_label: confirmLabel } : {}),
...(selectionLabelI18n ? { selection_label_i18n: selectionLabelI18n } : {}),
...(confirmLabelI18n ? { confirm_label_i18n: confirmLabelI18n } : {}),
};
}
// 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 }) {
function buildArtifactGate({ id, phase, title, artifact, action, locale = "en" }) {
requireString(title, "title", "artifact-gate");
if (!artifact || typeof artifact !== "object") {
throw new Error("builders: artifact-gate requires an artifact object");
@@ -105,10 +111,10 @@ function buildArtifactGate({ id, phase, title, artifact, action }) {
}
const ACTION_OPTIONS = {
approve_reject: [
{ id: "approve", label: "Salva e procedi", recommended: true },
{ id: "reject", label: "Rifiuta" },
{ id: "approve", label: gateText(locale, "save"), label_i18n: gateText("en", "save"), recommended: true },
{ id: "reject", label: gateText(locale, "reject"), label_i18n: gateText("en", "reject") },
],
confirm: [{ id: "approve", label: "Salva e procedi", recommended: true }],
confirm: [{ id: "approve", label: gateText(locale, "save"), label_i18n: gateText("en", "save"), recommended: true }],
view_only: [],
};
return {
@@ -180,7 +186,7 @@ function buildSchemaLinkingRequest({ id, phase, title, tables }) {
// 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 }) {
function buildJoinReviewRequest({ id, phase, title, options, locale = "en" }) {
requireString(title, "title", "join-review");
const opts = requireArray(options, "options", "join-review");
if (opts.length === 0) {
@@ -194,7 +200,8 @@ function buildJoinReviewRequest({ id, phase, title, options }) {
widget: "join-review",
title,
options: [...opts],
confirm_label: "Continue",
confirm_label: gateText(locale, "continue"),
confirm_label_i18n: gateText("en", "continue"),
reserved: RESERVED,
};
}
+4 -2
View File
@@ -156,12 +156,14 @@ function enrichCteResultColumns(payload, planTables, getColumns) {
// comes from persisted state, not model prose). `decisions` is session-show's ledger
// dump; `emits` is the phase's substantive decision-type list from workflow.yaml.
// No matching decisions -> data returned unchanged. Pure: returns a NEW object.
function appendLedgerSection(data, decisions, emits) {
function appendLedgerSection(data, decisions, emits, locale = "en") {
const { gateText } = require("./locale.js");
const emitSet = new Set(emits || []);
const rows = (decisions || []).filter((d) => d && emitSet.has(d.type));
if (rows.length === 0) return data;
const section = {
title: "Decisioni registrate in questa fase (dal ledger)",
title: gateText(locale, "ledgerTitle"),
title_i18n: gateText("en", "ledgerTitle"),
items: rows.map((d) => ({
label: `${d.type}: ${d.subject}`,
value: d.detail || "",
@@ -0,0 +1,41 @@
// Only deterministic chrome is translated. Model prose and domain payloads are opaque.
const en = {
save: "Save and proceed", reject: "Reject", continue: "Continue",
approvalPrompt: "Approve or reject?", chooseApproval: "Choose Save and proceed or Reject.",
datamartTitle: "Generate a datamart?", datamartYes: "Yes, generate the datamart",
datamartNo: "No, skip the datamart",
memoryTitle: "Select memories to apply to the question", memorySelection: "memories to apply",
memoryApply: "Apply selected memories", memoryEmpty: "No reusable memories for this question; continuing to the next phase.",
memorySummary: "Memory for future questions", archiveRepair: "Resolve archive conflict",
repairError: "The correction could not complete. Review the current archive state before retrying.",
ledgerTitle: "Decisions recorded in this phase (from the ledger)",
escapeHelp: "Esc does not close the gate: use Back / Exit / Other.",
inputHelp: "Answer through the gate widgets. To send free text to the model, start the line with '!'.",
memoryIndexWarning: "Memory saved. Index update is incomplete; an administrator can retry it in Memory management.",
backUsage: "Usage: /torna <session_id> [N]",
backInvalid: "Invalid target (current phase {phase}).",
decision: "Decision",
};
const it = {
save: "Salva e procedi", reject: "Rifiuta", continue: "Continua",
approvalPrompt: "Approvi o rifiuti?", chooseApproval: "Scegli «Salva e procedi» o «Rifiuta».",
datamartTitle: "Vuoi generare un datamart?", datamartYes: "Sì, genera il datamart",
datamartNo: "No, salta il datamart",
memoryTitle: "Seleziona le memory da applicare alla domanda", memorySelection: "memory da applicare",
memoryApply: "Applica le memory selezionate", memoryEmpty: "Nessuna memory riutilizzabile per questa domanda — passo alla fase successiva.",
memorySummary: "Memory per le domande future", archiveRepair: "Risolvi il conflitto negli archivi",
repairError: "La correzione non è stata completata. Verifica lo stato attuale degli archivi prima di riprovare.",
ledgerTitle: "Decisioni registrate in questa fase (dal ledger)",
escapeHelp: "Esc non chiude il gate: usa Torna indietro / Esci / Altro dalle opzioni.",
inputHelp: "Durante la sessione rispondi con i widget del gate. Per inviare testo libero al modello inizia la riga con '!'.",
memoryIndexWarning: "Memory salvata. L'aggiornamento dell'indice è incompleto; un amministratore può riprovarlo da Gestione Memory.",
backUsage: "Uso: /torna <session_id> [N]",
backInvalid: "Target non valido (fase corrente {phase}).",
decision: "Decisione",
};
const catalogs = { en, it };
function gateText(locale, key) {
const language = String(locale || "en").toLowerCase().split("-")[0];
return catalogs[language]?.[key] ?? en[key];
}
module.exports = { gateText };