/* 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}); }));