@twelvehart/supermemory-runtime 1.0.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/.env.example +57 -0
  2. package/README.md +374 -0
  3. package/dist/index.js +189 -0
  4. package/dist/mcp/index.js +1132 -0
  5. package/docker-compose.prod.yml +91 -0
  6. package/docker-compose.yml +358 -0
  7. package/drizzle/0000_dapper_the_professor.sql +159 -0
  8. package/drizzle/0001_api_keys.sql +51 -0
  9. package/drizzle/meta/0000_snapshot.json +1532 -0
  10. package/drizzle/meta/_journal.json +13 -0
  11. package/drizzle.config.ts +20 -0
  12. package/package.json +114 -0
  13. package/scripts/add-extraction-job.ts +122 -0
  14. package/scripts/benchmark-pgvector.ts +122 -0
  15. package/scripts/bootstrap.sh +209 -0
  16. package/scripts/check-runtime-pack.ts +111 -0
  17. package/scripts/claude-mcp-config.ts +336 -0
  18. package/scripts/docker-entrypoint.sh +183 -0
  19. package/scripts/doctor.ts +377 -0
  20. package/scripts/init-db.sql +33 -0
  21. package/scripts/install.sh +1110 -0
  22. package/scripts/mcp-setup.ts +271 -0
  23. package/scripts/migrations/001_create_pgvector_extension.sql +31 -0
  24. package/scripts/migrations/002_create_memory_embeddings_table.sql +75 -0
  25. package/scripts/migrations/003_create_hnsw_index.sql +94 -0
  26. package/scripts/migrations/004_create_memory_embeddings_standalone.sql +70 -0
  27. package/scripts/migrations/005_create_chunks_table.sql +95 -0
  28. package/scripts/migrations/006_create_processing_queue.sql +45 -0
  29. package/scripts/migrations/generate_test_data.sql +42 -0
  30. package/scripts/migrations/phase1_comprehensive_test.sql +204 -0
  31. package/scripts/migrations/run_migrations.sh +286 -0
  32. package/scripts/migrations/test_hnsw_index.sql +255 -0
  33. package/scripts/pre-commit-secrets +282 -0
  34. package/scripts/run-extraction-worker.ts +46 -0
  35. package/scripts/run-phase1-tests.sh +291 -0
  36. package/scripts/setup.ts +222 -0
  37. package/scripts/smoke-install.sh +12 -0
  38. package/scripts/test-health-endpoint.sh +328 -0
  39. package/src/api/index.ts +2 -0
  40. package/src/api/middleware/auth.ts +80 -0
  41. package/src/api/middleware/csrf.ts +308 -0
  42. package/src/api/middleware/errorHandler.ts +166 -0
  43. package/src/api/middleware/rateLimit.ts +360 -0
  44. package/src/api/middleware/validation.ts +514 -0
  45. package/src/api/routes/documents.ts +286 -0
  46. package/src/api/routes/profiles.ts +237 -0
  47. package/src/api/routes/search.ts +71 -0
  48. package/src/api/stores/index.ts +58 -0
  49. package/src/config/bootstrap-env.ts +3 -0
  50. package/src/config/env.ts +71 -0
  51. package/src/config/feature-flags.ts +25 -0
  52. package/src/config/index.ts +140 -0
  53. package/src/config/secrets.config.ts +291 -0
  54. package/src/db/client.ts +92 -0
  55. package/src/db/index.ts +73 -0
  56. package/src/db/postgres.ts +72 -0
  57. package/src/db/schema/chunks.schema.ts +31 -0
  58. package/src/db/schema/containers.schema.ts +46 -0
  59. package/src/db/schema/documents.schema.ts +49 -0
  60. package/src/db/schema/embeddings.schema.ts +32 -0
  61. package/src/db/schema/index.ts +11 -0
  62. package/src/db/schema/memories.schema.ts +72 -0
  63. package/src/db/schema/profiles.schema.ts +34 -0
  64. package/src/db/schema/queue.schema.ts +59 -0
  65. package/src/db/schema/relationships.schema.ts +42 -0
  66. package/src/db/schema.ts +223 -0
  67. package/src/db/worker-connection.ts +47 -0
  68. package/src/index.ts +235 -0
  69. package/src/mcp/CLAUDE.md +1 -0
  70. package/src/mcp/index.ts +1380 -0
  71. package/src/mcp/legacyState.ts +22 -0
  72. package/src/mcp/rateLimit.ts +358 -0
  73. package/src/mcp/resources.ts +309 -0
  74. package/src/mcp/results.ts +104 -0
  75. package/src/mcp/tools.ts +401 -0
  76. package/src/queues/config.ts +119 -0
  77. package/src/queues/index.ts +289 -0
  78. package/src/sdk/client.ts +225 -0
  79. package/src/sdk/errors.ts +266 -0
  80. package/src/sdk/http.ts +560 -0
  81. package/src/sdk/index.ts +244 -0
  82. package/src/sdk/resources/base.ts +65 -0
  83. package/src/sdk/resources/connections.ts +204 -0
  84. package/src/sdk/resources/documents.ts +163 -0
  85. package/src/sdk/resources/index.ts +10 -0
  86. package/src/sdk/resources/memories.ts +150 -0
  87. package/src/sdk/resources/search.ts +60 -0
  88. package/src/sdk/resources/settings.ts +36 -0
  89. package/src/sdk/types.ts +674 -0
  90. package/src/services/chunking/index.ts +451 -0
  91. package/src/services/chunking.service.ts +650 -0
  92. package/src/services/csrf.service.ts +252 -0
  93. package/src/services/documents.repository.ts +219 -0
  94. package/src/services/documents.service.ts +191 -0
  95. package/src/services/embedding.service.ts +404 -0
  96. package/src/services/extraction.service.ts +300 -0
  97. package/src/services/extractors/code.extractor.ts +451 -0
  98. package/src/services/extractors/index.ts +9 -0
  99. package/src/services/extractors/markdown.extractor.ts +461 -0
  100. package/src/services/extractors/pdf.extractor.ts +315 -0
  101. package/src/services/extractors/text.extractor.ts +118 -0
  102. package/src/services/extractors/url.extractor.ts +243 -0
  103. package/src/services/index.ts +235 -0
  104. package/src/services/ingestion.service.ts +177 -0
  105. package/src/services/llm/anthropic.ts +400 -0
  106. package/src/services/llm/base.ts +460 -0
  107. package/src/services/llm/contradiction-detector.service.ts +526 -0
  108. package/src/services/llm/heuristics.ts +148 -0
  109. package/src/services/llm/index.ts +309 -0
  110. package/src/services/llm/memory-classifier.service.ts +383 -0
  111. package/src/services/llm/memory-extension-detector.service.ts +523 -0
  112. package/src/services/llm/mock.ts +470 -0
  113. package/src/services/llm/openai.ts +398 -0
  114. package/src/services/llm/prompts.ts +438 -0
  115. package/src/services/llm/types.ts +373 -0
  116. package/src/services/memory.repository.ts +1769 -0
  117. package/src/services/memory.service.ts +1338 -0
  118. package/src/services/memory.types.ts +234 -0
  119. package/src/services/persistence/index.ts +295 -0
  120. package/src/services/pipeline.service.ts +509 -0
  121. package/src/services/profile.repository.ts +436 -0
  122. package/src/services/profile.service.ts +560 -0
  123. package/src/services/profile.types.ts +270 -0
  124. package/src/services/relationships/detector.ts +1128 -0
  125. package/src/services/relationships/index.ts +268 -0
  126. package/src/services/relationships/memory-integration.ts +459 -0
  127. package/src/services/relationships/strategies.ts +132 -0
  128. package/src/services/relationships/types.ts +370 -0
  129. package/src/services/search.service.ts +761 -0
  130. package/src/services/search.types.ts +220 -0
  131. package/src/services/secrets.service.ts +384 -0
  132. package/src/services/vectorstore/base.ts +327 -0
  133. package/src/services/vectorstore/index.ts +444 -0
  134. package/src/services/vectorstore/memory.ts +286 -0
  135. package/src/services/vectorstore/migration.ts +295 -0
  136. package/src/services/vectorstore/mock.ts +403 -0
  137. package/src/services/vectorstore/pgvector.ts +695 -0
  138. package/src/services/vectorstore/types.ts +247 -0
  139. package/src/startup.ts +389 -0
  140. package/src/types/api.types.ts +193 -0
  141. package/src/types/document.types.ts +103 -0
  142. package/src/types/index.ts +241 -0
  143. package/src/types/profile.base.ts +133 -0
  144. package/src/utils/errors.ts +447 -0
  145. package/src/utils/id.ts +15 -0
  146. package/src/utils/index.ts +101 -0
  147. package/src/utils/logger.ts +313 -0
  148. package/src/utils/sanitization.ts +501 -0
  149. package/src/utils/secret-validation.ts +273 -0
  150. package/src/utils/synonyms.ts +188 -0
  151. package/src/utils/validation.ts +581 -0
  152. package/src/workers/chunking.worker.ts +242 -0
  153. package/src/workers/embedding.worker.ts +358 -0
  154. package/src/workers/extraction.worker.ts +346 -0
  155. package/src/workers/indexing.worker.ts +505 -0
  156. package/tsconfig.json +38 -0
@@ -0,0 +1,436 @@
1
+ /**
2
+ * Profile Repository - Database operations for user profiles
3
+ *
4
+ * Handles persistence of user profiles and facts using PostgreSQL.
5
+ */
6
+
7
+ import { UserProfile, ProfileFact, FactType, PROFILE_DEFAULTS } from './profile.types.js'
8
+ import { getPostgresDatabase } from '../db/postgres.js'
9
+ import { getDatabaseUrl, isPostgresUrl } from '../db/client.js'
10
+ import { userProfiles } from '../db/schema/profiles.schema.js'
11
+ import { containerTags } from '../db/schema/containers.schema.js'
12
+ import { eq } from 'drizzle-orm'
13
+
14
+ let _db: ReturnType<typeof getPostgresDatabase> | null = null
15
+
16
+ function getDb(): ReturnType<typeof getPostgresDatabase> {
17
+ if (_db) return _db
18
+ const databaseUrl = getDatabaseUrl()
19
+ if (!isPostgresUrl(databaseUrl)) {
20
+ throw new Error(
21
+ 'ProfileRepository requires a PostgreSQL DATABASE_URL. SQLite is only supported in tests and is not compatible with profile persistence.'
22
+ )
23
+ }
24
+ _db = getPostgresDatabase(databaseUrl)
25
+ return _db
26
+ }
27
+
28
+ const db = new Proxy({} as ReturnType<typeof getPostgresDatabase>, {
29
+ get(_target, prop) {
30
+ return getDb()[prop as keyof ReturnType<typeof getPostgresDatabase>]
31
+ },
32
+ })
33
+
34
+ const PROFILE_VERSION_KEY = 'profileVersion'
35
+
36
+ function normalizeFacts(facts: unknown): ProfileFact[] {
37
+ if (!Array.isArray(facts)) {
38
+ return []
39
+ }
40
+
41
+ return facts.map((fact) => normalizeFact(fact as ProfileFact))
42
+ }
43
+
44
+ function normalizeFact(fact: ProfileFact): ProfileFact {
45
+ return {
46
+ ...fact,
47
+ extractedAt: new Date(fact.extractedAt),
48
+ lastAccessedAt: new Date(fact.lastAccessedAt),
49
+ expiresAt: fact.expiresAt ? new Date(fact.expiresAt) : undefined,
50
+ }
51
+ }
52
+
53
+ function normalizeRecordObject(value: unknown): Record<string, unknown> {
54
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
55
+ return { ...(value as Record<string, unknown>) }
56
+ }
57
+ return {}
58
+ }
59
+
60
+ function getProfileVersion(record: { computedTraits: unknown } | null | undefined): number {
61
+ const computedTraits = normalizeRecordObject(record?.computedTraits)
62
+ const storedVersion = computedTraits[PROFILE_VERSION_KEY]
63
+ if (typeof storedVersion === 'number' && Number.isFinite(storedVersion)) {
64
+ return storedVersion
65
+ }
66
+ return 1
67
+ }
68
+
69
+ function withProfileVersion(computedTraits: Record<string, unknown>, version: number): Record<string, unknown> {
70
+ return {
71
+ ...computedTraits,
72
+ [PROFILE_VERSION_KEY]: version,
73
+ }
74
+ }
75
+
76
+ async function ensureContainerTag(tag: string): Promise<void> {
77
+ await db.insert(containerTags).values({ tag }).onConflictDoNothing({ target: containerTags.tag })
78
+ }
79
+
80
+ function mapDbProfile(record: typeof userProfiles.$inferSelect): UserProfile {
81
+ const computedTraits = normalizeRecordObject(record.computedTraits)
82
+ const version = getProfileVersion({ computedTraits })
83
+
84
+ return {
85
+ containerTag: record.containerTag,
86
+ staticFacts: normalizeFacts(record.staticFacts),
87
+ dynamicFacts: normalizeFacts(record.dynamicFacts),
88
+ createdAt: record.createdAt,
89
+ updatedAt: record.updatedAt,
90
+ version,
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Database interface for profile storage
96
+ * Implement this interface for different storage backends
97
+ */
98
+ export interface ProfileDatabase {
99
+ findByContainerTag(containerTag: string): Promise<UserProfile | null>
100
+ upsert(profile: UserProfile): Promise<UserProfile>
101
+ updateFacts(
102
+ containerTag: string,
103
+ staticFacts: ProfileFact[],
104
+ dynamicFacts: ProfileFact[]
105
+ ): Promise<UserProfile | null>
106
+ delete(containerTag: string): Promise<boolean>
107
+ listAll(): Promise<UserProfile[]>
108
+ }
109
+
110
+ /**
111
+ * PostgreSQL implementation of ProfileDatabase
112
+ */
113
+ class PostgresProfileDatabase implements ProfileDatabase {
114
+ async findByContainerTag(containerTag: string): Promise<UserProfile | null> {
115
+ const [record] = await db.select().from(userProfiles).where(eq(userProfiles.containerTag, containerTag))
116
+
117
+ return record ? mapDbProfile(record) : null
118
+ }
119
+
120
+ async upsert(profile: UserProfile): Promise<UserProfile> {
121
+ await ensureContainerTag(profile.containerTag)
122
+ const [existing] = await db.select().from(userProfiles).where(eq(userProfiles.containerTag, profile.containerTag))
123
+
124
+ if (existing) {
125
+ const computedTraits = normalizeRecordObject(existing.computedTraits)
126
+ const nextVersion = getProfileVersion({ computedTraits }) + 1
127
+
128
+ const [updated] = await db
129
+ .update(userProfiles)
130
+ .set({
131
+ staticFacts: profile.staticFacts,
132
+ dynamicFacts: profile.dynamicFacts,
133
+ updatedAt: new Date(),
134
+ computedTraits: withProfileVersion(computedTraits, nextVersion),
135
+ })
136
+ .where(eq(userProfiles.containerTag, profile.containerTag))
137
+ .returning()
138
+
139
+ return updated ? mapDbProfile(updated) : profile
140
+ }
141
+
142
+ const computedTraits = withProfileVersion({}, 1)
143
+ const [created] = await db
144
+ .insert(userProfiles)
145
+ .values({
146
+ containerTag: profile.containerTag,
147
+ staticFacts: profile.staticFacts,
148
+ dynamicFacts: profile.dynamicFacts,
149
+ createdAt: profile.createdAt ?? new Date(),
150
+ updatedAt: profile.updatedAt ?? new Date(),
151
+ computedTraits,
152
+ })
153
+ .returning()
154
+
155
+ return created ? mapDbProfile(created) : profile
156
+ }
157
+
158
+ async updateFacts(
159
+ containerTag: string,
160
+ staticFacts: ProfileFact[],
161
+ dynamicFacts: ProfileFact[]
162
+ ): Promise<UserProfile | null> {
163
+ await ensureContainerTag(containerTag)
164
+ const [existing] = await db.select().from(userProfiles).where(eq(userProfiles.containerTag, containerTag))
165
+
166
+ if (!existing) {
167
+ return null
168
+ }
169
+
170
+ const computedTraits = normalizeRecordObject(existing.computedTraits)
171
+ const nextVersion = getProfileVersion({ computedTraits }) + 1
172
+
173
+ const [updated] = await db
174
+ .update(userProfiles)
175
+ .set({
176
+ staticFacts,
177
+ dynamicFacts,
178
+ updatedAt: new Date(),
179
+ computedTraits: withProfileVersion(computedTraits, nextVersion),
180
+ })
181
+ .where(eq(userProfiles.containerTag, containerTag))
182
+ .returning()
183
+
184
+ return updated ? mapDbProfile(updated) : null
185
+ }
186
+
187
+ async delete(containerTag: string): Promise<boolean> {
188
+ const deleted = await db
189
+ .delete(userProfiles)
190
+ .where(eq(userProfiles.containerTag, containerTag))
191
+ .returning({ containerTag: userProfiles.containerTag })
192
+
193
+ return deleted.length > 0
194
+ }
195
+
196
+ async listAll(): Promise<UserProfile[]> {
197
+ const records = await db.select().from(userProfiles)
198
+ return records.map(mapDbProfile)
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Profile Repository - Main interface for profile database operations
204
+ */
205
+ export class ProfileRepository {
206
+ private db: ProfileDatabase
207
+
208
+ constructor(database?: ProfileDatabase) {
209
+ this.db = database ?? new PostgresProfileDatabase()
210
+ }
211
+
212
+ /**
213
+ * Find a profile by container tag
214
+ */
215
+ async findByContainerTag(containerTag: string): Promise<UserProfile | null> {
216
+ return this.db.findByContainerTag(containerTag)
217
+ }
218
+
219
+ /**
220
+ * Create or update a profile
221
+ */
222
+ async upsert(profile: UserProfile): Promise<UserProfile> {
223
+ return this.db.upsert(profile)
224
+ }
225
+
226
+ /**
227
+ * Update facts for a profile
228
+ */
229
+ async updateFacts(
230
+ containerTag: string,
231
+ staticFacts: ProfileFact[],
232
+ dynamicFacts: ProfileFact[]
233
+ ): Promise<UserProfile | null> {
234
+ return this.db.updateFacts(containerTag, staticFacts, dynamicFacts)
235
+ }
236
+
237
+ /**
238
+ * Delete a profile
239
+ */
240
+ async delete(containerTag: string): Promise<boolean> {
241
+ return this.db.delete(containerTag)
242
+ }
243
+
244
+ /**
245
+ * List all profiles
246
+ */
247
+ async listAll(): Promise<UserProfile[]> {
248
+ return this.db.listAll()
249
+ }
250
+
251
+ /**
252
+ * Add a single fact to a profile
253
+ */
254
+ async addFact(containerTag: string, fact: ProfileFact): Promise<UserProfile | null> {
255
+ const profile = await this.findByContainerTag(containerTag)
256
+ if (!profile) {
257
+ return null
258
+ }
259
+
260
+ const targetArray = fact.type === 'static' ? 'staticFacts' : 'dynamicFacts'
261
+ const updatedFacts = [...profile[targetArray], fact]
262
+
263
+ // Enforce max dynamic facts limit
264
+ let finalDynamicFacts = profile.dynamicFacts
265
+ let finalStaticFacts = profile.staticFacts
266
+
267
+ if (fact.type === 'dynamic') {
268
+ finalDynamicFacts = updatedFacts
269
+ if (finalDynamicFacts.length > PROFILE_DEFAULTS.maxDynamicFacts) {
270
+ // Remove oldest expired or least recently accessed
271
+ finalDynamicFacts = this.pruneExcessDynamicFacts(finalDynamicFacts)
272
+ }
273
+ } else {
274
+ finalStaticFacts = updatedFacts
275
+ }
276
+
277
+ return this.updateFacts(containerTag, finalStaticFacts, finalDynamicFacts)
278
+ }
279
+
280
+ /**
281
+ * Remove expired dynamic facts from a profile
282
+ */
283
+ async removeExpiredFacts(containerTag: string): Promise<UserProfile | null> {
284
+ const profile = await this.findByContainerTag(containerTag)
285
+ if (!profile) {
286
+ return null
287
+ }
288
+
289
+ const now = new Date()
290
+ const validDynamicFacts = profile.dynamicFacts.filter(
291
+ (fact: ProfileFact) => !fact.expiresAt || fact.expiresAt > now
292
+ )
293
+
294
+ if (validDynamicFacts.length === profile.dynamicFacts.length) {
295
+ return profile // No changes needed
296
+ }
297
+
298
+ return this.updateFacts(containerTag, profile.staticFacts, validDynamicFacts)
299
+ }
300
+
301
+ /**
302
+ * Find facts by content similarity (simple substring match)
303
+ * Replace with vector similarity for production
304
+ */
305
+ async findSimilarFacts(containerTag: string, content: string, type?: FactType): Promise<ProfileFact[]> {
306
+ const profile = await this.findByContainerTag(containerTag)
307
+ if (!profile) {
308
+ return []
309
+ }
310
+
311
+ const searchTerms = content.toLowerCase().split(/\s+/)
312
+ const allFacts =
313
+ type === 'static'
314
+ ? profile.staticFacts
315
+ : type === 'dynamic'
316
+ ? profile.dynamicFacts
317
+ : [...profile.staticFacts, ...profile.dynamicFacts]
318
+
319
+ return allFacts.filter((fact: ProfileFact) => {
320
+ const factWords = fact.content.toLowerCase()
321
+ return searchTerms.some((term) => factWords.includes(term))
322
+ })
323
+ }
324
+
325
+ /**
326
+ * Reinforce a fact (increment count and update access time)
327
+ */
328
+ async reinforceFact(containerTag: string, factId: string): Promise<ProfileFact | null> {
329
+ const profile = await this.findByContainerTag(containerTag)
330
+ if (!profile) {
331
+ return null
332
+ }
333
+
334
+ const updateFact = (facts: ProfileFact[]): ProfileFact[] => {
335
+ return facts.map((fact) => {
336
+ if (fact.id === factId) {
337
+ return {
338
+ ...fact,
339
+ reinforcementCount: fact.reinforcementCount + 1,
340
+ lastAccessedAt: new Date(),
341
+ }
342
+ }
343
+ return fact
344
+ })
345
+ }
346
+
347
+ const staticFact = profile.staticFacts.find((f: ProfileFact) => f.id === factId)
348
+ const dynamicFact = profile.dynamicFacts.find((f: ProfileFact) => f.id === factId)
349
+
350
+ if (staticFact) {
351
+ await this.updateFacts(containerTag, updateFact(profile.staticFacts), profile.dynamicFacts)
352
+ return { ...staticFact, reinforcementCount: staticFact.reinforcementCount + 1 }
353
+ }
354
+
355
+ if (dynamicFact) {
356
+ await this.updateFacts(containerTag, profile.staticFacts, updateFact(profile.dynamicFacts))
357
+ return { ...dynamicFact, reinforcementCount: dynamicFact.reinforcementCount + 1 }
358
+ }
359
+
360
+ return null
361
+ }
362
+
363
+ /**
364
+ * Promote a dynamic fact to static
365
+ */
366
+ async promoteFact(containerTag: string, factId: string): Promise<ProfileFact | null> {
367
+ const profile = await this.findByContainerTag(containerTag)
368
+ if (!profile) {
369
+ return null
370
+ }
371
+
372
+ const factIndex = profile.dynamicFacts.findIndex((f: ProfileFact) => f.id === factId)
373
+ if (factIndex === -1) {
374
+ return null
375
+ }
376
+
377
+ const fact = profile.dynamicFacts[factIndex]
378
+ if (!fact) {
379
+ return null
380
+ }
381
+
382
+ const promotedFact: ProfileFact = {
383
+ ...fact,
384
+ type: 'static',
385
+ expiresAt: undefined,
386
+ }
387
+
388
+ const newDynamicFacts = [...profile.dynamicFacts.slice(0, factIndex), ...profile.dynamicFacts.slice(factIndex + 1)]
389
+ const newStaticFacts = [...profile.staticFacts, promotedFact]
390
+
391
+ await this.updateFacts(containerTag, newStaticFacts, newDynamicFacts)
392
+ return promotedFact
393
+ }
394
+
395
+ /**
396
+ * Prune excess dynamic facts, keeping most relevant ones
397
+ */
398
+ private pruneExcessDynamicFacts(facts: ProfileFact[]): ProfileFact[] {
399
+ const now = new Date()
400
+
401
+ // First remove expired facts
402
+ const validFacts = facts.filter((fact) => !fact.expiresAt || fact.expiresAt > now)
403
+
404
+ if (validFacts.length <= PROFILE_DEFAULTS.maxDynamicFacts) {
405
+ return validFacts
406
+ }
407
+
408
+ // Sort by relevance score (higher is better)
409
+ validFacts.sort((a, b) => {
410
+ const scoreA = this.calculateRelevanceScore(a)
411
+ const scoreB = this.calculateRelevanceScore(b)
412
+ return scoreB - scoreA
413
+ })
414
+
415
+ return validFacts.slice(0, PROFILE_DEFAULTS.maxDynamicFacts)
416
+ }
417
+
418
+ /**
419
+ * Calculate relevance score for a fact
420
+ */
421
+ private calculateRelevanceScore(fact: ProfileFact): number {
422
+ const now = new Date()
423
+ const ageHours = (now.getTime() - fact.extractedAt.getTime()) / (1000 * 60 * 60)
424
+ const recencyScore = Math.max(0, 1 - ageHours / 168) // Decay over 1 week
425
+
426
+ const accessRecency = (now.getTime() - fact.lastAccessedAt.getTime()) / (1000 * 60 * 60)
427
+ const accessScore = Math.max(0, 1 - accessRecency / 168)
428
+
429
+ const reinforcementScore = Math.min(1, fact.reinforcementCount / 10)
430
+
431
+ return fact.confidence * 0.3 + recencyScore * 0.3 + accessScore * 0.2 + reinforcementScore * 0.2
432
+ }
433
+ }
434
+
435
+ // Export singleton instance
436
+ export const profileRepository = new ProfileRepository()