atlas-iac/dockerfiles/hermes-webui-hux/runtime/wave_c_multimodal_onboarding_release.js

397 lines
22 KiB
JavaScript

/* node:coverage disable */
(function (root, factory) {
'use strict';
const common = typeof module === 'object' && module.exports;
const foundation = common ? require('../foundation.js') : root.HermesHuxFoundation;
const shellApi = common ? require('../shell.js') : root.HermesHuxShell;
const contract = common ? require('./wave_a_contract.js') : root.HermesHuxWaveAContract;
const api = factory(foundation, shellApi, contract);
if (common) module.exports = api;
else root.HermesHuxWaveC = api;
}(typeof globalThis === 'object' ? globalThis : this, function (foundation, shellApi, contract) {
/* node:coverage enable */
'use strict';
const CARD_SPECS = Object.freeze({
multimodal: Object.freeze({card: 'HUX-07', flag: 'hux.multimodal',
dependencies: ['hux.foundation', 'hux.projects', 'hux.artifacts', 'hux.autonomy'],
routes: ['/hux/v1/projects/{project_id}/conversations/{id}/multimodal/items',
'/hux/v1/projects/{project_id}/conversations/{id}/multimodal/items/{item_id}',
'/hux/v1/projects/{project_id}/conversations/{id}/multimodal/items/{item_id}/transcript-corrections',
'/hux/v1/projects/{project_id}/conversations/{id}/capture-intents']}),
onboarding: Object.freeze({card: 'HUX-09', flag: 'hux.onboarding',
dependencies: ['hux.foundation', 'hux.projects', 'hux.friendly_modes'],
routes: ['/hux/v1/projects/{project_id}/conversations/{id}/suggestions/evaluate',
'/hux/v1/projects/{project_id}/conversations/{id}/suggestions/{suggestion_id}/decisions',
'/hux/v1/projects/{project_id}/conversations/{id}/suggestions/states']}),
release: Object.freeze({card: 'HUX-12', flag: 'hux.release_followthrough',
dependencies: ['hux.foundation', 'hux.projects'],
routes: ['/hux/v1/projects/{project_id}/conversations/{id}/releases',
'/hux/v1/projects/{project_id}/conversations/{id}/releases/{release_id}',
'/hux/v1/projects/{project_id}/conversations/{id}/releases/{release_id}/transitions']}),
});
const WORKLOADS = new Set(['hermes-chat-router', 'hermes-chat-tenant', 'hermes-webui',
'hermes-agent', 'hermes-switchyard']);
const CONTEXTS = new Set(['first_session', 'empty_project', 'after_artifact',
'after_research', 'after_approval', 'idle']);
const ACTIONS = new Set(['open_mode', 'create_project', 'open_memory', 'open_artifacts',
'start_workflow', 'none']);
const SHA = /^sha256:[0-9a-f]{64}$/;
const COMMIT = /^[0-9a-f]{40}$/;
const FLUX = /^main@sha1:[0-9a-f]{40}$/;
const UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$/;
class WaveCContractError extends Error {
constructor(message) { super(message); this.name = 'WaveCContractError'; }
}
function record(value) {
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
}
function exact(value, required, optional) {
const allowed = new Set(required.concat(optional || []));
return Boolean(record(value) && required.every((key) => Object.hasOwn(value, key)) &&
Object.keys(value).every((key) => allowed.has(key)));
}
function isUtc(value) {
return typeof value === 'string' && UTC.test(value) && !Number.isNaN(Date.parse(value));
}
function immutable(value) {
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
Object.values(value).forEach(immutable);
return Object.freeze(value);
}
function element(doc, tag, attributes, text) {
const node = doc.createElement(tag);
Object.entries(attributes || {}).forEach(([name, value]) => node.setAttribute(name, value));
if (text !== undefined) node.textContent = String(text);
return node;
}
function sameIdentity(raw, expected) {
return contract.sameCanonicalIdentity(raw, expected);
}
function normalizeCapabilityCards(payload, expected) {
contract.normalizeCapabilities(payload, expected);
const cards = {};
Object.entries(CARD_SPECS).forEach(([name, spec]) => {
const raw = payload.cards.find((item) => item.card === spec.card);
if (!raw || raw.flag !== spec.flag || typeof raw.enabled !== 'boolean') {
throw new WaveCContractError(`${spec.card} capability is invalid`);
}
cards[name] = immutable({enabled: raw.enabled, routes: [...raw.routes]});
});
return immutable(cards);
}
function capabilityGap(client, cards, name) {
const spec = CARD_SPECS[name];
const card = cards[name];
if (!card || !card.enabled || !client.enabled(spec.flag) ||
spec.dependencies.some((flag) => !client.enabled(flag))) return 'feature flag or dependency is off';
const missing = spec.routes.filter((route) => !card.routes.includes(route));
return missing.length ? 'server route is not available' : null;
}
function gapView(doc, panel, title) {
const section = element(doc, 'section', {'class': 'hux-wave-c hux-wave-c--gap',
'role': 'status', 'aria-live': 'polite'});
section.appendChild(element(doc, 'h3', {}, title));
section.appendChild(element(doc, 'strong', {}, 'Not available from this Hermes server'));
section.appendChild(element(doc, 'p', {},
'The required capability route has not been advertised. Nothing was enabled or changed.'));
panel.replaceChildren(section);
}
function normalizeMediaPage(raw, expected, projectId, conversationId) {
if (!exact(raw, ['items', 'next']) || raw.next !== null ||
!Array.isArray(raw.items) || raw.items.length > 200) {
throw new WaveCContractError('Multimodal page crossed its scope');
}
const media = raw.items.map((item) => {
if (!exact(item, ['schema', 'id', 'owner', 'project_id', 'conversation_id', 'kind', 'source',
'filename', 'mime', 'bytes', 'hash', 'approval_id', 'status', 'created_at', 'revision'],
['lineage', 'latest_correction_id']) || item.schema !== 'hux.multimodal_item.v1' ||
!contract.isId(item.id, 'mmi') ||
item.owner !== expected.userRef || item.project_id !== projectId ||
item.conversation_id !== conversationId || !['image', 'document', 'audio', 'video'].includes(item.kind) ||
!Number.isSafeInteger(item.bytes) || item.bytes < 1 || item.status !== 'metadata_only' ||
!Number.isSafeInteger(item.revision) || item.revision < 1 || !isUtc(item.created_at)) {
throw new WaveCContractError('Multimodal item is invalid');
}
const fileName = contract.safeText(item.filename, '', 160);
if (!fileName || /[/\\]/.test(fileName)) throw new WaveCContractError('Media name is invalid');
return immutable({id: item.id, kind: item.kind, fileName, bytes: item.bytes});
});
return immutable({media});
}
function normalizeTrigger(raw, expectedSurface) {
if (!record(raw) || Object.keys(raw).length !== 2 || raw.surface !== expectedSurface ||
!CONTEXTS.has(raw.context)) throw new TypeError('An exact contextual trigger is required');
return immutable({surface: raw.surface, context: raw.context});
}
function normalizeEvaluation(raw, expected, _sessionId, _conversationId, trigger, privacyEnabled) {
if (!record(raw) || raw.stored === false) {
if (exact(raw, ['suggestion', 'reason', 'stored']) && raw.suggestion === null &&
typeof raw.reason === 'string' && raw.reason.length <= 80) return null;
throw new WaveCContractError('Suggestion evaluation is invalid');
}
if (!exact(raw, ['suggestion', 'state', 'revision', 'stored']) || raw.stored !== true ||
!Number.isSafeInteger(raw.revision) || raw.revision < 1) throw new WaveCContractError('Suggestion is invalid');
const item = raw.suggestion;
const state = raw.state;
if (!record(item) || item.schema !== 'hux.suggestion.v1' || !contract.isId(item.id, 'sug') ||
!record(item.trigger) || item.trigger.surface !== trigger.surface || item.trigger.context !== trigger.context ||
!ACTIONS.has(item.action && item.action.type) || !record(item.suppression) ||
item.suppression.dismissable !== true || item.suppression.never_again_supported !== true ||
!Number.isSafeInteger(item.suppression.max_shows) || item.suppression.max_shows < 1 ||
!record(state) || state.schema !== 'hux.suggestion_state.v1' || state.owner !== expected.userRef ||
state.suggestion_id !== item.id || !Number.isSafeInteger(state.shows) || state.shows < 1 ||
state.shows > item.suppression.max_shows || state.never_again !== false) {
throw new WaveCContractError('Suggestion is invalid or suppressed');
}
const title = contract.safeText(item.title, '', 80);
const body = contract.safeText(item.body, '', 280);
if (!title || !body || (item.action.type === 'open_memory' && !privacyEnabled)) {
throw new WaveCContractError('Suggestion is not eligible');
}
return immutable({suggestionId: item.id, title, body, action: item.action.type,
shows: state.shows, revision: raw.revision, trigger});
}
function releaseEvidence(raw) {
if (!record(raw) || raw.schema !== 'hux.release.v1' || !contract.isId(raw.id, 'rel') ||
!WORKLOADS.has(raw.workload) || !COMMIT.test(String(raw.commit)) ||
!record(raw.evidence) || !Array.isArray(raw.transitions)) return null;
if (raw.state !== 'live_verified') return immutable({id: raw.id, workload: raw.workload,
commit: raw.commit, live: false});
const evidence = raw.evidence;
const health = evidence.health_check;
const digestsMatch = SHA.test(String(evidence.image_digest)) &&
evidence.image_digest === evidence.harbor_digest && evidence.image_digest === evidence.pod_digest;
const final = raw.transitions[raw.transitions.length - 1];
if (!digestsMatch || !FLUX.test(String(evidence.flux_revision)) || !record(health) ||
health.status !== 'pass' || !isUtc(health.at) || !record(final) ||
final.from !== 'converged' || final.to !== 'live_verified' || !isUtc(final.at)) return null;
return immutable({id: raw.id, workload: raw.workload, commit: raw.commit, live: true,
verifiedAt: health.at, digest: evidence.image_digest});
}
function normalizeReleasePage(raw, _expected, projectId, conversationId) {
if (!exact(raw, ['items', 'next']) || raw.next !== null || !Array.isArray(raw.items) || raw.items.length > 100) {
throw new WaveCContractError('Release evidence crossed its scope');
}
const items = raw.items.map((view) => exact(view, ['release', 'scope', 'revision', 'ledger_hash']) &&
exact(view.scope, ['project_id', 'conversation_id']) && view.scope.project_id === projectId &&
view.scope.conversation_id === conversationId && Number.isSafeInteger(view.revision) && view.revision >= 1 &&
SHA.test(String(view.ledger_hash)) ? releaseEvidence(view.release) : null);
return immutable({items: items.filter(Boolean), withheld: items.filter((item) => item === null).length});
}
function announce(doc, panel, message, alert) {
panel.replaceChildren(element(doc, 'p', {'class': alert ? 'hux-wave-c__error' : 'hux-wave-c__status',
'role': alert ? 'alert' : 'status', 'aria-live': alert ? 'assertive' : 'polite'}, message));
}
function multimodalExtension(options) {
return Object.freeze({id: 'wave-c-multimodal', flag: 'hux.multimodal', label: 'Media', order: 70,
render(context) {
const panel = context.container;
if (capabilityGap(context.client, options.cards(), 'multimodal')) {
gapView(options.document, panel, 'Multimodal workspace'); return;
}
announce(options.document, panel, 'Loading conversation media…', false);
context.client.request(`/projects/${encodeURIComponent(options.projectId)}/conversations/` +
`${encodeURIComponent(options.conversationId)}/multimodal/items`).then((response) => {
const page = normalizeMediaPage(response.body, context.client.identity,
options.projectId, options.conversationId);
const section = element(options.document, 'section', {'class': 'hux-wave-c',
'aria-labelledby': 'hux-wave-c-media-title'});
section.appendChild(element(options.document, 'h3', {'id': 'hux-wave-c-media-title'},
'Multimodal workspace'));
section.appendChild(element(options.document, 'p', {},
`${page.media.length} scoped metadata items; binary content remains outside HUX.`));
const list = element(options.document, 'ul', {'class': 'hux-wave-c__list'});
page.media.forEach((item) => list.appendChild(element(options.document, 'li', {},
`${item.kind}: ${item.fileName} · ${Math.ceil(item.bytes / 1024)} KB`)));
section.appendChild(list);
section.appendChild(element(options.document, 'p', {'class': 'hux-wave-c__note'},
'Capture and upload remain approval-gated; this view never receives media bytes.'));
panel.replaceChildren(section);
}).catch(() => announce(options.document, panel,
'Conversation media could not be verified. Nothing was shown.', true));
}});
}
function actionLabel(action) {
return {open_mode: 'Choose a mode', create_project: 'Add to a project',
open_memory: 'Review memory', open_artifacts: 'View artifacts',
start_workflow: 'Use suggestion'}[action] || null;
}
function onboardingExtension(options) {
let panel = null;
let client = null;
let active = null;
const decided = new Set();
async function decide(choice) {
const evaluation = active;
if (!evaluation || decided.has(evaluation.suggestionId)) return;
if (choice === 'acted') {
if (!actionLabel(evaluation.action) || typeof options.onSuggestionAction !== 'function') {
announce(options.document, panel, 'This optional action is not connected. Nothing changed.', true); return;
}
await options.onSuggestionAction(Object.freeze({type: evaluation.action,
conversationId: options.conversationId}));
}
const key = contract.safeIdempotencyKey(options.idempotencyKey());
const decision = choice === 'dismiss' ? 'dismissed' : choice;
const path = `/projects/${encodeURIComponent(options.projectId)}/conversations/` +
`${encodeURIComponent(options.conversationId)}/suggestions/` +
`${encodeURIComponent(evaluation.suggestionId)}/decisions`;
const response = await client.request(path, {method: 'POST', headers: {
'Idempotency-Key': key, 'If-Match': String(evaluation.revision)},
body: {decision, clicked: true}});
const raw = response.body;
if (!exact(raw, ['state', 'revision', 'decision']) || raw.decision !== decision ||
!Number.isSafeInteger(raw.revision) || raw.revision <= evaluation.revision ||
!record(raw.state) || raw.state.owner !== client.identity.userRef ||
raw.state.suggestion_id !== evaluation.suggestionId) throw new WaveCContractError('Decision crossed its scope');
decided.add(evaluation.suggestionId); active = null;
announce(options.document, panel, choice === 'acted' ? 'Suggestion completed.' : 'Suggestion dismissed.', false);
}
function show(evaluation) {
active = evaluation;
const aside = element(options.document, 'aside', {'class': 'hux-wave-c hux-wave-c--suggestion',
'aria-labelledby': `hux-wave-c-${evaluation.suggestionId}`});
aside.appendChild(element(options.document, 'span', {'class': 'hux-wave-c__kicker'}, 'Optional next step'));
aside.appendChild(element(options.document, 'h3', {'id': `hux-wave-c-${evaluation.suggestionId}`}, evaluation.title));
aside.appendChild(element(options.document, 'p', {}, evaluation.body));
const controls = element(options.document, 'div', {'class': 'hux-wave-c__actions'});
const label = actionLabel(evaluation.action);
[['acted', label], ['dismiss', 'Not now'], ['never_again', "Don't suggest this again"]]
.filter((entry) => entry[1]).forEach(([choice, text]) => {
const button = element(options.document, 'button', {'type': 'button'}, text);
button.addEventListener('click', () => decide(choice).catch(() =>
announce(options.document, panel, 'That choice could not be saved. Nothing else changed.', true)));
controls.appendChild(button);
});
aside.appendChild(controls); panel.replaceChildren(aside);
}
async function present(rawTrigger) {
if (!panel || !client) return false;
const trigger = normalizeTrigger(rawTrigger, client.identity.surface);
if (capabilityGap(client, options.cards(), 'onboarding')) {
gapView(options.document, panel, 'Contextual suggestions'); return false;
}
announce(options.document, panel, 'Checking for an optional next step…', false);
const response = await client.request(`/projects/${encodeURIComponent(options.projectId)}/conversations/` +
`${encodeURIComponent(options.conversationId)}/suggestions/evaluate`, {method: 'POST',
headers: {'Idempotency-Key': contract.safeIdempotencyKey(options.idempotencyKey())},
body: {context: trigger.context, no_store: false}});
const evaluation = normalizeEvaluation(response.body, client.identity, options.sessionId,
options.conversationId, trigger, client.enabled('hux.privacy'));
if (!evaluation || decided.has(evaluation.suggestionId)) return false;
show(evaluation); return true;
}
return Object.freeze({present, extension: Object.freeze({id: 'wave-c-onboarding',
flag: 'hux.onboarding', label: 'Suggestions', order: 80, render(context) {
panel = context.container; client = context.client;
if (capabilityGap(client, options.cards(), 'onboarding')) gapView(options.document, panel, 'Contextual suggestions');
else announce(options.document, panel,
'Suggestions appear only after an exact app event. Nothing is shown automatically.', false);
}})});
}
function releaseExtension(options) {
return Object.freeze({id: 'wave-c-release', flag: 'hux.release_followthrough',
label: 'Release trust', order: 90, render(context) {
const panel = context.container;
if (capabilityGap(context.client, options.cards(), 'release')) {
gapView(options.document, panel, 'Release evidence'); return;
}
announce(options.document, panel, 'Verifying live release evidence…', false);
context.client.request(`/projects/${encodeURIComponent(options.projectId)}/conversations/` +
`${encodeURIComponent(options.conversationId)}/releases`).then((response) => {
const page = normalizeReleasePage(response.body, context.client.identity,
options.projectId, options.conversationId);
const section = element(options.document, 'section', {'class': 'hux-wave-c',
'aria-labelledby': 'hux-wave-c-release-title'});
section.appendChild(element(options.document, 'h3', {'id': 'hux-wave-c-release-title'}, 'Release evidence'));
const list = element(options.document, 'ul', {'class': 'hux-wave-c__releases'});
page.items.forEach((item) => {
const row = element(options.document, 'li', {'data-live-verified': item.live ? 'true' : 'false'});
row.appendChild(element(options.document, 'strong', {}, item.workload));
row.appendChild(element(options.document, 'span', {}, item.live ?
`Live verified ${item.verifiedAt}` : 'Release evidence is not live verified.'));
row.appendChild(element(options.document, 'code', {}, item.commit.slice(0, 12)));
list.appendChild(row);
});
section.appendChild(list);
if (page.withheld) section.appendChild(element(options.document, 'p', {'role': 'status'},
`${page.withheld} release record was withheld because its evidence was invalid.`));
panel.replaceChildren(section);
}).catch(() => announce(options.document, panel,
'Live release evidence could not be verified. No deployment claim was shown.', true));
}});
}
function defaultKeyFactory() {
let sequence = 0;
return () => `webui:suggestion:${Date.now().toString(36)}:${(++sequence).toString(36)}`;
}
function createWaveCRuntime(options) {
const settings = options || {};
const doc = settings.document || (typeof document === 'object' ? document : null);
const fetcher = settings.fetcher || (typeof fetch === 'function' ? fetch.bind(globalThis) : null);
if (!doc || !fetcher || !contract.isId(settings.projectId, 'prj') ||
!contract.isId(settings.conversationId, 'conv') || !contract.isId(settings.sessionId, 'ses')) {
throw new TypeError('Wave C runtime requires a document, fetcher, and opaque scope ids');
}
let cards = immutable({});
const base = String(settings.baseUrl || '/hux/v1').replace(/\/$/, '');
const capabilityUrl = contract.safeEndpoint(base, '/capabilities');
const captureFetcher = async (url, init) => {
const response = await fetcher(url, init);
if (url !== capabilityUrl || !response.ok) return response;
const body = await response.json();
cards = normalizeCapabilityCards(body, contract.normalizeExpectedIdentity(settings.expectedIdentity));
return {status: response.status, ok: response.ok, headers: response.headers, json: async () => body};
};
const client = contract.createCanonicalClient({...settings, fetcher: captureFetcher});
const shell = shellApi.createShell({client, document: doc, instanceId: settings.instanceId || 'wave-c'});
const shared = {document: doc, projectId: settings.projectId, conversationId: settings.conversationId,
sessionId: settings.sessionId, cards: () => cards,
idempotencyKey: settings.idempotencyKey || defaultKeyFactory(),
onSuggestionAction: settings.onSuggestionAction};
const onboarding = onboardingExtension(shared);
const removers = [shell.register(multimodalExtension(shared)),
shell.register(onboarding.extension), shell.register(releaseExtension(shared))];
let mounted = false;
return Object.freeze({client, presentSuggestion: onboarding.present,
async mount(target) {
if (mounted) throw new Error('Wave C runtime is already mounted');
mounted = true; shell.mount(target); await client.negotiate();
},
destroy() { removers.forEach((remove) => remove()); shell.destroy(); mounted = false; },
});
}
return Object.freeze({CARD_SPECS, WaveCContractError, actionLabel, capabilityGap,
createWaveCRuntime, multimodalExtension, normalizeCapabilityCards, normalizeEvaluation,
normalizeMediaPage, normalizeReleasePage, normalizeTrigger, onboardingExtension,
releaseEvidence, releaseExtension});
}));