/** * Memory Card — Cross-module data interchange format. * * Any module can export items as Memory Cards; any module can import/reference them. * Think of it as a universal "clip" format for data flowing between rSpace modules. */ import type { DocumentId } from './document'; // ============================================================================ // TYPES // ============================================================================ /** * Core Memory Card — the universal interchange unit. */ export interface MemoryCard { /** Unique ID (UUID v4 or module-specific) */ id: string; /** Semantic type */ type: MemoryCardType | string; /** Human-readable title */ title: string; /** Optional body text (markdown) */ body?: string; /** Source provenance */ source: { module: string; docId: DocumentId; itemId?: string; }; /** Freeform tags for filtering/grouping */ tags: string[]; /** Unix timestamp (ms) */ createdAt: number; /** Module-specific structured data */ data?: Record; } /** * Well-known card types. Modules can extend with custom strings. */ export type MemoryCardType = | 'note' | 'task' | 'event' | 'link' | 'file' | 'vote' | 'transaction' | 'trip' | 'contact' | 'shape' | 'book' | 'product' | 'post'; // ============================================================================ // HELPERS // ============================================================================ /** * Create a Memory Card with defaults. */ export function createCard( fields: Pick & Partial ): MemoryCard { return { id: fields.id ?? generateId(), type: fields.type, title: fields.title, body: fields.body, source: fields.source, tags: fields.tags ?? [], createdAt: fields.createdAt ?? Date.now(), data: fields.data, }; } /** * Filter cards by type. */ export function filterByType(cards: MemoryCard[], type: string): MemoryCard[] { return cards.filter((c) => c.type === type); } /** * Filter cards by tag (any match). */ export function filterByTag(cards: MemoryCard[], tag: string): MemoryCard[] { return cards.filter((c) => c.tags.includes(tag)); } /** * Filter cards by source module. */ export function filterByModule(cards: MemoryCard[], module: string): MemoryCard[] { return cards.filter((c) => c.source.module === module); } /** * Sort cards by creation time (newest first). */ export function sortByNewest(cards: MemoryCard[]): MemoryCard[] { return [...cards].sort((a, b) => b.createdAt - a.createdAt); } /** * Search cards by title/body text (case-insensitive substring match). */ export function searchCards(cards: MemoryCard[], query: string): MemoryCard[] { const q = query.toLowerCase(); return cards.filter( (c) => c.title.toLowerCase().includes(q) || (c.body && c.body.toLowerCase().includes(q)) ); } // ============================================================================ // CARD EXPORTER INTERFACE // ============================================================================ /** * Modules implement this to export their data as Memory Cards. */ export interface CardExporter { module: string; /** Export all items from a document as cards */ exportCards(docId: DocumentId, doc: any): MemoryCard[]; /** Export a single item as a card */ exportCard?(docId: DocumentId, doc: any, itemId: string): MemoryCard | null; } /** * Registry of card exporters. */ const exporters = new Map(); export function registerExporter(exporter: CardExporter): void { exporters.set(exporter.module, exporter); } export function getExporter(module: string): CardExporter | undefined { return exporters.get(module); } export function getAllExporters(): CardExporter[] { return Array.from(exporters.values()); } // ============================================================================ // UTILITIES // ============================================================================ function generateId(): string { // crypto.randomUUID() is available in all modern browsers and Bun if (typeof crypto !== 'undefined' && crypto.randomUUID) { return crypto.randomUUID(); } // Fallback return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; }