mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-05 08:06:19 +00:00
414 lines
9.8 KiB
TypeScript
414 lines
9.8 KiB
TypeScript
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<string, string[]> = {
|
|
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<string, "updates" | "extends" | "derives"> =
|
|
{}
|
|
|
|
// 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 }
|
|
}
|