/* node:coverage disable */ (function (root, factory) { 'use strict'; const waveA = typeof module === 'object' && module.exports ? require('./wave_a_contract.js') : root.HermesHuxWaveAContract; const api = factory(waveA); if (typeof module === 'object' && module.exports) module.exports = api; else root.HermesHuxWaveBContract = api; }(typeof globalThis === 'object' ? globalThis : this, function (waveA) { /* node:coverage enable */ 'use strict'; const UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$/; const MESSAGE = /^[A-Za-z0-9._:-]{1,120}$/; const SHA = /^sha256:[0-9a-f]{64}$/; const TAG = /^[a-z0-9][a-z0-9-]{0,39}$/; const TYPES = new Set(['markdown', 'code', 'html', 'svg', 'image', 'json', 'csv', 'document', 'audio']); const TEXT_TYPES = new Set(['markdown', 'code', 'html', 'svg', 'json', 'csv']); const MODES = Object.freeze([ Object.freeze({id: 'fast', label: 'Fast', description: 'Prioritize a quick, direct response.'}), Object.freeze({id: 'thoughtful', label: 'Thoughtful', description: 'Allow more analysis before answering.'}), Object.freeze({id: 'research', label: 'Research', description: 'Use evidence and require citations.'}), Object.freeze({id: 'create', label: 'Create', description: 'Prioritize durable creative output.'}), Object.freeze({id: 'private', label: 'Private', description: 'Use ephemeral retention and no memory writes.'}), ]); const MODE_IDS = new Set(MODES.map((mode) => mode.id)); class WaveBContractError extends Error { constructor(message) { super(message); this.name = 'WaveBContractError'; } } function object(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : null; } function need(condition, message) { if (!condition) throw new WaveBContractError(message); } function integer(value, min, max) { return Number.isSafeInteger(value) && value >= min && value <= max; } function utc(value) { return typeof value === 'string' && UTC.test(value) && !Number.isNaN(Date.parse(value)); } function id(value, prefix) { return waveA.isId(value, prefix); } function text(value, fallback, limit) { return waveA.safeText(value, fallback, limit); } function unique(values, max, validator, label) { need(Array.isArray(values) && values.length <= max && values.every(validator) && new Set(values).size === values.length, `Invalid ${label}`); return values.slice(); } function page(raw, limit, label) { need(object(raw) && Array.isArray(raw.items) && raw.items.length <= limit && (raw.next === null || typeof raw.next === 'string'), `Invalid ${label} page`); return raw.items; } function revision(raw) { need(integer(raw.revision, 1, Number.MAX_SAFE_INTEGER), 'Invalid revision'); return raw.revision; } function timestamps(raw) { need(utc(raw.created_at) && utc(raw.updated_at) && raw.updated_at >= raw.created_at, 'Invalid timestamps'); } function owner(raw, expected) { need(raw.owner === expected.userRef, 'Record crossed the user boundary'); } function tags(raw) { return unique(raw, 32, (item) => typeof item === 'string' && TAG.test(item), 'tags'); } function normalizeProject(raw, identity, expectedId) { need(object(raw) && raw.schema === 'hux.project.v1' && id(raw.id, 'prj') && (!expectedId || raw.id === expectedId), 'Invalid project'); owner(raw, identity); timestamps(raw); revision(raw); need(typeof raw.name === 'string' && raw.name.trim() && raw.name.length <= 120 && typeof raw.pinned === 'boolean' && typeof raw.archived === 'boolean' && (raw.default_mode === undefined || MODE_IDS.has(raw.default_mode)), 'Invalid project fields'); return Object.freeze({id: raw.id, name: text(raw.name, 'Untitled project', 120), description: text(raw.description, '', 500), tags: tags(raw.tags), pinned: raw.pinned, archived: raw.archived, defaultMode: raw.default_mode || null, revision: raw.revision, updatedAt: raw.updated_at}); } function normalizeConversation(raw, identity, projectId) { need(object(raw) && raw.schema === 'hux.conversation.v1' && id(raw.id, 'conv') && raw.project_id === projectId, 'Conversation crossed the project boundary'); owner(raw, identity); timestamps(raw); revision(raw); need(typeof raw.title === 'string' && raw.title.trim() && raw.title.length <= 200 && typeof raw.pinned === 'boolean' && typeof raw.archived === 'boolean' && (raw.mode === undefined || MODE_IDS.has(raw.mode)), 'Invalid conversation fields'); const artifactIds = unique(raw.artifact_ids, 500, (item) => id(item, 'art'), 'artifact references'); let branch = null; if (raw.branch !== undefined) { need(object(raw.branch) && id(raw.branch.parent_conversation_id, 'conv') && MESSAGE.test(raw.branch.branch_point_message_id), 'Invalid conversation branch'); branch = Object.freeze({parentId: raw.branch.parent_conversation_id, pointMessageId: raw.branch.branch_point_message_id}); } return Object.freeze({id: raw.id, projectId, title: text(raw.title, 'Untitled conversation', 200), tags: tags(raw.tags), pinned: raw.pinned, archived: raw.archived, mode: raw.mode || null, branch, artifactIds, revision: raw.revision, updatedAt: raw.updated_at}); } function normalizeConversationPage(raw, identity, projectId) { const items = page(raw, 2000, 'conversation').map((item) => normalizeConversation(item, identity, projectId)); need(new Set(items.map((item) => item.id)).size === items.length, 'Duplicate conversations'); return Object.freeze(items); } function normalizeLineage(raw, identity, projectId, conversationId) { need(object(raw) && Array.isArray(raw.ancestors) && raw.ancestors.length <= 64 && Array.isArray(raw.children) && raw.children.length <= 200, 'Invalid lineage'); const current = normalizeConversation(raw.conversation, identity, projectId); need(current.id === conversationId, 'Lineage crossed the conversation boundary'); const ancestors = raw.ancestors.map((item) => normalizeConversation(item, identity, projectId)); const children = raw.children.map((item) => normalizeConversation(item, identity, projectId)); need(new Set([...ancestors, current, ...children].map((item) => item.id)).size === ancestors.length + children.length + 1, 'Lineage contains duplicate records'); return Object.freeze({conversation: current, ancestors: Object.freeze(ancestors), children: Object.freeze(children)}); } function normalizeSearch(raw, identity, projectId) { const items = normalizeConversationPage(raw, identity, projectId); need(Array.isArray(raw.indexed) && Array.isArray(raw.not_indexed) && raw.indexed.every((item) => ['title', 'tags', 'project_name', 'artifact_titles'].includes(item)) && raw.not_indexed.includes('message_text'), 'Search scope is not explicit'); return Object.freeze({items, indexed: Object.freeze(raw.indexed.slice()), messageTextIndexed: false}); } function normalizeVersion(raw) { need(object(raw) && integer(raw.version, 1, 200) && utc(raw.created_at) && object(raw.created_by) && ['user', 'assistant', 'tool', 'system', 'operator'].includes(raw.created_by.type) && typeof raw.created_by.id === 'string' && raw.created_by.id.length <= 120 && object(raw.content_ref) && SHA.test(raw.content_ref.hash) && integer(raw.content_ref.bytes, 0, 25 * 1024 * 1024) && typeof raw.content_ref.mime === 'string' && raw.content_ref.mime.length <= 120, 'Invalid artifact version'); let lineage = null; if (raw.lineage !== undefined) { need(object(raw.lineage) && id(raw.lineage.artifact_id, 'art') && integer(raw.lineage.version, 1, 200), 'Invalid artifact lineage'); lineage = Object.freeze({artifactId: raw.lineage.artifact_id, version: raw.lineage.version}); } return Object.freeze({version: raw.version, createdAt: raw.created_at, creator: raw.created_by.type, bytes: raw.content_ref.bytes, mime: text(raw.content_ref.mime, 'application/octet-stream', 120), note: text(raw.note, '', 200), diffFrom: integer(raw.diff_from, 1, 200) ? raw.diff_from : null, lineage}); } function normalizeArtifact(raw, identity, conversationId, projectId) { need(object(raw) && raw.schema === 'hux.artifact.v1' && id(raw.id, 'art') && raw.conversation_id === conversationId && (raw.project_id === undefined || raw.project_id === projectId) && TYPES.has(raw.type) && typeof raw.title === 'string' && raw.title.trim() && raw.title.length <= 200 && ['public', 'personal', 'sensitive', 'restricted'].includes(raw.sensitivity) && object(raw.access) && raw.access.mode === 'owner', 'Artifact crossed its workspace boundary'); owner(raw, identity); timestamps(raw); revision(raw); need(Array.isArray(raw.versions) && raw.versions.length >= 1 && raw.versions.length <= 200, 'Invalid artifact history'); const versions = raw.versions.map(normalizeVersion); need(versions.every((entry, index) => entry.version === index + 1) && raw.current_version === versions.length, 'Artifact history is not immutable and sequential'); return Object.freeze({id: raw.id, conversationId, projectId: raw.project_id || null, type: raw.type, title: text(raw.title, 'Untitled artifact', 200), sensitivity: raw.sensitivity, currentVersion: raw.current_version, versions: Object.freeze(versions), revision: raw.revision, promoted: Boolean(object(raw.promotion) && raw.promotion.project_id === projectId && integer(raw.promotion.version, 1, raw.current_version)), updatedAt: raw.updated_at}); } function normalizeArtifactPage(raw, identity, conversationId, projectId) { const items = page(raw, 50, 'artifact').map((item) => normalizeArtifact(item, identity, conversationId, projectId)); need(new Set(items.map((item) => item.id)).size === items.length, 'Duplicate artifacts'); return Object.freeze({items: Object.freeze(items), next: raw.next}); } function normalizeArtifactContent(raw, artifact, number) { need(object(raw) && raw.artifact_id === artifact.id && raw.type === artifact.type && object(raw.version) && raw.version.version === number, 'Artifact content crossed its version boundary'); const version = normalizeVersion(raw.version); need((typeof raw.content === 'string') !== (typeof raw.content_base64 === 'string') && (raw.content === undefined || TEXT_TYPES.has(artifact.type)), 'Unsafe artifact content envelope'); if (raw.content === undefined || artifact.sensitivity === 'restricted') { return Object.freeze({version, preview: null, reason: artifact.sensitivity === 'restricted' ? 'Preview hidden because this artifact is restricted.' : 'Binary content stays attachment-only.'}); } return Object.freeze({version, preview: text(raw.content, '', 20000), reason: raw.content.length > 20000 ? 'Preview truncated at 20,000 characters.' : ''}); } function normalizeDiff(raw, from, to) { need(object(raw) && raw.from === from && raw.to === to && (typeof raw.unified === 'string') !== Boolean(object(raw.binary)), 'Invalid artifact diff'); if (typeof raw.unified === 'string') { const lines = raw.unified.slice(0, 20000).split('\n').map((line) => text(line, '', 1000)); return Object.freeze({text: lines.join('\n'), binary: false}); } need(integer(raw.binary.from_bytes, 0, 25 * 1024 * 1024) && integer(raw.binary.to_bytes, 0, 25 * 1024 * 1024), 'Invalid binary diff'); return Object.freeze({text: `${raw.binary.from_bytes} bytes → ${raw.binary.to_bytes} bytes`, binary: true}); } function safeSourceUrl(value) { if (typeof value !== 'string' || value.length > 2000) return null; let parsed; try { parsed = new URL(value); } catch (_) { return null; } if (parsed.protocol !== 'https:' || !parsed.hostname || parsed.username || parsed.password) return null; const sensitive = /^(?:access_?token|api_?key|auth|authorization|code|credential|key|password|secret|sig|signature|token)$/i; if ([...parsed.searchParams.keys()].some((key) => sensitive.test(key))) return null; parsed.hash = ''; return parsed.toString(); } function isTextType(value) { return TEXT_TYPES.has(value); } function normalizeSource(raw, conversationId) { need(object(raw) && raw.schema === 'hux.source.v1' && id(raw.id, 'src') && ['web', 'document', 'artifact', 'memory', 'tool_output', 'dataset'].includes(raw.kind) && typeof raw.title === 'string' && raw.title.trim() && raw.title.length <= 300 && ['primary', 'secondary', 'unknown'].includes(raw.classification) && utc(raw.retrieved_at), 'Invalid source'); if (object(raw.provenance) && raw.provenance.conversation_id !== undefined) { need(raw.provenance.conversation_id === conversationId, 'Source crossed the conversation boundary'); } return Object.freeze({id: raw.id, title: text(raw.title, 'Untitled source', 300), kind: raw.kind, classification: raw.classification, publisher: text(raw.publisher, '', 200), publishedAt: utc(raw.published_at) ? raw.published_at : null, url: safeSourceUrl(raw.uri)}); } function normalizeCitationPage(raw, messageId, conversationId) { const items = page(raw, 200, 'citation').map((bundle) => { need(object(bundle) && object(bundle.citation) && Array.isArray(bundle.passages) && Array.isArray(bundle.sources), 'Invalid citation bundle'); const citation = bundle.citation; need(citation.schema === 'hux.citation.v1' && id(citation.id, 'cit') && citation.message_id === messageId && typeof citation.claim === 'string' && citation.claim.trim() && citation.claim.length <= 1000 && ['supports', 'partially_supports', 'contradicts', 'unverified'].includes(citation.support), 'Citation crossed the message boundary'); const passageIds = unique(citation.passage_ids, 32, (item) => id(item, 'psg'), 'citation passages'); need(passageIds.length > 0, 'Citation needs evidence'); const sources = bundle.sources.map((source) => normalizeSource(source, conversationId)); const sourceMap = new Map(sources.map((source) => [source.id, source])); const passages = bundle.passages.map((passage) => { need(object(passage) && passage.schema === 'hux.passage.v1' && id(passage.id, 'psg') && passageIds.includes(passage.id) && id(passage.source_id, 'src') && sourceMap.has(passage.source_id) && typeof passage.text === 'string' && passage.text.trim() && passage.text.length <= 4000, 'Invalid supporting passage'); return Object.freeze({id: passage.id, sourceId: passage.source_id, text: text(passage.text, 'Passage unavailable.', 4000)}); }); need(passages.length === passageIds.length && new Set(passages.map((item) => item.id)).size === passages.length, 'Citation evidence is incomplete'); return Object.freeze({id: citation.id, messageId, claim: text(citation.claim, 'Claim unavailable.', 1000), support: citation.support, note: text(citation.note, '', 500), passages: Object.freeze(passages), sources: Object.freeze(sources)}); }); need(new Set(items.map((item) => item.id)).size === items.length, 'Duplicate citations'); return Object.freeze(items); } function normalizeNotebook(raw, conversationId, etag) { need(object(raw) && raw.schema === 'hux.research_notebook.v1' && id(raw.id, 'nb') && raw.conversation_id === conversationId && typeof raw.question === 'string' && raw.question.trim() && raw.question.length <= 1000 && ['open', 'answered', 'abandoned'].includes(raw.status) && utc(raw.updated_at), 'Notebook crossed the conversation boundary'); const rev = revision(raw); need(String(etag || '').replaceAll('"', '') === String(rev), 'Notebook ETag does not match its revision'); const sourceIds = unique(raw.source_ids, 500, (item) => id(item, 'src'), 'notebook sources'); const passageIds = unique(raw.passage_ids, 500, (item) => id(item, 'psg'), 'notebook passages'); const citationIds = unique(raw.citation_ids, 500, (item) => id(item, 'cit'), 'notebook citations'); const assumptions = unique(raw.assumptions, 64, (item) => typeof item === 'string' && item.trim() && item.length <= 500, 'assumptions'); const questions = unique(raw.unresolved_questions, 64, (item) => typeof item === 'string' && item.trim() && item.length <= 500, 'questions'); need(Array.isArray(raw.notes) && raw.notes.length <= 128, 'Invalid notebook notes'); const notes = raw.notes.map((note) => { need(object(note) && utc(note.at) && typeof note.text === 'string' && note.text.trim() && note.text.length <= 2000 && (note.source_id === undefined || id(note.source_id, 'src')), 'Invalid notebook note'); return Object.freeze({at: note.at, text: text(note.text, 'Note unavailable.', 2000), sourceId: note.source_id || null}); }); return Object.freeze({id: raw.id, conversationId, question: text(raw.question, 'Research question unavailable.', 1000), status: raw.status, sourceIds: Object.freeze(sourceIds), passageIds: Object.freeze(passageIds), citationIds: Object.freeze(citationIds), assumptions: Object.freeze(assumptions.map((item) => text(item, '', 500))), unresolvedQuestions: Object.freeze(questions.map((item) => text(item, '', 500))), notes: Object.freeze(notes), revision: rev, updatedAt: raw.updated_at}); } return Object.freeze({MESSAGE, MODES, MODE_IDS, WaveBContractError, isId: id, isTextType, normalizeArtifact, normalizeArtifactContent, normalizeArtifactPage, normalizeCitationPage, normalizeConversation, normalizeConversationPage, normalizeDiff, normalizeLineage, normalizeNotebook, normalizeProject, normalizeSearch, normalizeSource, safeSourceUrl}); }));