123 lines
3.2 KiB
TypeScript
123 lines
3.2 KiB
TypeScript
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,
|
|
};
|
|
}
|