import { createHash, randomUUID } from 'node:crypto'; import type { Fact, FactTopic, Space, Topic } from '../types/api'; import type { FactRecord, SpaceRecord, TopicRecord } from '../types/domain'; export function normalizeTopicName(name: string): string { return name.trim().replace(/\s+/g, ' ').toLowerCase(); } export function canonicalizeTopicName(name: string): string { return name.trim().replace(/\s+/g, ' '); } export function normalizeSpaceName(name: string): string { return normalizeTopicName(name); } export function canonicalizeSpaceName(name: string): string { return canonicalizeTopicName(name); } export function nowIsoString(): string { return new Date().toISOString(); } export function createId(): string { return randomUUID(); } export function serializeMetadata(metadata: unknown): string | null { if (metadata === undefined || metadata === null) { return null; } return JSON.stringify(metadata); } export function deserializeMetadata(metadata: string | null): unknown | null { if (metadata === null) { return null; } return JSON.parse(metadata); } export function serializeEmbedding(embedding: number[]): string { return JSON.stringify(embedding); } export function deserializeEmbedding(embedding: string): number[] { return JSON.parse(embedding) as number[]; } export function createContentHash(input: string): string { return createHash('sha256').update(input).digest('hex'); } export function cosineSimilarity(left: number[], right: number[]): number { if (left.length === 0 || left.length !== right.length) { return 0; } let dot = 0; let leftMagnitude = 0; let rightMagnitude = 0; for (let index = 0; index < left.length; index += 1) { const leftValue = left[index] ?? 0; const rightValue = right[index] ?? 0; dot += leftValue * rightValue; leftMagnitude += leftValue * leftValue; rightMagnitude += rightValue * rightValue; } if (leftMagnitude === 0 || rightMagnitude === 0) { return 0; } return dot / (Math.sqrt(leftMagnitude) * Math.sqrt(rightMagnitude)); } export function mapSpaceRow(record: SpaceRecord): Space { return { id: record.id, name: record.name, normalizedName: record.normalized_name, description: record.description, metadata: deserializeMetadata(record.metadata) as Space['metadata'], createdAt: record.created_at, updatedAt: record.updated_at, }; } export function mapTopicRow(record: TopicRecord): Topic { return { id: record.id, spaceId: record.space_id, name: record.name, normalizedName: record.normalized_name, category: record.category, granularity: record.granularity, description: record.description, metadata: deserializeMetadata(record.metadata) as Topic['metadata'], createdAt: record.created_at, updatedAt: record.updated_at, }; } export function mapFactRow(record: FactRecord, topics: FactTopic[]): Fact { return { id: record.id, spaceId: record.space_id, statement: record.statement, summary: record.summary, source: record.source, confidence: record.confidence, metadata: deserializeMetadata(record.metadata) as Fact['metadata'], createdAt: record.created_at, updatedAt: record.updated_at, topics, }; }