@soleri/core 9.11.0 → 9.13.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 (234) hide show
  1. package/dist/adapters/types.d.ts +2 -0
  2. package/dist/adapters/types.d.ts.map +1 -1
  3. package/dist/brain/brain.d.ts +5 -1
  4. package/dist/brain/brain.d.ts.map +1 -1
  5. package/dist/brain/brain.js +97 -10
  6. package/dist/brain/brain.js.map +1 -1
  7. package/dist/dream/cron-manager.d.ts +10 -0
  8. package/dist/dream/cron-manager.d.ts.map +1 -0
  9. package/dist/dream/cron-manager.js +122 -0
  10. package/dist/dream/cron-manager.js.map +1 -0
  11. package/dist/dream/dream-engine.d.ts +34 -0
  12. package/dist/dream/dream-engine.d.ts.map +1 -0
  13. package/dist/dream/dream-engine.js +88 -0
  14. package/dist/dream/dream-engine.js.map +1 -0
  15. package/dist/dream/dream-ops.d.ts +8 -0
  16. package/dist/dream/dream-ops.d.ts.map +1 -0
  17. package/dist/dream/dream-ops.js +49 -0
  18. package/dist/dream/dream-ops.js.map +1 -0
  19. package/dist/dream/index.d.ts +7 -0
  20. package/dist/dream/index.d.ts.map +1 -0
  21. package/dist/dream/index.js +5 -0
  22. package/dist/dream/index.js.map +1 -0
  23. package/dist/dream/schema.d.ts +3 -0
  24. package/dist/dream/schema.d.ts.map +1 -0
  25. package/dist/dream/schema.js +16 -0
  26. package/dist/dream/schema.js.map +1 -0
  27. package/dist/embeddings/index.d.ts +5 -0
  28. package/dist/embeddings/index.d.ts.map +1 -0
  29. package/dist/embeddings/index.js +3 -0
  30. package/dist/embeddings/index.js.map +1 -0
  31. package/dist/embeddings/openai-provider.d.ts +31 -0
  32. package/dist/embeddings/openai-provider.d.ts.map +1 -0
  33. package/dist/embeddings/openai-provider.js +120 -0
  34. package/dist/embeddings/openai-provider.js.map +1 -0
  35. package/dist/embeddings/pipeline.d.ts +36 -0
  36. package/dist/embeddings/pipeline.d.ts.map +1 -0
  37. package/dist/embeddings/pipeline.js +78 -0
  38. package/dist/embeddings/pipeline.js.map +1 -0
  39. package/dist/embeddings/types.d.ts +62 -0
  40. package/dist/embeddings/types.d.ts.map +1 -0
  41. package/dist/embeddings/types.js +3 -0
  42. package/dist/embeddings/types.js.map +1 -0
  43. package/dist/engine/bin/soleri-engine.js +4 -1
  44. package/dist/engine/bin/soleri-engine.js.map +1 -1
  45. package/dist/engine/module-manifest.d.ts.map +1 -1
  46. package/dist/engine/module-manifest.js +20 -0
  47. package/dist/engine/module-manifest.js.map +1 -1
  48. package/dist/engine/register-engine.d.ts.map +1 -1
  49. package/dist/engine/register-engine.js +12 -0
  50. package/dist/engine/register-engine.js.map +1 -1
  51. package/dist/flows/chain-types.d.ts +8 -8
  52. package/dist/flows/dispatch-registry.d.ts +15 -1
  53. package/dist/flows/dispatch-registry.d.ts.map +1 -1
  54. package/dist/flows/dispatch-registry.js +28 -1
  55. package/dist/flows/dispatch-registry.js.map +1 -1
  56. package/dist/flows/executor.d.ts +20 -2
  57. package/dist/flows/executor.d.ts.map +1 -1
  58. package/dist/flows/executor.js +79 -1
  59. package/dist/flows/executor.js.map +1 -1
  60. package/dist/flows/index.d.ts +2 -1
  61. package/dist/flows/index.d.ts.map +1 -1
  62. package/dist/flows/index.js.map +1 -1
  63. package/dist/flows/types.d.ts +43 -21
  64. package/dist/flows/types.d.ts.map +1 -1
  65. package/dist/index.d.ts +6 -1
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +4 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/persona/defaults.d.ts +8 -0
  70. package/dist/persona/defaults.d.ts.map +1 -1
  71. package/dist/persona/defaults.js +49 -0
  72. package/dist/persona/defaults.js.map +1 -1
  73. package/dist/plugins/types.d.ts +31 -31
  74. package/dist/runtime/admin-ops.d.ts.map +1 -1
  75. package/dist/runtime/admin-ops.js +15 -0
  76. package/dist/runtime/admin-ops.js.map +1 -1
  77. package/dist/runtime/admin-setup-ops.js +2 -2
  78. package/dist/runtime/admin-setup-ops.js.map +1 -1
  79. package/dist/runtime/embedding-ops.d.ts +12 -0
  80. package/dist/runtime/embedding-ops.d.ts.map +1 -0
  81. package/dist/runtime/embedding-ops.js +96 -0
  82. package/dist/runtime/embedding-ops.js.map +1 -0
  83. package/dist/runtime/facades/embedding-facade.d.ts +7 -0
  84. package/dist/runtime/facades/embedding-facade.d.ts.map +1 -0
  85. package/dist/runtime/facades/embedding-facade.js +8 -0
  86. package/dist/runtime/facades/embedding-facade.js.map +1 -0
  87. package/dist/runtime/facades/index.d.ts.map +1 -1
  88. package/dist/runtime/facades/index.js +12 -0
  89. package/dist/runtime/facades/index.js.map +1 -1
  90. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
  91. package/dist/runtime/facades/orchestrate-facade.js +120 -0
  92. package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
  93. package/dist/runtime/feature-flags.d.ts.map +1 -1
  94. package/dist/runtime/feature-flags.js +4 -0
  95. package/dist/runtime/feature-flags.js.map +1 -1
  96. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  97. package/dist/runtime/orchestrate-ops.js +140 -9
  98. package/dist/runtime/orchestrate-ops.js.map +1 -1
  99. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  100. package/dist/runtime/planning-extra-ops.js +51 -0
  101. package/dist/runtime/planning-extra-ops.js.map +1 -1
  102. package/dist/runtime/preflight.d.ts +32 -0
  103. package/dist/runtime/preflight.d.ts.map +1 -0
  104. package/dist/runtime/preflight.js +29 -0
  105. package/dist/runtime/preflight.js.map +1 -0
  106. package/dist/runtime/runtime.d.ts.map +1 -1
  107. package/dist/runtime/runtime.js +33 -2
  108. package/dist/runtime/runtime.js.map +1 -1
  109. package/dist/runtime/types.d.ts +27 -0
  110. package/dist/runtime/types.d.ts.map +1 -1
  111. package/dist/skills/step-tracker.d.ts +39 -0
  112. package/dist/skills/step-tracker.d.ts.map +1 -0
  113. package/dist/skills/step-tracker.js +105 -0
  114. package/dist/skills/step-tracker.js.map +1 -0
  115. package/dist/skills/sync-skills.d.ts +3 -2
  116. package/dist/skills/sync-skills.d.ts.map +1 -1
  117. package/dist/skills/sync-skills.js +42 -8
  118. package/dist/skills/sync-skills.js.map +1 -1
  119. package/dist/subagent/dispatcher.d.ts +4 -3
  120. package/dist/subagent/dispatcher.d.ts.map +1 -1
  121. package/dist/subagent/dispatcher.js +57 -35
  122. package/dist/subagent/dispatcher.js.map +1 -1
  123. package/dist/subagent/index.d.ts +1 -0
  124. package/dist/subagent/index.d.ts.map +1 -1
  125. package/dist/subagent/index.js.map +1 -1
  126. package/dist/subagent/orphan-reaper.d.ts +51 -4
  127. package/dist/subagent/orphan-reaper.d.ts.map +1 -1
  128. package/dist/subagent/orphan-reaper.js +103 -3
  129. package/dist/subagent/orphan-reaper.js.map +1 -1
  130. package/dist/subagent/types.d.ts +7 -0
  131. package/dist/subagent/types.d.ts.map +1 -1
  132. package/dist/subagent/workspace-resolver.d.ts +2 -0
  133. package/dist/subagent/workspace-resolver.d.ts.map +1 -1
  134. package/dist/subagent/workspace-resolver.js +3 -1
  135. package/dist/subagent/workspace-resolver.js.map +1 -1
  136. package/dist/vault/vault-entries.d.ts +18 -0
  137. package/dist/vault/vault-entries.d.ts.map +1 -1
  138. package/dist/vault/vault-entries.js +73 -0
  139. package/dist/vault/vault-entries.js.map +1 -1
  140. package/dist/vault/vault-manager.d.ts.map +1 -1
  141. package/dist/vault/vault-manager.js +1 -0
  142. package/dist/vault/vault-manager.js.map +1 -1
  143. package/dist/vault/vault-schema.d.ts.map +1 -1
  144. package/dist/vault/vault-schema.js +14 -0
  145. package/dist/vault/vault-schema.js.map +1 -1
  146. package/dist/vault/vault.d.ts +1 -0
  147. package/dist/vault/vault.d.ts.map +1 -1
  148. package/dist/vault/vault.js.map +1 -1
  149. package/package.json +3 -5
  150. package/src/__tests__/cron-manager.test.ts +132 -0
  151. package/src/__tests__/deviation-detection.test.ts +234 -0
  152. package/src/__tests__/embeddings.test.ts +536 -0
  153. package/src/__tests__/preflight.test.ts +97 -0
  154. package/src/__tests__/step-persistence.test.ts +324 -0
  155. package/src/__tests__/step-tracker.test.ts +260 -0
  156. package/src/__tests__/subagent/dispatcher.test.ts +122 -4
  157. package/src/__tests__/subagent/orphan-reaper.test.ts +148 -12
  158. package/src/__tests__/subagent/process-lifecycle.test.ts +422 -0
  159. package/src/__tests__/subagent/workspace-resolver.test.ts +6 -1
  160. package/src/adapters/types.ts +2 -0
  161. package/src/brain/brain.ts +117 -9
  162. package/src/dream/cron-manager.ts +137 -0
  163. package/src/dream/dream-engine.ts +119 -0
  164. package/src/dream/dream-ops.ts +56 -0
  165. package/src/dream/dream.test.ts +182 -0
  166. package/src/dream/index.ts +6 -0
  167. package/src/dream/schema.ts +17 -0
  168. package/src/embeddings/openai-provider.ts +158 -0
  169. package/src/embeddings/pipeline.ts +126 -0
  170. package/src/embeddings/types.ts +67 -0
  171. package/src/engine/bin/soleri-engine.ts +4 -1
  172. package/src/engine/module-manifest.test.ts +4 -4
  173. package/src/engine/module-manifest.ts +20 -0
  174. package/src/engine/register-engine.ts +12 -0
  175. package/src/flows/dispatch-registry.ts +44 -1
  176. package/src/flows/executor.ts +93 -2
  177. package/src/flows/index.ts +2 -0
  178. package/src/flows/types.ts +39 -1
  179. package/src/index.ts +12 -0
  180. package/src/persona/defaults.test.ts +39 -1
  181. package/src/persona/defaults.ts +65 -0
  182. package/src/planning/goal-ancestry.test.ts +3 -5
  183. package/src/planning/planner.test.ts +2 -3
  184. package/src/runtime/admin-ops.test.ts +2 -2
  185. package/src/runtime/admin-ops.ts +17 -0
  186. package/src/runtime/admin-setup-ops.ts +2 -2
  187. package/src/runtime/embedding-ops.ts +116 -0
  188. package/src/runtime/facades/admin-facade.test.ts +31 -0
  189. package/src/runtime/facades/embedding-facade.ts +11 -0
  190. package/src/runtime/facades/index.ts +12 -0
  191. package/src/runtime/facades/orchestrate-facade.test.ts +16 -0
  192. package/src/runtime/facades/orchestrate-facade.ts +146 -0
  193. package/src/runtime/feature-flags.ts +4 -0
  194. package/src/runtime/orchestrate-ops.test.ts +131 -0
  195. package/src/runtime/orchestrate-ops.ts +158 -10
  196. package/src/runtime/planning-extra-ops.ts +77 -0
  197. package/src/runtime/preflight.ts +53 -0
  198. package/src/runtime/runtime.ts +41 -2
  199. package/src/runtime/types.ts +20 -0
  200. package/src/skills/__tests__/sync-skills.test.ts +132 -0
  201. package/src/skills/step-tracker.ts +162 -0
  202. package/src/skills/sync-skills.ts +54 -9
  203. package/src/subagent/dispatcher.ts +62 -39
  204. package/src/subagent/index.ts +1 -0
  205. package/src/subagent/orphan-reaper.test.ts +135 -0
  206. package/src/subagent/orphan-reaper.ts +130 -7
  207. package/src/subagent/types.ts +10 -0
  208. package/src/subagent/workspace-resolver.ts +3 -1
  209. package/src/vault/vault-entries.ts +112 -0
  210. package/src/vault/vault-manager.ts +1 -0
  211. package/src/vault/vault-scaling.test.ts +3 -2
  212. package/src/vault/vault-schema.ts +15 -0
  213. package/src/vault/vault.ts +1 -0
  214. package/vitest.config.ts +2 -1
  215. package/dist/brain/strength-scorer.d.ts +0 -31
  216. package/dist/brain/strength-scorer.d.ts.map +0 -1
  217. package/dist/brain/strength-scorer.js +0 -264
  218. package/dist/brain/strength-scorer.js.map +0 -1
  219. package/dist/engine/index.d.ts +0 -21
  220. package/dist/engine/index.d.ts.map +0 -1
  221. package/dist/engine/index.js +0 -18
  222. package/dist/engine/index.js.map +0 -1
  223. package/dist/hooks/index.d.ts +0 -2
  224. package/dist/hooks/index.d.ts.map +0 -1
  225. package/dist/hooks/index.js +0 -2
  226. package/dist/hooks/index.js.map +0 -1
  227. package/dist/persona/index.d.ts +0 -5
  228. package/dist/persona/index.d.ts.map +0 -1
  229. package/dist/persona/index.js +0 -4
  230. package/dist/persona/index.js.map +0 -1
  231. package/dist/vault/vault-interfaces.d.ts +0 -153
  232. package/dist/vault/vault-interfaces.d.ts.map +0 -1
  233. package/dist/vault/vault-interfaces.js +0 -2
  234. package/dist/vault/vault-interfaces.js.map +0 -1
@@ -0,0 +1,536 @@
1
+ /**
2
+ * Embeddings — Unit Tests
3
+ *
4
+ * Covers:
5
+ * 1. OpenAIEmbeddingProvider (mocked fetch)
6
+ * 2. Vector storage CRUD (real in-memory SQLite)
7
+ * 3. EmbeddingPipeline (batch + incremental)
8
+ * 4. Brain hybrid search backward compatibility
9
+ */
10
+
11
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
12
+ import { SQLitePersistenceProvider } from '../persistence/sqlite-provider.js';
13
+ import { initializeSchema } from '../vault/vault-schema.js';
14
+ import {
15
+ storeVector,
16
+ getVector,
17
+ deleteVector,
18
+ getEntriesWithoutVectors,
19
+ cosineSearch,
20
+ } from '../vault/vault-entries.js';
21
+ import { EmbeddingPipeline } from '../embeddings/pipeline.js';
22
+ import { OpenAIEmbeddingProvider } from '../embeddings/openai-provider.js';
23
+ import type { EmbeddingProvider, EmbeddingResult } from '../embeddings/types.js';
24
+ import type { PersistenceProvider } from '../persistence/types.js';
25
+ import { Vault } from '../vault/vault.js';
26
+ import { Brain } from '../brain/brain.js';
27
+ import { LLMError } from '../llm/types.js';
28
+
29
+ // ── Helpers ─────────────────────────────────────────────────────────────
30
+
31
+ /** Create a normalized vector of given dimensions. */
32
+ function makeVector(seed: number, dims: number): number[] {
33
+ const v: number[] = [];
34
+ for (let i = 0; i < dims; i++) {
35
+ v.push(Math.sin(seed * (i + 1)));
36
+ }
37
+ // Normalize
38
+ const norm = Math.sqrt(v.reduce((s, x) => s + x * x, 0));
39
+ return v.map((x) => x / norm);
40
+ }
41
+
42
+ /** Insert a minimal vault entry directly via SQL. */
43
+ function insertEntry(
44
+ provider: PersistenceProvider,
45
+ id: string,
46
+ title: string,
47
+ description: string,
48
+ ): void {
49
+ provider.run(
50
+ `INSERT INTO entries (id, type, domain, title, severity, description, tags, applies_to, origin)
51
+ VALUES (@id, 'pattern', 'test', @title, 'suggestion', @description, '[]', '[]', 'user')`,
52
+ { id, title, description },
53
+ );
54
+ }
55
+
56
+ /** Build a mock OpenAI-compatible fetch response. */
57
+ function okResponse(vectors: number[][], tokens: number): Response {
58
+ const body = JSON.stringify({
59
+ data: vectors.map((embedding, index) => ({ embedding, index })),
60
+ usage: { prompt_tokens: tokens, total_tokens: tokens },
61
+ model: 'text-embedding-3-small',
62
+ });
63
+ return new Response(body, { status: 200, headers: { 'Content-Type': 'application/json' } });
64
+ }
65
+
66
+ function errorResponse(status: number, message: string): Response {
67
+ return new Response(JSON.stringify({ error: { message } }), {
68
+ status,
69
+ headers: { 'Content-Type': 'application/json' },
70
+ });
71
+ }
72
+
73
+ // =============================================================================
74
+ // 1. OpenAIEmbeddingProvider
75
+ // =============================================================================
76
+
77
+ describe('OpenAIEmbeddingProvider', () => {
78
+ let fetchSpy: ReturnType<typeof vi.spyOn>;
79
+
80
+ beforeEach(() => {
81
+ fetchSpy = vi.spyOn(globalThis, 'fetch');
82
+ });
83
+
84
+ afterEach(() => {
85
+ vi.restoreAllMocks();
86
+ });
87
+
88
+ it('throws when no API key or key pool is provided', () => {
89
+ expect(
90
+ () => new OpenAIEmbeddingProvider({ provider: 'openai', model: 'text-embedding-3-small' }),
91
+ ).toThrow(/API key/);
92
+ });
93
+
94
+ it('embeds multiple texts successfully', async () => {
95
+ const vec1 = [0.1, 0.2, 0.3];
96
+ const vec2 = [0.4, 0.5, 0.6];
97
+ fetchSpy.mockResolvedValueOnce(okResponse([vec1, vec2], 20));
98
+
99
+ const provider = new OpenAIEmbeddingProvider({
100
+ provider: 'openai',
101
+ model: 'text-embedding-3-small',
102
+ apiKey: 'sk-test-key',
103
+ });
104
+
105
+ const result = await provider.embed(['hello', 'world']);
106
+
107
+ expect(result.vectors).toHaveLength(2);
108
+ expect(result.vectors[0]).toEqual(vec1);
109
+ expect(result.vectors[1]).toEqual(vec2);
110
+ expect(result.tokensUsed).toBe(20);
111
+ expect(result.model).toBe('text-embedding-3-small');
112
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
113
+ });
114
+
115
+ it('returns empty result for empty input', async () => {
116
+ const provider = new OpenAIEmbeddingProvider({
117
+ provider: 'openai',
118
+ model: 'text-embedding-3-small',
119
+ apiKey: 'sk-test-key',
120
+ });
121
+
122
+ const result = await provider.embed([]);
123
+ expect(result.vectors).toHaveLength(0);
124
+ expect(result.tokensUsed).toBe(0);
125
+ expect(fetchSpy).not.toHaveBeenCalled();
126
+ });
127
+
128
+ it('chunks requests that exceed batchSize', async () => {
129
+ const batchSize = 2;
130
+ const vec1 = [0.1, 0.2];
131
+ const vec2 = [0.3, 0.4];
132
+ const vec3 = [0.5, 0.6];
133
+
134
+ fetchSpy
135
+ .mockResolvedValueOnce(okResponse([vec1, vec2], 10))
136
+ .mockResolvedValueOnce(okResponse([vec3], 5));
137
+
138
+ const provider = new OpenAIEmbeddingProvider({
139
+ provider: 'openai',
140
+ model: 'text-embedding-3-small',
141
+ apiKey: 'sk-test-key',
142
+ batchSize,
143
+ });
144
+
145
+ const result = await provider.embed(['a', 'b', 'c']);
146
+
147
+ expect(fetchSpy).toHaveBeenCalledTimes(2);
148
+ expect(result.vectors).toHaveLength(3);
149
+ expect(result.vectors[0]).toEqual(vec1);
150
+ expect(result.vectors[2]).toEqual(vec3);
151
+ expect(result.tokensUsed).toBe(15);
152
+ });
153
+
154
+ it('retries on 429 rate limit and succeeds', async () => {
155
+ const vec = [0.1, 0.2, 0.3];
156
+ fetchSpy
157
+ .mockResolvedValueOnce(errorResponse(429, 'Rate limited'))
158
+ .mockResolvedValueOnce(okResponse([vec], 5));
159
+
160
+ const provider = new OpenAIEmbeddingProvider({
161
+ provider: 'openai',
162
+ model: 'text-embedding-3-small',
163
+ apiKey: 'sk-test-key',
164
+ });
165
+
166
+ const result = await provider.embed(['hello']);
167
+ expect(result.vectors[0]).toEqual(vec);
168
+ // First call fails (429), retry succeeds
169
+ expect(fetchSpy).toHaveBeenCalledTimes(2);
170
+ });
171
+
172
+ it('throws immediately on 401 (not retryable)', async () => {
173
+ fetchSpy.mockResolvedValue(errorResponse(401, 'Unauthorized'));
174
+
175
+ const provider = new OpenAIEmbeddingProvider({
176
+ provider: 'openai',
177
+ model: 'text-embedding-3-small',
178
+ apiKey: 'sk-bad-key',
179
+ });
180
+
181
+ await expect(provider.embed(['test'])).rejects.toThrow(LLMError);
182
+ // Only one attempt — 401 is not retryable
183
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
184
+ });
185
+
186
+ it('retries on 500 server error', async () => {
187
+ const vec = [0.9, 0.8];
188
+ fetchSpy
189
+ .mockResolvedValueOnce(errorResponse(500, 'Internal Server Error'))
190
+ .mockResolvedValueOnce(okResponse([vec], 3));
191
+
192
+ const provider = new OpenAIEmbeddingProvider({
193
+ provider: 'openai',
194
+ model: 'text-embedding-3-small',
195
+ apiKey: 'sk-test-key',
196
+ });
197
+
198
+ const result = await provider.embed(['retry me']);
199
+ expect(result.vectors[0]).toEqual(vec);
200
+ expect(fetchSpy).toHaveBeenCalledTimes(2);
201
+ });
202
+
203
+ it('exhausts retries on persistent 5xx', async () => {
204
+ // Each retry reads the body, so we need a fresh Response each time
205
+ fetchSpy.mockImplementation(async () => errorResponse(503, 'Service Unavailable'));
206
+
207
+ const provider = new OpenAIEmbeddingProvider({
208
+ provider: 'openai',
209
+ model: 'text-embedding-3-small',
210
+ apiKey: 'sk-test-key',
211
+ });
212
+
213
+ await expect(provider.embed(['fail'])).rejects.toThrow(/503/);
214
+ // 3 attempts total (default maxAttempts)
215
+ expect(fetchSpy).toHaveBeenCalledTimes(3);
216
+ });
217
+ });
218
+
219
+ // =============================================================================
220
+ // 2. Vector Storage (vault-entries)
221
+ // =============================================================================
222
+
223
+ describe('Vector Storage', () => {
224
+ let provider: SQLitePersistenceProvider;
225
+
226
+ beforeEach(() => {
227
+ provider = new SQLitePersistenceProvider(':memory:');
228
+ initializeSchema(provider);
229
+ });
230
+
231
+ afterEach(() => {
232
+ provider.close();
233
+ });
234
+
235
+ it('storeVector + getVector roundtrip', () => {
236
+ insertEntry(provider, 'e1', 'Test Entry', 'A test');
237
+ const vec = [0.1, 0.2, 0.3, 0.4];
238
+
239
+ storeVector(provider, 'e1', vec, 'test-model', 4);
240
+
241
+ const stored = getVector(provider, 'e1');
242
+ expect(stored).not.toBeNull();
243
+ expect(stored!.entryId).toBe('e1');
244
+ expect(stored!.model).toBe('test-model');
245
+ expect(stored!.dimensions).toBe(4);
246
+ // Float32 precision — compare with tolerance
247
+ for (let i = 0; i < vec.length; i++) {
248
+ expect(stored!.vector[i]).toBeCloseTo(vec[i], 5);
249
+ }
250
+ });
251
+
252
+ it('storeVector upserts — second store overwrites', () => {
253
+ insertEntry(provider, 'e1', 'Test', 'Desc');
254
+ storeVector(provider, 'e1', [0.1, 0.2], 'model-v1', 2);
255
+ storeVector(provider, 'e1', [0.9, 0.8], 'model-v2', 2);
256
+
257
+ const stored = getVector(provider, 'e1');
258
+ expect(stored!.model).toBe('model-v2');
259
+ expect(stored!.vector[0]).toBeCloseTo(0.9, 5);
260
+ });
261
+
262
+ it('getVector returns null for missing entry', () => {
263
+ expect(getVector(provider, 'nonexistent')).toBeNull();
264
+ });
265
+
266
+ it('deleteVector removes stored vector', () => {
267
+ insertEntry(provider, 'e1', 'Test', 'Desc');
268
+ storeVector(provider, 'e1', [0.5, 0.5], 'model', 2);
269
+ expect(getVector(provider, 'e1')).toBeTruthy();
270
+
271
+ deleteVector(provider, 'e1');
272
+ const result = getVector(provider, 'e1');
273
+ expect(result).toBeFalsy();
274
+ });
275
+
276
+ it('getEntriesWithoutVectors returns only entries missing vectors', () => {
277
+ insertEntry(provider, 'e1', 'Has vector', 'Desc');
278
+ insertEntry(provider, 'e2', 'No vector', 'Desc');
279
+ insertEntry(provider, 'e3', 'Also no vector', 'Desc');
280
+
281
+ storeVector(provider, 'e1', [0.1, 0.2], 'model-a', 2);
282
+
283
+ const missing = getEntriesWithoutVectors(provider, 'model-a');
284
+ expect(missing).toContain('e2');
285
+ expect(missing).toContain('e3');
286
+ expect(missing).not.toContain('e1');
287
+ expect(missing).toHaveLength(2);
288
+ });
289
+
290
+ it('getEntriesWithoutVectors considers model — different model means missing', () => {
291
+ insertEntry(provider, 'e1', 'Test', 'Desc');
292
+ storeVector(provider, 'e1', [0.1, 0.2], 'model-a', 2);
293
+
294
+ // Entry has vector for model-a, not model-b
295
+ const missingForB = getEntriesWithoutVectors(provider, 'model-b');
296
+ expect(missingForB).toContain('e1');
297
+
298
+ const missingForA = getEntriesWithoutVectors(provider, 'model-a');
299
+ expect(missingForA).not.toContain('e1');
300
+ });
301
+
302
+ it('cosineSearch returns results ordered by similarity', () => {
303
+ insertEntry(provider, 'e1', 'First', 'Desc');
304
+ insertEntry(provider, 'e2', 'Second', 'Desc');
305
+ insertEntry(provider, 'e3', 'Third', 'Desc');
306
+
307
+ // Store vectors: e1 is close to query, e3 is far
308
+ const dims = 8;
309
+ const query = makeVector(1, dims);
310
+ const closeVec = makeVector(1.05, dims); // very similar to query
311
+ const midVec = makeVector(3, dims); // somewhat different
312
+ const farVec = makeVector(50, dims); // very different
313
+
314
+ storeVector(provider, 'e1', closeVec, 'model', dims);
315
+ storeVector(provider, 'e2', midVec, 'model', dims);
316
+ storeVector(provider, 'e3', farVec, 'model', dims);
317
+
318
+ const results = cosineSearch(provider, query, 3);
319
+
320
+ expect(results).toHaveLength(3);
321
+ // e1 (closeVec) should be most similar
322
+ expect(results[0].entryId).toBe('e1');
323
+ expect(results[0].similarity).toBeGreaterThan(results[1].similarity);
324
+ // Similarities should be in descending order
325
+ expect(results[0].similarity).toBeGreaterThanOrEqual(results[1].similarity);
326
+ expect(results[1].similarity).toBeGreaterThanOrEqual(results[2].similarity);
327
+ });
328
+
329
+ it('cosineSearch respects topK limit', () => {
330
+ const dims = 4;
331
+ for (let i = 1; i <= 5; i++) {
332
+ insertEntry(provider, `e${i}`, `Entry ${i}`, 'Desc');
333
+ storeVector(provider, `e${i}`, makeVector(i, dims), 'model', dims);
334
+ }
335
+
336
+ const results = cosineSearch(provider, makeVector(1, dims), 2);
337
+ expect(results).toHaveLength(2);
338
+ });
339
+
340
+ it('cosineSearch returns empty for zero-norm query', () => {
341
+ insertEntry(provider, 'e1', 'Test', 'Desc');
342
+ storeVector(provider, 'e1', [0.1, 0.2], 'model', 2);
343
+
344
+ const results = cosineSearch(provider, [0, 0], 5);
345
+ expect(results).toHaveLength(0);
346
+ });
347
+ });
348
+
349
+ // =============================================================================
350
+ // 3. EmbeddingPipeline
351
+ // =============================================================================
352
+
353
+ describe('EmbeddingPipeline', () => {
354
+ let persistence: SQLitePersistenceProvider;
355
+ let mockProvider: EmbeddingProvider;
356
+ let embedCalls: string[][];
357
+
358
+ beforeEach(() => {
359
+ persistence = new SQLitePersistenceProvider(':memory:');
360
+ initializeSchema(persistence);
361
+ embedCalls = [];
362
+
363
+ mockProvider = {
364
+ providerName: 'mock',
365
+ model: 'mock-model',
366
+ dimensions: 4,
367
+ embed: vi.fn(async (texts: string[]): Promise<EmbeddingResult> => {
368
+ embedCalls.push(texts);
369
+ return {
370
+ vectors: texts.map((_, i) => makeVector(i + 1, 4)),
371
+ tokensUsed: texts.length * 5,
372
+ model: 'mock-model',
373
+ };
374
+ }),
375
+ };
376
+ });
377
+
378
+ afterEach(() => {
379
+ persistence.close();
380
+ });
381
+
382
+ it('batchEmbed embeds all entries missing vectors', async () => {
383
+ insertEntry(persistence, 'e1', 'First', 'Description one');
384
+ insertEntry(persistence, 'e2', 'Second', 'Description two');
385
+
386
+ const pipeline = new EmbeddingPipeline(mockProvider, persistence);
387
+ const result = await pipeline.batchEmbed();
388
+
389
+ expect(result.embedded).toBe(2);
390
+ expect(result.failed).toBe(0);
391
+ expect(result.tokensUsed).toBeGreaterThan(0);
392
+
393
+ // Both entries should now have vectors
394
+ expect(getVector(persistence, 'e1')).toBeTruthy();
395
+ expect(getVector(persistence, 'e2')).toBeTruthy();
396
+ });
397
+
398
+ it('batchEmbed fires onProgress callback', async () => {
399
+ insertEntry(persistence, 'e1', 'First', 'Desc');
400
+ insertEntry(persistence, 'e2', 'Second', 'Desc');
401
+
402
+ const progress: Array<[number, number]> = [];
403
+ const pipeline = new EmbeddingPipeline(mockProvider, persistence);
404
+ await pipeline.batchEmbed({
405
+ onProgress: (completed, total) => progress.push([completed, total]),
406
+ });
407
+
408
+ expect(progress.length).toBeGreaterThan(0);
409
+ // Last progress call should have completed == total
410
+ const last = progress[progress.length - 1];
411
+ expect(last[0]).toBe(last[1]);
412
+ });
413
+
414
+ it('batchEmbed skips entries that already have vectors', async () => {
415
+ insertEntry(persistence, 'e1', 'Has Vector', 'Desc');
416
+ insertEntry(persistence, 'e2', 'No Vector', 'Desc');
417
+
418
+ // Pre-store vector for e1
419
+ storeVector(persistence, 'e1', [0.1, 0.2, 0.3, 0.4], 'mock-model', 4);
420
+
421
+ const pipeline = new EmbeddingPipeline(mockProvider, persistence);
422
+ const result = await pipeline.batchEmbed();
423
+
424
+ expect(result.embedded).toBe(1); // Only e2
425
+ // The mock embed should only have been called with e2's text
426
+ expect(embedCalls).toHaveLength(1);
427
+ });
428
+
429
+ it('batchEmbed returns zeros when all entries already embedded', async () => {
430
+ insertEntry(persistence, 'e1', 'Already done', 'Desc');
431
+ storeVector(persistence, 'e1', [0.1, 0.2, 0.3, 0.4], 'mock-model', 4);
432
+
433
+ const pipeline = new EmbeddingPipeline(mockProvider, persistence);
434
+ const result = await pipeline.batchEmbed();
435
+
436
+ expect(result.embedded).toBe(0);
437
+ expect(result.skipped).toBe(0);
438
+ expect(result.failed).toBe(0);
439
+ expect(mockProvider.embed).not.toHaveBeenCalled();
440
+ });
441
+
442
+ it('embedEntry embeds a single entry', async () => {
443
+ insertEntry(persistence, 'e1', 'Test', 'Desc');
444
+
445
+ const pipeline = new EmbeddingPipeline(mockProvider, persistence);
446
+ const embedded = await pipeline.embedEntry('e1', 'Test\nDesc');
447
+
448
+ expect(embedded).toBe(true);
449
+ expect(getVector(persistence, 'e1')).toBeTruthy();
450
+ expect(mockProvider.embed).toHaveBeenCalledTimes(1);
451
+ });
452
+
453
+ it('embedEntry returns false when vector already exists for same model', async () => {
454
+ insertEntry(persistence, 'e1', 'Test', 'Desc');
455
+ storeVector(persistence, 'e1', [0.1, 0.2, 0.3, 0.4], 'mock-model', 4);
456
+
457
+ const pipeline = new EmbeddingPipeline(mockProvider, persistence);
458
+ const embedded = await pipeline.embedEntry('e1', 'Test\nDesc');
459
+
460
+ expect(embedded).toBe(false);
461
+ expect(mockProvider.embed).not.toHaveBeenCalled();
462
+ });
463
+
464
+ it('embedEntry re-embeds when stored vector is for a different model', async () => {
465
+ insertEntry(persistence, 'e1', 'Test', 'Desc');
466
+ storeVector(persistence, 'e1', [0.1, 0.2, 0.3, 0.4], 'old-model', 4);
467
+
468
+ const pipeline = new EmbeddingPipeline(mockProvider, persistence);
469
+ const embedded = await pipeline.embedEntry('e1', 'Test\nDesc');
470
+
471
+ expect(embedded).toBe(true);
472
+ const stored = getVector(persistence, 'e1');
473
+ expect(stored!.model).toBe('mock-model');
474
+ });
475
+ });
476
+
477
+ // =============================================================================
478
+ // 4. Brain Hybrid Search (backward compatibility)
479
+ // =============================================================================
480
+
481
+ describe('Brain hybrid search compatibility', () => {
482
+ it('Brain constructor accepts optional embeddingProvider', () => {
483
+ const vault = new Vault(':memory:');
484
+ const mockEmb: EmbeddingProvider = {
485
+ providerName: 'mock',
486
+ model: 'mock-model',
487
+ dimensions: 4,
488
+ embed: vi.fn(async () => ({ vectors: [], tokensUsed: 0, model: 'mock-model' })),
489
+ };
490
+
491
+ expect(() => new Brain(vault, undefined, mockEmb)).not.toThrow();
492
+ });
493
+
494
+ it('Brain works without embeddingProvider (backward compat)', async () => {
495
+ const vault = new Vault(':memory:');
496
+
497
+ // Seed an entry so search can find something
498
+ vault.seed([
499
+ {
500
+ id: 'compat-1',
501
+ type: 'pattern',
502
+ domain: 'test',
503
+ title: 'Backward compatibility pattern',
504
+ severity: 'suggestion',
505
+ description: 'This tests that Brain search works without embedding provider',
506
+ tags: ['compat'],
507
+ appliesTo: [],
508
+ },
509
+ ]);
510
+
511
+ const brain = new Brain(vault);
512
+ const results = await brain.intelligentSearch('backward compatibility');
513
+
514
+ // Should not throw — returns results from FTS only
515
+ expect(Array.isArray(results)).toBe(true);
516
+ expect(results.length).toBeGreaterThan(0);
517
+ });
518
+
519
+ it('Brain.setEmbeddingProvider can set and clear provider', () => {
520
+ const vault = new Vault(':memory:');
521
+ const brain = new Brain(vault);
522
+
523
+ const mockEmb: EmbeddingProvider = {
524
+ providerName: 'mock',
525
+ model: 'mock-model',
526
+ dimensions: 4,
527
+ embed: vi.fn(async () => ({ vectors: [], tokensUsed: 0, model: 'mock-model' })),
528
+ };
529
+
530
+ // Set provider
531
+ expect(() => brain.setEmbeddingProvider(mockEmb)).not.toThrow();
532
+
533
+ // Clear provider
534
+ expect(() => brain.setEmbeddingProvider(undefined)).not.toThrow();
535
+ });
536
+ });
@@ -0,0 +1,97 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { buildPreflightManifest, type PreflightInput } from '../runtime/preflight.js';
3
+
4
+ function makeInput(overrides: Partial<PreflightInput> = {}): PreflightInput {
5
+ return {
6
+ facades: [
7
+ {
8
+ name: 'agent_vault',
9
+ ops: [
10
+ { name: 'search_intelligent', description: 'Search knowledge' },
11
+ { name: 'capture_knowledge', description: 'Capture knowledge' },
12
+ ],
13
+ },
14
+ {
15
+ name: 'agent_plan',
16
+ ops: [{ name: 'create_plan', description: 'Create a plan' }],
17
+ },
18
+ ],
19
+ skills: ['vault-capture', 'debugging'],
20
+ executingPlans: [{ id: 'plan-1', objective: 'Add preflight manifest', status: 'executing' }],
21
+ vaultStats: {
22
+ totalEntries: 42,
23
+ byDomain: { architecture: 10, testing: 15, patterns: 17 },
24
+ },
25
+ ...overrides,
26
+ };
27
+ }
28
+
29
+ describe('buildPreflightManifest', () => {
30
+ it('flattens facade ops into tools array', () => {
31
+ const manifest = buildPreflightManifest(makeInput());
32
+ expect(manifest.tools).toHaveLength(3);
33
+ expect(manifest.tools[0]).toEqual({
34
+ facade: 'agent_vault',
35
+ op: 'search_intelligent',
36
+ description: 'Search knowledge',
37
+ });
38
+ expect(manifest.tools[2]).toEqual({
39
+ facade: 'agent_plan',
40
+ op: 'create_plan',
41
+ description: 'Create a plan',
42
+ });
43
+ });
44
+
45
+ it('passes through skills array', () => {
46
+ const manifest = buildPreflightManifest(makeInput());
47
+ expect(manifest.skills).toEqual(['vault-capture', 'debugging']);
48
+ });
49
+
50
+ it('maps executing plans to activePlans', () => {
51
+ const manifest = buildPreflightManifest(makeInput());
52
+ expect(manifest.activePlans).toEqual([
53
+ { planId: 'plan-1', title: 'Add preflight manifest', status: 'executing' },
54
+ ]);
55
+ });
56
+
57
+ it('builds vault summary from stats', () => {
58
+ const manifest = buildPreflightManifest(makeInput());
59
+ expect(manifest.vaultSummary).toEqual({
60
+ entryCount: 42,
61
+ connected: true,
62
+ domains: ['architecture', 'testing', 'patterns'],
63
+ });
64
+ });
65
+
66
+ it('handles empty inputs gracefully', () => {
67
+ const manifest = buildPreflightManifest(
68
+ makeInput({
69
+ facades: [],
70
+ skills: [],
71
+ executingPlans: [],
72
+ vaultStats: { totalEntries: 0, byDomain: {} },
73
+ }),
74
+ );
75
+ expect(manifest.tools).toEqual([]);
76
+ expect(manifest.skills).toEqual([]);
77
+ expect(manifest.activePlans).toEqual([]);
78
+ expect(manifest.vaultSummary).toEqual({
79
+ entryCount: 0,
80
+ connected: true,
81
+ domains: [],
82
+ });
83
+ });
84
+
85
+ it('handles multiple executing plans', () => {
86
+ const manifest = buildPreflightManifest(
87
+ makeInput({
88
+ executingPlans: [
89
+ { id: 'plan-1', objective: 'First plan', status: 'executing' },
90
+ { id: 'plan-2', objective: 'Second plan', status: 'executing' },
91
+ ],
92
+ }),
93
+ );
94
+ expect(manifest.activePlans).toHaveLength(2);
95
+ expect(manifest.activePlans[1].planId).toBe('plan-2');
96
+ });
97
+ });