@soleri/core 8.0.0 → 9.0.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 (239) hide show
  1. package/dist/brain/brain.d.ts +1 -8
  2. package/dist/brain/brain.d.ts.map +1 -1
  3. package/dist/brain/brain.js +5 -134
  4. package/dist/brain/brain.js.map +1 -1
  5. package/dist/brain/knowledge-synthesizer.d.ts.map +1 -1
  6. package/dist/brain/knowledge-synthesizer.js +0 -2
  7. package/dist/brain/knowledge-synthesizer.js.map +1 -1
  8. package/dist/cognee/client.d.ts +5 -0
  9. package/dist/cognee/client.d.ts.map +1 -1
  10. package/dist/cognee/client.js +83 -16
  11. package/dist/cognee/client.js.map +1 -1
  12. package/dist/cognee/sync-manager.d.ts +67 -8
  13. package/dist/cognee/sync-manager.d.ts.map +1 -1
  14. package/dist/cognee/sync-manager.js +129 -32
  15. package/dist/cognee/sync-manager.js.map +1 -1
  16. package/dist/cognee/types.d.ts +16 -0
  17. package/dist/cognee/types.d.ts.map +1 -1
  18. package/dist/context/context-engine.d.ts +2 -5
  19. package/dist/context/context-engine.d.ts.map +1 -1
  20. package/dist/context/context-engine.js +4 -31
  21. package/dist/context/context-engine.js.map +1 -1
  22. package/dist/curator/classifier.d.ts.map +1 -1
  23. package/dist/curator/classifier.js +0 -2
  24. package/dist/curator/classifier.js.map +1 -1
  25. package/dist/curator/curator.d.ts +2 -5
  26. package/dist/curator/curator.d.ts.map +1 -1
  27. package/dist/curator/curator.js +4 -23
  28. package/dist/curator/curator.js.map +1 -1
  29. package/dist/curator/quality-gate.d.ts.map +1 -1
  30. package/dist/curator/quality-gate.js +0 -2
  31. package/dist/curator/quality-gate.js.map +1 -1
  32. package/dist/domain-packs/index.d.ts +0 -3
  33. package/dist/domain-packs/index.d.ts.map +1 -1
  34. package/dist/domain-packs/index.js +0 -3
  35. package/dist/domain-packs/index.js.map +1 -1
  36. package/dist/domain-packs/loader.d.ts.map +1 -1
  37. package/dist/domain-packs/loader.js +20 -4
  38. package/dist/domain-packs/loader.js.map +1 -1
  39. package/dist/domain-packs/pack-runtime.d.ts +5 -5
  40. package/dist/domain-packs/pack-runtime.d.ts.map +1 -1
  41. package/dist/domain-packs/pack-runtime.js +2 -2
  42. package/dist/domain-packs/pack-runtime.js.map +1 -1
  43. package/dist/domain-packs/types.d.ts +8 -2
  44. package/dist/domain-packs/types.d.ts.map +1 -1
  45. package/dist/domain-packs/types.js.map +1 -1
  46. package/dist/engine/bin/soleri-engine.js +18 -7
  47. package/dist/engine/bin/soleri-engine.js.map +1 -1
  48. package/dist/engine/core-ops.d.ts.map +1 -1
  49. package/dist/engine/core-ops.js +11 -6
  50. package/dist/engine/core-ops.js.map +1 -1
  51. package/dist/engine/index.d.ts +2 -0
  52. package/dist/engine/index.d.ts.map +1 -1
  53. package/dist/engine/index.js +1 -0
  54. package/dist/engine/index.js.map +1 -1
  55. package/dist/engine/module-manifest.d.ts +28 -0
  56. package/dist/engine/module-manifest.d.ts.map +1 -0
  57. package/dist/engine/module-manifest.js +85 -0
  58. package/dist/engine/module-manifest.js.map +1 -0
  59. package/dist/engine/register-engine.d.ts +19 -0
  60. package/dist/engine/register-engine.d.ts.map +1 -1
  61. package/dist/engine/register-engine.js +15 -9
  62. package/dist/engine/register-engine.js.map +1 -1
  63. package/dist/index.d.ts +5 -6
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +3 -5
  66. package/dist/index.js.map +1 -1
  67. package/dist/intake/content-classifier.d.ts.map +1 -1
  68. package/dist/intake/content-classifier.js +0 -2
  69. package/dist/intake/content-classifier.js.map +1 -1
  70. package/dist/intelligence/types.d.ts +7 -0
  71. package/dist/intelligence/types.d.ts.map +1 -1
  72. package/dist/llm/llm-client.d.ts.map +1 -1
  73. package/dist/llm/llm-client.js +8 -4
  74. package/dist/llm/llm-client.js.map +1 -1
  75. package/dist/llm/oauth-discovery.d.ts +0 -8
  76. package/dist/llm/oauth-discovery.d.ts.map +1 -1
  77. package/dist/llm/oauth-discovery.js +0 -19
  78. package/dist/llm/oauth-discovery.js.map +1 -1
  79. package/dist/llm/types.d.ts +4 -2
  80. package/dist/llm/types.d.ts.map +1 -1
  81. package/dist/packs/pack-installer.d.ts +2 -1
  82. package/dist/packs/pack-installer.d.ts.map +1 -1
  83. package/dist/packs/pack-installer.js +10 -1
  84. package/dist/packs/pack-installer.js.map +1 -1
  85. package/dist/persistence/index.d.ts +0 -1
  86. package/dist/persistence/index.d.ts.map +1 -1
  87. package/dist/persistence/index.js +0 -1
  88. package/dist/persistence/index.js.map +1 -1
  89. package/dist/persistence/types.d.ts +2 -6
  90. package/dist/persistence/types.d.ts.map +1 -1
  91. package/dist/persona/defaults.d.ts +16 -0
  92. package/dist/persona/defaults.d.ts.map +1 -0
  93. package/dist/persona/defaults.js +78 -0
  94. package/dist/persona/defaults.js.map +1 -0
  95. package/dist/persona/index.d.ts +5 -0
  96. package/dist/persona/index.d.ts.map +1 -0
  97. package/dist/persona/index.js +4 -0
  98. package/dist/persona/index.js.map +1 -0
  99. package/dist/persona/loader.d.ts +11 -0
  100. package/dist/persona/loader.d.ts.map +1 -0
  101. package/dist/persona/loader.js +45 -0
  102. package/dist/persona/loader.js.map +1 -0
  103. package/dist/persona/prompt-generator.d.ts +13 -0
  104. package/dist/persona/prompt-generator.d.ts.map +1 -0
  105. package/dist/persona/prompt-generator.js +89 -0
  106. package/dist/persona/prompt-generator.js.map +1 -0
  107. package/dist/persona/types.d.ts +56 -0
  108. package/dist/persona/types.d.ts.map +1 -0
  109. package/dist/persona/types.js +9 -0
  110. package/dist/persona/types.js.map +1 -0
  111. package/dist/plugins/index.d.ts +4 -0
  112. package/dist/plugins/index.d.ts.map +1 -1
  113. package/dist/plugins/index.js +4 -0
  114. package/dist/plugins/index.js.map +1 -1
  115. package/dist/plugins/plugin-registry.d.ts +4 -0
  116. package/dist/plugins/plugin-registry.d.ts.map +1 -1
  117. package/dist/plugins/plugin-registry.js +4 -0
  118. package/dist/plugins/plugin-registry.js.map +1 -1
  119. package/dist/plugins/types.d.ts +36 -31
  120. package/dist/plugins/types.d.ts.map +1 -1
  121. package/dist/plugins/types.js +6 -3
  122. package/dist/plugins/types.js.map +1 -1
  123. package/dist/runtime/admin-extra-ops.d.ts.map +1 -1
  124. package/dist/runtime/admin-extra-ops.js +5 -27
  125. package/dist/runtime/admin-extra-ops.js.map +1 -1
  126. package/dist/runtime/admin-ops.d.ts.map +1 -1
  127. package/dist/runtime/admin-ops.js +5 -37
  128. package/dist/runtime/admin-ops.js.map +1 -1
  129. package/dist/runtime/capture-ops.d.ts.map +1 -1
  130. package/dist/runtime/capture-ops.js +32 -16
  131. package/dist/runtime/capture-ops.js.map +1 -1
  132. package/dist/runtime/claude-md-helpers.d.ts +0 -9
  133. package/dist/runtime/claude-md-helpers.d.ts.map +1 -1
  134. package/dist/runtime/claude-md-helpers.js +1 -14
  135. package/dist/runtime/claude-md-helpers.js.map +1 -1
  136. package/dist/runtime/cognee-sync-ops.d.ts +2 -2
  137. package/dist/runtime/cognee-sync-ops.d.ts.map +1 -1
  138. package/dist/runtime/cognee-sync-ops.js +45 -7
  139. package/dist/runtime/cognee-sync-ops.js.map +1 -1
  140. package/dist/runtime/facades/admin-facade.d.ts.map +1 -1
  141. package/dist/runtime/facades/admin-facade.js +1 -2
  142. package/dist/runtime/facades/admin-facade.js.map +1 -1
  143. package/dist/runtime/facades/index.d.ts +1 -1
  144. package/dist/runtime/facades/index.d.ts.map +1 -1
  145. package/dist/runtime/facades/index.js +1 -10
  146. package/dist/runtime/facades/index.js.map +1 -1
  147. package/dist/runtime/pack-ops.d.ts +3 -0
  148. package/dist/runtime/pack-ops.d.ts.map +1 -1
  149. package/dist/runtime/pack-ops.js +18 -1
  150. package/dist/runtime/pack-ops.js.map +1 -1
  151. package/dist/runtime/plugin-ops.d.ts.map +1 -1
  152. package/dist/runtime/plugin-ops.js +3 -0
  153. package/dist/runtime/plugin-ops.js.map +1 -1
  154. package/dist/runtime/runtime.d.ts.map +1 -1
  155. package/dist/runtime/runtime.js +14 -53
  156. package/dist/runtime/runtime.js.map +1 -1
  157. package/dist/runtime/session-briefing.d.ts.map +1 -1
  158. package/dist/runtime/session-briefing.js +14 -0
  159. package/dist/runtime/session-briefing.js.map +1 -1
  160. package/dist/runtime/types.d.ts +6 -8
  161. package/dist/runtime/types.d.ts.map +1 -1
  162. package/dist/runtime/vault-linking-ops.d.ts.map +1 -1
  163. package/dist/runtime/vault-linking-ops.js +42 -4
  164. package/dist/runtime/vault-linking-ops.js.map +1 -1
  165. package/dist/runtime/vault-sharing-ops.d.ts.map +1 -1
  166. package/dist/runtime/vault-sharing-ops.js +53 -3
  167. package/dist/runtime/vault-sharing-ops.js.map +1 -1
  168. package/dist/vault/linking.d.ts +37 -0
  169. package/dist/vault/linking.d.ts.map +1 -1
  170. package/dist/vault/linking.js +73 -0
  171. package/dist/vault/linking.js.map +1 -1
  172. package/dist/vault/vault.d.ts +9 -2
  173. package/dist/vault/vault.d.ts.map +1 -1
  174. package/dist/vault/vault.js +21 -12
  175. package/dist/vault/vault.js.map +1 -1
  176. package/package.json +6 -4
  177. package/src/__tests__/curator-pipeline-e2e.test.ts +187 -0
  178. package/src/__tests__/module-manifest-drift.test.ts +59 -0
  179. package/src/brain/brain.ts +4 -157
  180. package/src/brain/knowledge-synthesizer.ts +0 -2
  181. package/src/context/context-engine.ts +3 -31
  182. package/src/curator/classifier.ts +0 -2
  183. package/src/curator/curator.ts +5 -28
  184. package/src/curator/quality-gate.ts +0 -2
  185. package/src/domain-packs/index.ts +0 -6
  186. package/src/domain-packs/loader.ts +25 -5
  187. package/src/domain-packs/pack-runtime.ts +6 -6
  188. package/src/domain-packs/types.ts +8 -2
  189. package/src/engine/bin/soleri-engine.ts +23 -7
  190. package/src/engine/core-ops.ts +11 -6
  191. package/src/engine/index.ts +2 -0
  192. package/src/engine/module-manifest.ts +99 -0
  193. package/src/engine/register-engine.ts +21 -9
  194. package/src/index.ts +20 -17
  195. package/src/intake/content-classifier.ts +0 -2
  196. package/src/intelligence/types.ts +8 -0
  197. package/src/llm/llm-client.ts +12 -6
  198. package/src/llm/oauth-discovery.ts +0 -18
  199. package/src/llm/types.ts +4 -2
  200. package/src/packs/pack-installer.ts +16 -1
  201. package/src/persistence/index.ts +0 -1
  202. package/src/persistence/types.ts +2 -6
  203. package/src/persona/defaults.ts +96 -0
  204. package/src/persona/index.ts +9 -0
  205. package/src/persona/loader.ts +50 -0
  206. package/src/persona/prompt-generator.ts +109 -0
  207. package/src/persona/types.ts +72 -0
  208. package/src/plugins/index.ts +4 -0
  209. package/src/plugins/plugin-registry.ts +6 -1
  210. package/src/plugins/types.ts +10 -5
  211. package/src/runtime/admin-extra-ops.ts +5 -28
  212. package/src/runtime/admin-ops.ts +5 -38
  213. package/src/runtime/capture-ops.ts +33 -14
  214. package/src/runtime/claude-md-helpers.ts +1 -19
  215. package/src/runtime/facades/admin-facade.ts +1 -2
  216. package/src/runtime/facades/index.ts +1 -11
  217. package/src/runtime/pack-ops.ts +26 -1
  218. package/src/runtime/plugin-ops.ts +3 -0
  219. package/src/runtime/runtime.ts +14 -54
  220. package/src/runtime/session-briefing.ts +14 -0
  221. package/src/runtime/types.ts +6 -8
  222. package/src/runtime/vault-linking-ops.ts +43 -4
  223. package/src/runtime/vault-sharing-ops.ts +63 -4
  224. package/src/vault/linking.ts +94 -0
  225. package/src/vault/vault.ts +24 -12
  226. package/src/__tests__/cognee-client-gaps.test.ts +0 -474
  227. package/src/__tests__/cognee-client.test.ts +0 -524
  228. package/src/__tests__/cognee-hybrid-search.test.ts +0 -492
  229. package/src/__tests__/cognee-integration.test.ts +0 -80
  230. package/src/__tests__/cognee-sync-manager-deep.test.ts +0 -654
  231. package/src/__tests__/cognee-sync-manager.test.ts +0 -104
  232. package/src/__tests__/postgres-provider.test.ts +0 -116
  233. package/src/cognee/client.ts +0 -370
  234. package/src/cognee/sync-manager.ts +0 -389
  235. package/src/cognee/types.ts +0 -62
  236. package/src/health/doctor-checks.ts +0 -115
  237. package/src/persistence/postgres-provider.ts +0 -310
  238. package/src/runtime/cognee-sync-ops.ts +0 -63
  239. package/src/runtime/facades/cognee-facade.ts +0 -164
@@ -169,6 +169,28 @@ export class LinkManager {
169
169
  return result;
170
170
  }
171
171
 
172
+ // ===========================================================================
173
+ // BULK QUERIES
174
+ // ===========================================================================
175
+
176
+ /**
177
+ * Get all links where either source or target is in the given ID set.
178
+ * Used for pack export — to find links within an export set.
179
+ */
180
+ getAllLinksForEntries(entryIds: string[]): VaultLink[] {
181
+ if (entryIds.length === 0) return [];
182
+ try {
183
+ const placeholders = entryIds.map(() => '?').join(',');
184
+ const rows = this.provider.all<VaultLinkRow>(
185
+ `SELECT * FROM vault_links WHERE source_id IN (${placeholders}) OR target_id IN (${placeholders})`,
186
+ [...entryIds, ...entryIds],
187
+ );
188
+ return rows.map(rowToVaultLink);
189
+ } catch {
190
+ return [];
191
+ }
192
+ }
193
+
172
194
  // ===========================================================================
173
195
  // ORPHAN DETECTION
174
196
  // ===========================================================================
@@ -292,6 +314,78 @@ export class LinkManager {
292
314
  }
293
315
  }
294
316
 
317
+ // ===========================================================================
318
+ // BACKFILL — one-time link generation for existing entries
319
+ // ===========================================================================
320
+
321
+ /**
322
+ * Generate links for orphan entries using FTS5 suggestions.
323
+ * Processes orphans in batches and creates links above the threshold.
324
+ *
325
+ * @param opts.threshold Minimum suggestion score to auto-create link (default: 0.7)
326
+ * @param opts.maxLinks Max links per entry (default: 3)
327
+ * @param opts.dryRun Preview without creating links (default: false)
328
+ * @param opts.batchSize Entries per batch (default: 50)
329
+ * @param opts.onProgress Progress callback
330
+ * @returns Stats: processed, linksCreated, durationMs
331
+ */
332
+ backfillLinks(opts?: {
333
+ threshold?: number;
334
+ maxLinks?: number;
335
+ dryRun?: boolean;
336
+ batchSize?: number;
337
+ onProgress?: (stats: { processed: number; total: number; linksCreated: number }) => void;
338
+ }): {
339
+ processed: number;
340
+ linksCreated: number;
341
+ durationMs: number;
342
+ preview?: Array<{ sourceId: string; targetId: string; linkType: string; score: number }>;
343
+ } {
344
+ const threshold = opts?.threshold ?? 0.7;
345
+ const maxLinks = opts?.maxLinks ?? 3;
346
+ const dryRun = opts?.dryRun ?? false;
347
+ const batchSize = opts?.batchSize ?? 50;
348
+ const start = Date.now();
349
+
350
+ const orphans = this.getOrphans(10000);
351
+ let processed = 0;
352
+ let linksCreated = 0;
353
+ const preview: Array<{ sourceId: string; targetId: string; linkType: string; score: number }> =
354
+ [];
355
+
356
+ for (let i = 0; i < orphans.length; i += batchSize) {
357
+ const batch = orphans.slice(i, i + batchSize);
358
+ for (const entry of batch) {
359
+ const suggestions = this.suggestLinks(entry.id, maxLinks + 2);
360
+ const qualifying = suggestions.filter((s) => s.score >= threshold).slice(0, maxLinks);
361
+
362
+ for (const s of qualifying) {
363
+ if (dryRun) {
364
+ preview.push({
365
+ sourceId: entry.id,
366
+ targetId: s.entryId,
367
+ linkType: s.suggestedType,
368
+ score: s.score,
369
+ });
370
+ } else {
371
+ this.addLink(entry.id, s.entryId, s.suggestedType);
372
+ }
373
+ linksCreated++;
374
+ }
375
+ processed++;
376
+ }
377
+
378
+ opts?.onProgress?.({ processed, total: orphans.length, linksCreated });
379
+ }
380
+
381
+ return {
382
+ processed,
383
+ linksCreated,
384
+ durationMs: Date.now() - start,
385
+ ...(dryRun ? { preview } : {}),
386
+ };
387
+ }
388
+
295
389
  // ===========================================================================
296
390
  // PRIVATE
297
391
  // ===========================================================================
@@ -52,7 +52,6 @@ export interface MemoryStats {
52
52
  export class Vault {
53
53
  private provider: PersistenceProvider;
54
54
  private sqliteProvider: SQLitePersistenceProvider | null;
55
- private syncManager: import('../cognee/sync-manager.js').CogneeSyncManager | null = null;
56
55
  private linkManager: LinkManager | null = null;
57
56
  private autoLinkEnabled = true;
58
57
  /** Minimum number of FTS5 suggestions to auto-link. Top N are linked. */
@@ -76,10 +75,32 @@ export class Vault {
76
75
  providerOrPath instanceof SQLitePersistenceProvider ? providerOrPath : null;
77
76
  }
78
77
  this.initialize();
78
+ this.checkFormatVersion();
79
79
  }
80
80
 
81
- setSyncManager(mgr: import('../cognee/sync-manager.js').CogneeSyncManager): void {
82
- this.syncManager = mgr;
81
+ /**
82
+ * Vault format version — tracks schema changes for migration safety.
83
+ * Increment when schema changes in a way that requires migration.
84
+ *
85
+ * History:
86
+ * 1 — Initial schema (v8.0.0): entries, memories, brain_feedback, FTS5
87
+ */
88
+ static readonly FORMAT_VERSION = 1;
89
+
90
+ private checkFormatVersion(): void {
91
+ const row = this.provider.get<{ user_version: number }>('PRAGMA user_version');
92
+ const current = row?.user_version ?? 0;
93
+
94
+ if (current === 0) {
95
+ // Fresh database — stamp with current version
96
+ this.provider.run(`PRAGMA user_version = ${Vault.FORMAT_VERSION}`);
97
+ } else if (current > Vault.FORMAT_VERSION) {
98
+ throw new Error(
99
+ `Vault format version ${current} is newer than engine supports (${Vault.FORMAT_VERSION}). ` +
100
+ `Upgrade @soleri/core to a compatible version.`,
101
+ );
102
+ }
103
+ // current < FORMAT_VERSION → future: run migration scripts here
83
104
  }
84
105
 
85
106
  setLinkManager(mgr: LinkManager, opts?: { enabled?: boolean; maxLinks?: number }): void {
@@ -388,9 +409,6 @@ export class Vault {
388
409
  origin: entry.origin ?? 'agent',
389
410
  });
390
411
  count++;
391
- if (this.syncManager) {
392
- this.syncManager.enqueue('ingest', entry.id, entry);
393
- }
394
412
  }
395
413
  // Auto-link after all entries are inserted (so they can link to each other).
396
414
  // Skip for large batches (>100) — use relink_vault for bulk imports.
@@ -582,9 +600,6 @@ export class Vault {
582
600
  }
583
601
  remove(id: string): boolean {
584
602
  const deleted = this.provider.run('DELETE FROM entries WHERE id = ?', [id]).changes > 0;
585
- if (deleted && this.syncManager) {
586
- this.syncManager.enqueue('delete', id);
587
- }
588
603
  return deleted;
589
604
  }
590
605
 
@@ -658,9 +673,6 @@ export class Vault {
658
673
  let count = 0;
659
674
  for (const id of ids) {
660
675
  count += this.provider.run('DELETE FROM entries WHERE id = ?', [id]).changes;
661
- if (this.syncManager) {
662
- this.syncManager.enqueue('delete', id);
663
- }
664
676
  }
665
677
  return count;
666
678
  });
@@ -1,474 +0,0 @@
1
- /**
2
- * CogneeClient gap tests — covers behaviors missing from the original test suite.
3
- *
4
- * Source of truth: these tests define expected behavior.
5
- * Code adapts to fulfill them.
6
- */
7
-
8
- import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest';
9
- import { CogneeClient } from '../cognee/client.js';
10
- import type { IntelligenceEntry } from '../intelligence/types.js';
11
-
12
- // ─── Helpers ──────────────────────────────────────────────────────
13
-
14
- function makeEntry(overrides: Partial<IntelligenceEntry> = {}): IntelligenceEntry {
15
- return {
16
- id: overrides.id ?? 'test-1',
17
- type: overrides.type ?? 'pattern',
18
- domain: overrides.domain ?? 'testing',
19
- title: overrides.title ?? 'Test Pattern',
20
- severity: overrides.severity ?? 'warning',
21
- description: overrides.description ?? 'A test pattern for unit tests.',
22
- tags: overrides.tags ?? ['testing', 'assertions'],
23
- ...(overrides.context !== undefined ? { context: overrides.context } : {}),
24
- ...(overrides.example !== undefined ? { example: overrides.example } : {}),
25
- ...(overrides.why !== undefined ? { why: overrides.why } : {}),
26
- ...(overrides.counterExample !== undefined ? { counterExample: overrides.counterExample } : {}),
27
- };
28
- }
29
-
30
- function isHealthCheck(url: string, init?: RequestInit): boolean {
31
- return url.endsWith(':8000/') || (url.endsWith('/') && (!init?.method || init.method === 'GET'));
32
- }
33
-
34
- function isAuthCall(url: string): boolean {
35
- return url.includes('/api/v1/auth/');
36
- }
37
-
38
- function mockWithAuth(apiHandler?: (url: string, init?: RequestInit) => Promise<Response>) {
39
- const mock = vi.fn(async (url: string, init?: RequestInit) => {
40
- if (isHealthCheck(url, init)) return new Response('ok', { status: 200 });
41
- if (isAuthCall(url)) {
42
- if (url.includes('/login')) {
43
- return new Response(JSON.stringify({ access_token: 'test-jwt' }), { status: 200 });
44
- }
45
- if (url.includes('/register')) {
46
- return new Response(JSON.stringify({ id: 'new-user' }), { status: 200 });
47
- }
48
- }
49
- if (apiHandler) return apiHandler(url, init);
50
- return new Response('ok', { status: 200 });
51
- });
52
- vi.stubGlobal('fetch', mock);
53
- return mock;
54
- }
55
-
56
- // ─── Tests ────────────────────────────────────────────────────────
57
-
58
- describe('CogneeClient — gap coverage', () => {
59
- afterEach(() => {
60
- vi.restoreAllMocks();
61
- });
62
-
63
- // ─── deleteEntries ────────────────────────────────────────────
64
-
65
- describe('deleteEntries', () => {
66
- it('should call POST /api/v1/delete with dataset and entryIds', async () => {
67
- let capturedBody = '';
68
- mockWithAuth(async (_url, init) => {
69
- capturedBody = init?.body as string;
70
- return new Response('ok', { status: 200 });
71
- });
72
- const client = new CogneeClient({ dataset: 'my-ds' });
73
- await client.healthCheck();
74
- const result = await client.deleteEntries(['e1', 'e2']);
75
- expect(result.deleted).toBe(2);
76
- const parsed = JSON.parse(capturedBody);
77
- expect(parsed.datasetName).toBe('my-ds');
78
- expect(parsed.entryIds).toEqual(['e1', 'e2']);
79
- });
80
-
81
- it('should return 0 when not available', async () => {
82
- const client = new CogneeClient();
83
- const result = await client.deleteEntries(['e1']);
84
- expect(result.deleted).toBe(0);
85
- });
86
-
87
- it('should return 0 for empty entryIds', async () => {
88
- mockWithAuth();
89
- const client = new CogneeClient();
90
- await client.healthCheck();
91
- const result = await client.deleteEntries([]);
92
- expect(result.deleted).toBe(0);
93
- });
94
-
95
- it('should return 0 on HTTP error without throwing', async () => {
96
- mockWithAuth(async () => new Response('error', { status: 500 }));
97
- const client = new CogneeClient();
98
- await client.healthCheck();
99
- const result = await client.deleteEntries(['e1']);
100
- expect(result.deleted).toBe(0);
101
- });
102
-
103
- it('should return 0 on network error without throwing', async () => {
104
- mockWithAuth(async () => {
105
- throw new Error('ECONNRESET');
106
- });
107
- const client = new CogneeClient();
108
- await client.healthCheck();
109
- const result = await client.deleteEntries(['e1']);
110
- expect(result.deleted).toBe(0);
111
- });
112
- });
113
-
114
- // ─── Entry serialization ──────────────────────────────────────
115
-
116
- describe('entry serialization (via addEntries FormData)', () => {
117
- it('should include vault-id prefix in serialized text', async () => {
118
- let capturedText = '';
119
- mockWithAuth(async (_url, init) => {
120
- const body = init?.body as FormData;
121
- if (body instanceof FormData) {
122
- const file = body.get('data') as File;
123
- if (file) capturedText = await file.text();
124
- }
125
- return new Response('ok', { status: 200 });
126
- });
127
- const client = new CogneeClient();
128
- await client.healthCheck();
129
- await client.addEntries([makeEntry({ id: 'my-entry-42', title: 'My Title' })]);
130
- expect(capturedText).toContain('[vault-id:my-entry-42]');
131
- expect(capturedText).toContain('My Title');
132
- client.resetPendingCognify();
133
- });
134
-
135
- it('should include description in serialized text', async () => {
136
- let capturedText = '';
137
- mockWithAuth(async (_url, init) => {
138
- const body = init?.body as FormData;
139
- if (body instanceof FormData) {
140
- const file = body.get('data') as File;
141
- if (file) capturedText = await file.text();
142
- }
143
- return new Response('ok', { status: 200 });
144
- });
145
- const client = new CogneeClient();
146
- await client.healthCheck();
147
- await client.addEntries([makeEntry({ description: 'Unique description text here' })]);
148
- expect(capturedText).toContain('Unique description text here');
149
- client.resetPendingCognify();
150
- });
151
-
152
- it('should include context when present', async () => {
153
- let capturedText = '';
154
- mockWithAuth(async (_url, init) => {
155
- const body = init?.body as FormData;
156
- if (body instanceof FormData) {
157
- const file = body.get('data') as File;
158
- if (file) capturedText = await file.text();
159
- }
160
- return new Response('ok', { status: 200 });
161
- });
162
- const client = new CogneeClient();
163
- await client.healthCheck();
164
- await client.addEntries([makeEntry({ context: 'Apply in production code only' })]);
165
- expect(capturedText).toContain('Apply in production code only');
166
- client.resetPendingCognify();
167
- });
168
-
169
- it('should include tags when present', async () => {
170
- let capturedText = '';
171
- mockWithAuth(async (_url, init) => {
172
- const body = init?.body as FormData;
173
- if (body instanceof FormData) {
174
- const file = body.get('data') as File;
175
- if (file) capturedText = await file.text();
176
- }
177
- return new Response('ok', { status: 200 });
178
- });
179
- const client = new CogneeClient();
180
- await client.healthCheck();
181
- await client.addEntries([makeEntry({ tags: ['react', 'performance'] })]);
182
- expect(capturedText).toContain('Tags: react, performance');
183
- client.resetPendingCognify();
184
- });
185
-
186
- it('should not include Tags line when tags are empty', async () => {
187
- let capturedText = '';
188
- mockWithAuth(async (_url, init) => {
189
- const body = init?.body as FormData;
190
- if (body instanceof FormData) {
191
- const file = body.get('data') as File;
192
- if (file) capturedText = await file.text();
193
- }
194
- return new Response('ok', { status: 200 });
195
- });
196
- const client = new CogneeClient();
197
- await client.healthCheck();
198
- await client.addEntries([makeEntry({ tags: [] })]);
199
- expect(capturedText).not.toContain('Tags:');
200
- client.resetPendingCognify();
201
- });
202
-
203
- it('should use entry.id as filename', async () => {
204
- let capturedFilename = '';
205
- mockWithAuth(async (_url, init) => {
206
- const body = init?.body as FormData;
207
- if (body instanceof FormData) {
208
- const file = body.get('data') as File;
209
- if (file) capturedFilename = file.name;
210
- }
211
- return new Response('ok', { status: 200 });
212
- });
213
- const client = new CogneeClient();
214
- await client.healthCheck();
215
- await client.addEntries([makeEntry({ id: 'pattern-arch-123' })]);
216
- expect(capturedFilename).toBe('pattern-arch-123.txt');
217
- client.resetPendingCognify();
218
- });
219
- });
220
-
221
- // ─── Debounce sliding window ──────────────────────────────────
222
-
223
- describe('cognify debounce — sliding window', () => {
224
- beforeEach(() => {
225
- vi.useFakeTimers();
226
- });
227
-
228
- afterEach(() => {
229
- vi.useRealTimers();
230
- vi.restoreAllMocks();
231
- });
232
-
233
- it('should extend debounce window on rapid ingests (sliding, not fixed)', async () => {
234
- let cognifyCount = 0;
235
- mockWithAuth(async (url) => {
236
- if (url.includes('/cognify')) cognifyCount++;
237
- return new Response('ok', { status: 200 });
238
- });
239
- const client = new CogneeClient({ cognifyDebounceMs: 100 });
240
- await client.healthCheck();
241
-
242
- // First ingest at t=0
243
- await client.addEntries([makeEntry({ id: 'e1' })]);
244
- // Advance 80ms (within window), second ingest resets the timer
245
- await vi.advanceTimersByTimeAsync(80);
246
- await client.addEntries([makeEntry({ id: 'e2' })]);
247
- // Advance 80ms (within the RESET window) — cognify should NOT have fired yet
248
- await vi.advanceTimersByTimeAsync(80);
249
- expect(cognifyCount).toBe(0);
250
-
251
- // Advance past the reset window (another 30ms = 110ms from second ingest)
252
- await vi.advanceTimersByTimeAsync(30);
253
- expect(cognifyCount).toBe(1);
254
- client.resetPendingCognify();
255
- });
256
-
257
- it('should fire separate cognify for different datasets', async () => {
258
- const cognifyDatasets: string[][] = [];
259
- mockWithAuth(async (url, init) => {
260
- if (url.includes('/cognify')) {
261
- const body = JSON.parse(init?.body as string);
262
- cognifyDatasets.push(body.datasets);
263
- }
264
- return new Response('ok', { status: 200 });
265
- });
266
- // Two clients with different datasets
267
- const client1 = new CogneeClient({ dataset: 'ds-alpha', cognifyDebounceMs: 50 });
268
- const client2 = new CogneeClient({ dataset: 'ds-beta', cognifyDebounceMs: 50 });
269
- await client1.healthCheck();
270
- await client2.healthCheck();
271
-
272
- await client1.addEntries([makeEntry({ id: 'a1' })]);
273
- await client2.addEntries([makeEntry({ id: 'b1' })]);
274
-
275
- await vi.advanceTimersByTimeAsync(60);
276
-
277
- // Each dataset should cognify independently
278
- expect(cognifyDatasets).toHaveLength(2);
279
- const allDatasets = cognifyDatasets.flat();
280
- expect(allDatasets).toContain('ds-alpha');
281
- expect(allDatasets).toContain('ds-beta');
282
-
283
- client1.resetPendingCognify();
284
- client2.resetPendingCognify();
285
- });
286
-
287
- it('should coalesce multiple ingests to same dataset into one cognify', async () => {
288
- let cognifyCount = 0;
289
- mockWithAuth(async (url) => {
290
- if (url.includes('/cognify')) cognifyCount++;
291
- return new Response('ok', { status: 200 });
292
- });
293
- const client = new CogneeClient({ cognifyDebounceMs: 50 });
294
- await client.healthCheck();
295
-
296
- await client.addEntries([makeEntry({ id: 'e1' })]);
297
- await client.addEntries([makeEntry({ id: 'e2' })]);
298
- await client.addEntries([makeEntry({ id: 'e3' })]);
299
-
300
- await vi.advanceTimersByTimeAsync(60);
301
-
302
- expect(cognifyCount).toBe(1);
303
- client.resetPendingCognify();
304
- });
305
- });
306
-
307
- // ─── Concurrent operations ────────────────────────────────────
308
-
309
- describe('concurrent operations', () => {
310
- it('should handle parallel addEntries without interference', async () => {
311
- mockWithAuth();
312
- const client = new CogneeClient();
313
- await client.healthCheck();
314
-
315
- const results = await Promise.all([
316
- client.addEntries([makeEntry({ id: 'p1' })]),
317
- client.addEntries([makeEntry({ id: 'p2' })]),
318
- client.addEntries([makeEntry({ id: 'p3' })]),
319
- client.addEntries([makeEntry({ id: 'p4' })]),
320
- client.addEntries([makeEntry({ id: 'p5' })]),
321
- ]);
322
-
323
- expect(results.every((r) => r.added === 1)).toBe(true);
324
- client.resetPendingCognify();
325
- });
326
-
327
- it('should handle parallel search calls', async () => {
328
- mockWithAuth(
329
- async () =>
330
- new Response(JSON.stringify([{ id: 'r1', text: 'Result', score: 0.9 }]), { status: 200 }),
331
- );
332
- const client = new CogneeClient();
333
- await client.healthCheck();
334
-
335
- const results = await Promise.all([
336
- client.search('query 1'),
337
- client.search('query 2'),
338
- client.search('query 3'),
339
- ]);
340
-
341
- expect(results.every((r) => r.length === 1)).toBe(true);
342
- });
343
- });
344
-
345
- // ─── Search edge cases ────────────────────────────────────────
346
-
347
- describe('search edge cases', () => {
348
- it('should handle empty string query gracefully', async () => {
349
- mockWithAuth(async () => new Response(JSON.stringify([]), { status: 200 }));
350
- const client = new CogneeClient();
351
- await client.healthCheck();
352
- const results = await client.search('');
353
- expect(Array.isArray(results)).toBe(true);
354
- });
355
-
356
- it('should handle results with non-string text field', async () => {
357
- mockWithAuth(
358
- async () =>
359
- new Response(
360
- JSON.stringify([
361
- { id: 'r1', text: 42, score: 0.8 },
362
- { id: 'r2', score: 0.7 },
363
- ]),
364
- { status: 200 },
365
- ),
366
- );
367
- const client = new CogneeClient();
368
- await client.healthCheck();
369
- const results = await client.search('query');
370
- // text should be coerced to string
371
- expect(typeof results[0].text).toBe('string');
372
- expect(typeof results[1].text).toBe('string');
373
- });
374
-
375
- it('should handle null/undefined text field', async () => {
376
- mockWithAuth(
377
- async () =>
378
- new Response(JSON.stringify([{ id: 'r1', text: null, score: 0.5 }]), { status: 200 }),
379
- );
380
- const client = new CogneeClient();
381
- await client.healthCheck();
382
- const results = await client.search('query');
383
- expect(typeof results[0].text).toBe('string');
384
- });
385
- });
386
-
387
- // ─── Auth edge cases ──────────────────────────────────────────
388
-
389
- describe('auth edge cases', () => {
390
- it('should cache auth token across multiple API calls', async () => {
391
- let loginCount = 0;
392
- const mock = vi.fn(async (url: string, init?: RequestInit) => {
393
- if (isHealthCheck(url, init)) return new Response('ok', { status: 200 });
394
- if (url.includes('/auth/login')) {
395
- loginCount++;
396
- return new Response(JSON.stringify({ access_token: 'cached-jwt' }), { status: 200 });
397
- }
398
- return new Response(JSON.stringify([]), { status: 200 });
399
- });
400
- vi.stubGlobal('fetch', mock);
401
-
402
- const client = new CogneeClient();
403
- await client.healthCheck();
404
-
405
- // Multiple API calls should reuse the same token
406
- await client.search('q1');
407
- await client.search('q2');
408
- await client.cognify();
409
-
410
- expect(loginCount).toBe(1);
411
- });
412
- });
413
-
414
- // ─── Position scoring ─────────────────────────────────────────
415
-
416
- describe('position-based scoring', () => {
417
- it('should give first result score 1.0 and last result score 0.05', async () => {
418
- const items = Array.from({ length: 10 }, (_, i) => ({
419
- id: `r${i}`,
420
- text: `Result ${i}`,
421
- }));
422
- mockWithAuth(async () => new Response(JSON.stringify(items), { status: 200 }));
423
- const client = new CogneeClient();
424
- await client.healthCheck();
425
- const results = await client.search('query', { limit: 10 });
426
- expect(results[0].score).toBe(1.0);
427
- expect(results[results.length - 1].score).toBeCloseTo(0.05, 2);
428
- });
429
-
430
- it('should produce monotonically decreasing scores', async () => {
431
- const items = Array.from({ length: 5 }, (_, i) => ({
432
- id: `r${i}`,
433
- text: `Result ${i}`,
434
- }));
435
- mockWithAuth(async () => new Response(JSON.stringify(items), { status: 200 }));
436
- const client = new CogneeClient();
437
- await client.healthCheck();
438
- const results = await client.search('query', { limit: 5 });
439
- for (let i = 1; i < results.length; i++) {
440
- expect(results[i].score).toBeLessThan(results[i - 1].score);
441
- }
442
- });
443
-
444
- it('should give single result score 1.0', async () => {
445
- mockWithAuth(
446
- async () =>
447
- new Response(JSON.stringify([{ id: 'only', text: 'Sole result' }]), { status: 200 }),
448
- );
449
- const client = new CogneeClient();
450
- await client.healthCheck();
451
- const results = await client.search('query');
452
- expect(results[0].score).toBe(1.0);
453
- });
454
-
455
- it('should prefer explicit Cognee scores over position', async () => {
456
- mockWithAuth(
457
- async () =>
458
- new Response(
459
- JSON.stringify([
460
- { id: 'r1', text: 'First', score: 0.3 },
461
- { id: 'r2', text: 'Second', score: 0.9 },
462
- ]),
463
- { status: 200 },
464
- ),
465
- );
466
- const client = new CogneeClient();
467
- await client.healthCheck();
468
- const results = await client.search('query');
469
- // Explicit scores should be used, not position
470
- expect(results[0].score).toBe(0.3);
471
- expect(results[1].score).toBe(0.9);
472
- });
473
- });
474
- });