import type { GraphApiDocument, GraphApiMemory } from "./types" export interface MockGraphOptions { documentCount?: number memoriesPerDoc?: number | [number, number] seed?: number } /** * Simple seeded PRNG (Mulberry32). * Returns a function that produces values in [0, 1). */ function createSeededRandom(seed: number): () => number { let s = seed | 0 return () => { s = (s + 0x6d2b79f5) | 0 let t = Math.imul(s ^ (s >>> 15), 1 | s) t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t return ((t ^ (t >>> 14)) >>> 0) / 4294967296 } } const DOCUMENT_TYPES = [ "webpage", "pdf", "md", "doc", "csv", "json", "notion", "text", "google_doc", "google_sheet", "google_slide", "tweet", "youtube", "image", "note", "google_drive", "video", "mcp", "word", ] as const const TITLE_PREFIXES = [ "Project Planning", "API Documentation", "Meeting Minutes", "Architecture Design", "Sprint Retrospective", "User Research", "Product Roadmap", "Technical Spec", "Onboarding Guide", "Release Notes", "Performance Report", "Security Audit", "Database Schema", "Deployment Guide", "Team Standup", "Bug Triage", "Feature Request", "Code Review", "Integration Test", "Data Pipeline", ] const TITLE_SUFFIXES = [ "Notes", "Q3", "Q4", "v2", "v3", "Final", "Draft", "2024", "2025", "Summary", "Overview", "Detailed", "Analysis", "Report", "Update", ] const MEMORY_TEMPLATES = [ "The user prefers {preference} when working with {topic}", "Key decision: {topic} will be implemented using {approach}", "Important constraint: {topic} must handle {requirement}", "The {component} module depends on {dependency} for {purpose}", "Performance target: {metric} should be under {threshold}", "The team agreed to {action} before {deadline}", "User feedback indicates {observation} about {feature}", "Best practice: always {action} when {condition}", "Known issue: {component} has {issue} under {condition}", "Migration plan: move from {oldThing} to {newThing} by {deadline}", ] const TEMPLATE_FILLS: Record = { preference: [ "TypeScript", "dark mode", "keyboard shortcuts", "minimal UI", "detailed logs", "async patterns", ], topic: [ "authentication", "caching", "data sync", "graph rendering", "search indexing", "memory management", ], approach: [ "event sourcing", "CQRS", "microservices", "edge functions", "WebSocket streams", "batch processing", ], requirement: [ "10k concurrent users", "sub-100ms latency", "offline mode", "real-time updates", "GDPR compliance", ], component: [ "auth", "graph", "search", "storage", "notification", "analytics", ], dependency: [ "Redis", "PostgreSQL", "S3", "Cloudflare Workers", "D3-force", "WebGL", ], purpose: [ "caching", "persistence", "rendering", "indexing", "scheduling", "routing", ], metric: [ "p99 latency", "time to interactive", "memory usage", "CPU utilization", "bundle size", ], threshold: ["200ms", "500ms", "50MB", "3 seconds", "100KB", "1GB"], action: [ "run benchmarks", "update dependencies", "write migration scripts", "review PRs", "deploy to staging", ], deadline: ["end of sprint", "next release", "Q4", "Friday", "the demo"], observation: [ "confusion", "delight", "frustration", "efficiency gains", "unexpected usage patterns", ], feature: [ "the graph view", "search filters", "memory chains", "document upload", "sharing", ], condition: [ "high load", "cold start", "large datasets", "slow networks", "concurrent edits", ], issue: [ "memory leaks", "race conditions", "stale data", "layout thrashing", "connection drops", ], oldThing: [ "REST API", "MongoDB", "class components", "Webpack", "manual testing", ], newThing: ["GraphQL", "PostgreSQL", "hooks", "Vite", "automated CI"], } const SUMMARY_TEMPLATES = [ "Documentation covering {topic} implementation details and best practices for the team.", "Notes from the recent discussion about {topic} and the decisions made regarding {approach}.", "Technical overview of the {component} system, including architecture and key design choices.", "Analysis of {metric} performance data, with recommendations for optimization.", "Guide for setting up and configuring {component} in development and production environments.", "Collection of user feedback and research findings related to {feature}.", ] function fillTemplate(template: string, random: () => number): string { return template.replace(/{(\w+)}/g, (_match, key: string) => { const options = TEMPLATE_FILLS[key] if (!options || options.length === 0) return key return options[Math.floor(random() * options.length)] }) } function generateTitle(random: () => number): string { const prefix = TITLE_PREFIXES[Math.floor(random() * TITLE_PREFIXES.length)] if (random() > 0.5) { const suffix = TITLE_SUFFIXES[Math.floor(random() * TITLE_SUFFIXES.length)] return `${prefix} ${suffix}` } return prefix } function generateSummary(random: () => number): string | null { if (random() < 0.15) return null const template = SUMMARY_TEMPLATES[Math.floor(random() * SUMMARY_TEMPLATES.length)] return fillTemplate(template, random) } function generateMemoryContent(random: () => number): string { const template = MEMORY_TEMPLATES[Math.floor(random() * MEMORY_TEMPLATES.length)] return fillTemplate(template, random) } function generateISODate( random: () => number, baseMs: number, rangeMs: number, ): string { const ms = baseMs + Math.floor(random() * rangeMs) return new Date(ms).toISOString() } export function generateMockGraphData(options: MockGraphOptions = {}): { documents: GraphApiDocument[] } { const { documentCount = 100, memoriesPerDoc = [2, 6] as [number, number], seed = 42, } = options const random = createSeededRandom(seed) // Time range: roughly Jan 2024 to Jun 2025 const baseTime = new Date("2024-01-01T00:00:00Z").getTime() const timeRange = 1000 * 60 * 60 * 24 * 540 // ~540 days const spaceIds = [ "space-default", "space-work", "space-personal", "space-research", ] const documents: GraphApiDocument[] = [] for (let d = 0; d < documentCount; d++) { const docId = `doc-${String(d).padStart(4, "0")}` const docCreatedAt = generateISODate(random, baseTime, timeRange) const docUpdatedAt = generateISODate( random, new Date(docCreatedAt).getTime(), 1000 * 60 * 60 * 24 * 30, // up to 30 days after creation ) // Determine memory count let memCount: number if (typeof memoriesPerDoc === "number") { memCount = memoriesPerDoc } else { const [min, max] = memoriesPerDoc memCount = min + Math.floor(random() * (max - min + 1)) } const spaceId = spaceIds[Math.floor(random() * spaceIds.length)] const memories: GraphApiMemory[] = [] // Decide if this document has a version chain (30% chance) const hasVersionChain = random() < 0.3 && memCount >= 3 let chainRootId: string | null = null let chainPrevId: string | null = null let chainVersion = 1 for (let m = 0; m < memCount; m++) { const memId = `mem-${docId}-${String(m).padStart(3, "0")}` const memCreatedAt = generateISODate( random, new Date(docCreatedAt).getTime(), 1000 * 60 * 60 * 24 * 14, ) const memUpdatedAt = generateISODate( random, new Date(memCreatedAt).getTime(), 1000 * 60 * 60 * 24 * 7, ) let parentMemoryId: string | null = null let rootMemoryId: string | null = null let version = 1 let isLatest = true let isForgotten = false let memoryRelations: Record = {} // Build version chain for first few memories if applicable if (hasVersionChain && m < 3) { if (m === 0) { // Root of the chain chainRootId = memId chainPrevId = memId chainVersion = 1 version = 1 isLatest = false isForgotten = random() < 0.3 } else { parentMemoryId = chainPrevId rootMemoryId = chainRootId chainVersion++ version = chainVersion chainPrevId = memId isLatest = m === 2 // last in the 3-memory chain isForgotten = !isLatest && random() < 0.2 // Add "updates" relation to parent if (parentMemoryId) { memoryRelations = { [parentMemoryId]: "updates" } } } } else { // Standalone memory isForgotten = random() < 0.1 isLatest = true version = 1 // Randomly add extends/derives relations to earlier memories in this doc if (m > 0 && random() < 0.2) { const targetIdx = Math.floor(random() * m) const targetMem = memories[targetIdx] if (targetMem) { const relType = random() < 0.5 ? "extends" : "derives" memoryRelations = { [targetMem.id]: relType, } } } } // Determine forgetAfter (for some non-forgotten memories, set a future expiry) let forgetAfter: string | null = null let forgetReason: string | null = null if (isForgotten) { forgetReason = random() < 0.5 ? "superseded" : "user-requested" } else if (random() < 0.1) { // Expiring memory const expiryMs = Date.now() + Math.floor(random() * 1000 * 60 * 60 * 24 * 30) forgetAfter = new Date(expiryMs).toISOString() } memories.push({ id: memId, memory: generateMemoryContent(random), isStatic: random() < 0.15, spaceId, isLatest, isForgotten, forgetAfter, forgetReason, version, parentMemoryId, rootMemoryId, createdAt: memCreatedAt, updatedAt: memUpdatedAt, memoryRelations: Object.keys(memoryRelations).length > 0 ? memoryRelations : undefined, }) } documents.push({ id: docId, title: generateTitle(random), summary: generateSummary(random), documentType: DOCUMENT_TYPES[Math.floor(random() * DOCUMENT_TYPES.length)], createdAt: docCreatedAt, updatedAt: docUpdatedAt, memories, }) } return { documents } }