@soleri/core 7.0.0 → 8.1.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 (294) hide show
  1. package/dist/agency/agency-manager.d.ts +27 -1
  2. package/dist/agency/agency-manager.d.ts.map +1 -1
  3. package/dist/agency/agency-manager.js +180 -9
  4. package/dist/agency/agency-manager.js.map +1 -1
  5. package/dist/agency/default-rules.d.ts +7 -0
  6. package/dist/agency/default-rules.d.ts.map +1 -0
  7. package/dist/agency/default-rules.js +79 -0
  8. package/dist/agency/default-rules.js.map +1 -0
  9. package/dist/agency/types.d.ts +48 -0
  10. package/dist/agency/types.d.ts.map +1 -1
  11. package/dist/brain/brain.d.ts +17 -2
  12. package/dist/brain/brain.d.ts.map +1 -1
  13. package/dist/brain/brain.js +118 -8
  14. package/dist/brain/brain.js.map +1 -1
  15. package/dist/brain/knowledge-synthesizer.d.ts +37 -0
  16. package/dist/brain/knowledge-synthesizer.d.ts.map +1 -0
  17. package/dist/brain/knowledge-synthesizer.js +159 -0
  18. package/dist/brain/knowledge-synthesizer.js.map +1 -0
  19. package/dist/brain/learning-radar.d.ts +96 -0
  20. package/dist/brain/learning-radar.d.ts.map +1 -0
  21. package/dist/brain/learning-radar.js +202 -0
  22. package/dist/brain/learning-radar.js.map +1 -0
  23. package/dist/brain/types.d.ts +15 -0
  24. package/dist/brain/types.d.ts.map +1 -1
  25. package/dist/context/context-engine.d.ts.map +1 -1
  26. package/dist/context/context-engine.js +82 -17
  27. package/dist/context/context-engine.js.map +1 -1
  28. package/dist/context/types.d.ts +5 -0
  29. package/dist/context/types.d.ts.map +1 -1
  30. package/dist/control/intent-router.d.ts +12 -1
  31. package/dist/control/intent-router.d.ts.map +1 -1
  32. package/dist/control/intent-router.js +68 -0
  33. package/dist/control/intent-router.js.map +1 -1
  34. package/dist/control/types.d.ts +17 -0
  35. package/dist/control/types.d.ts.map +1 -1
  36. package/dist/curator/classifier.d.ts +18 -0
  37. package/dist/curator/classifier.d.ts.map +1 -0
  38. package/dist/curator/classifier.js +59 -0
  39. package/dist/curator/classifier.js.map +1 -0
  40. package/dist/curator/quality-gate.d.ts +29 -0
  41. package/dist/curator/quality-gate.d.ts.map +1 -0
  42. package/dist/curator/quality-gate.js +86 -0
  43. package/dist/curator/quality-gate.js.map +1 -0
  44. package/dist/domain-packs/index.d.ts +0 -3
  45. package/dist/domain-packs/index.d.ts.map +1 -1
  46. package/dist/domain-packs/index.js +0 -3
  47. package/dist/domain-packs/index.js.map +1 -1
  48. package/dist/domain-packs/loader.d.ts.map +1 -1
  49. package/dist/domain-packs/loader.js +20 -4
  50. package/dist/domain-packs/loader.js.map +1 -1
  51. package/dist/domain-packs/pack-runtime.d.ts +5 -5
  52. package/dist/domain-packs/pack-runtime.d.ts.map +1 -1
  53. package/dist/domain-packs/pack-runtime.js +2 -2
  54. package/dist/domain-packs/pack-runtime.js.map +1 -1
  55. package/dist/domain-packs/types.d.ts +8 -2
  56. package/dist/domain-packs/types.d.ts.map +1 -1
  57. package/dist/domain-packs/types.js.map +1 -1
  58. package/dist/engine/bin/soleri-engine.js +13 -2
  59. package/dist/engine/bin/soleri-engine.js.map +1 -1
  60. package/dist/engine/index.d.ts +2 -0
  61. package/dist/engine/index.d.ts.map +1 -1
  62. package/dist/engine/index.js +1 -0
  63. package/dist/engine/index.js.map +1 -1
  64. package/dist/engine/module-manifest.d.ts +28 -0
  65. package/dist/engine/module-manifest.d.ts.map +1 -0
  66. package/dist/engine/module-manifest.js +85 -0
  67. package/dist/engine/module-manifest.js.map +1 -0
  68. package/dist/engine/register-engine.d.ts +19 -0
  69. package/dist/engine/register-engine.d.ts.map +1 -1
  70. package/dist/engine/register-engine.js +15 -2
  71. package/dist/engine/register-engine.js.map +1 -1
  72. package/dist/events/event-bus.d.ts +30 -0
  73. package/dist/events/event-bus.d.ts.map +1 -0
  74. package/dist/events/event-bus.js +51 -0
  75. package/dist/events/event-bus.js.map +1 -0
  76. package/dist/flows/chain-runner.d.ts +46 -0
  77. package/dist/flows/chain-runner.d.ts.map +1 -0
  78. package/dist/flows/chain-runner.js +271 -0
  79. package/dist/flows/chain-runner.js.map +1 -0
  80. package/dist/flows/chain-types.d.ts +103 -0
  81. package/dist/flows/chain-types.d.ts.map +1 -0
  82. package/dist/flows/chain-types.js +23 -0
  83. package/dist/flows/chain-types.js.map +1 -0
  84. package/dist/health/doctor-checks.d.ts +15 -0
  85. package/dist/health/doctor-checks.d.ts.map +1 -0
  86. package/dist/health/doctor-checks.js +98 -0
  87. package/dist/health/doctor-checks.js.map +1 -0
  88. package/dist/index.d.ts +0 -1
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +0 -1
  91. package/dist/index.js.map +1 -1
  92. package/dist/intake/content-classifier.d.ts.map +1 -1
  93. package/dist/intake/content-classifier.js +0 -2
  94. package/dist/intake/content-classifier.js.map +1 -1
  95. package/dist/intake/text-ingester.d.ts +52 -0
  96. package/dist/intake/text-ingester.d.ts.map +1 -0
  97. package/dist/intake/text-ingester.js +181 -0
  98. package/dist/intake/text-ingester.js.map +1 -0
  99. package/dist/llm/llm-client.d.ts.map +1 -1
  100. package/dist/llm/llm-client.js +45 -5
  101. package/dist/llm/llm-client.js.map +1 -1
  102. package/dist/llm/oauth-discovery.d.ts +18 -0
  103. package/dist/llm/oauth-discovery.d.ts.map +1 -0
  104. package/dist/llm/oauth-discovery.js +130 -0
  105. package/dist/llm/oauth-discovery.js.map +1 -0
  106. package/dist/llm/types.d.ts +4 -2
  107. package/dist/llm/types.d.ts.map +1 -1
  108. package/dist/packs/pack-installer.d.ts +2 -1
  109. package/dist/packs/pack-installer.d.ts.map +1 -1
  110. package/dist/packs/pack-installer.js +10 -1
  111. package/dist/packs/pack-installer.js.map +1 -1
  112. package/dist/persistence/index.d.ts +0 -1
  113. package/dist/persistence/index.d.ts.map +1 -1
  114. package/dist/persistence/index.js +0 -1
  115. package/dist/persistence/index.js.map +1 -1
  116. package/dist/persistence/types.d.ts +2 -6
  117. package/dist/persistence/types.d.ts.map +1 -1
  118. package/dist/planning/evidence-collector.d.ts +41 -0
  119. package/dist/planning/evidence-collector.d.ts.map +1 -0
  120. package/dist/planning/evidence-collector.js +194 -0
  121. package/dist/planning/evidence-collector.js.map +1 -0
  122. package/dist/planning/planner.d.ts +4 -0
  123. package/dist/planning/planner.d.ts.map +1 -1
  124. package/dist/planning/planner.js +11 -0
  125. package/dist/planning/planner.js.map +1 -1
  126. package/dist/plugins/index.d.ts +4 -0
  127. package/dist/plugins/index.d.ts.map +1 -1
  128. package/dist/plugins/index.js +4 -0
  129. package/dist/plugins/index.js.map +1 -1
  130. package/dist/plugins/plugin-registry.d.ts +4 -0
  131. package/dist/plugins/plugin-registry.d.ts.map +1 -1
  132. package/dist/plugins/plugin-registry.js +4 -0
  133. package/dist/plugins/plugin-registry.js.map +1 -1
  134. package/dist/plugins/types.d.ts +32 -27
  135. package/dist/plugins/types.d.ts.map +1 -1
  136. package/dist/plugins/types.js +6 -3
  137. package/dist/plugins/types.js.map +1 -1
  138. package/dist/queue/job-queue.d.ts +92 -0
  139. package/dist/queue/job-queue.d.ts.map +1 -0
  140. package/dist/queue/job-queue.js +180 -0
  141. package/dist/queue/job-queue.js.map +1 -0
  142. package/dist/queue/pipeline-runner.d.ts +62 -0
  143. package/dist/queue/pipeline-runner.d.ts.map +1 -0
  144. package/dist/queue/pipeline-runner.js +126 -0
  145. package/dist/queue/pipeline-runner.js.map +1 -0
  146. package/dist/runtime/admin-setup-ops.d.ts +20 -0
  147. package/dist/runtime/admin-setup-ops.d.ts.map +1 -0
  148. package/dist/runtime/admin-setup-ops.js +583 -0
  149. package/dist/runtime/admin-setup-ops.js.map +1 -0
  150. package/dist/runtime/chain-ops.d.ts +9 -0
  151. package/dist/runtime/chain-ops.d.ts.map +1 -0
  152. package/dist/runtime/chain-ops.js +107 -0
  153. package/dist/runtime/chain-ops.js.map +1 -0
  154. package/dist/runtime/claude-md-helpers.d.ts +56 -0
  155. package/dist/runtime/claude-md-helpers.d.ts.map +1 -0
  156. package/dist/runtime/claude-md-helpers.js +160 -0
  157. package/dist/runtime/claude-md-helpers.js.map +1 -0
  158. package/dist/runtime/curator-extra-ops.d.ts +3 -2
  159. package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
  160. package/dist/runtime/curator-extra-ops.js +81 -3
  161. package/dist/runtime/curator-extra-ops.js.map +1 -1
  162. package/dist/runtime/facades/admin-facade.d.ts.map +1 -1
  163. package/dist/runtime/facades/admin-facade.js +5 -2
  164. package/dist/runtime/facades/admin-facade.js.map +1 -1
  165. package/dist/runtime/facades/agency-facade.d.ts.map +1 -1
  166. package/dist/runtime/facades/agency-facade.js +64 -0
  167. package/dist/runtime/facades/agency-facade.js.map +1 -1
  168. package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
  169. package/dist/runtime/facades/brain-facade.js +122 -1
  170. package/dist/runtime/facades/brain-facade.js.map +1 -1
  171. package/dist/runtime/facades/control-facade.d.ts.map +1 -1
  172. package/dist/runtime/facades/control-facade.js +42 -0
  173. package/dist/runtime/facades/control-facade.js.map +1 -1
  174. package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
  175. package/dist/runtime/facades/memory-facade.js +20 -2
  176. package/dist/runtime/facades/memory-facade.js.map +1 -1
  177. package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
  178. package/dist/runtime/facades/plan-facade.js +2 -0
  179. package/dist/runtime/facades/plan-facade.js.map +1 -1
  180. package/dist/runtime/facades/vault-facade.d.ts.map +1 -1
  181. package/dist/runtime/facades/vault-facade.js +25 -5
  182. package/dist/runtime/facades/vault-facade.js.map +1 -1
  183. package/dist/runtime/intake-ops.d.ts +7 -5
  184. package/dist/runtime/intake-ops.d.ts.map +1 -1
  185. package/dist/runtime/intake-ops.js +98 -5
  186. package/dist/runtime/intake-ops.js.map +1 -1
  187. package/dist/runtime/memory-extra-ops.d.ts +6 -3
  188. package/dist/runtime/memory-extra-ops.d.ts.map +1 -1
  189. package/dist/runtime/memory-extra-ops.js +292 -4
  190. package/dist/runtime/memory-extra-ops.js.map +1 -1
  191. package/dist/runtime/pack-ops.d.ts +3 -0
  192. package/dist/runtime/pack-ops.d.ts.map +1 -1
  193. package/dist/runtime/pack-ops.js +18 -1
  194. package/dist/runtime/pack-ops.js.map +1 -1
  195. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  196. package/dist/runtime/planning-extra-ops.js +85 -0
  197. package/dist/runtime/planning-extra-ops.js.map +1 -1
  198. package/dist/runtime/playbook-ops.js +1 -1
  199. package/dist/runtime/playbook-ops.js.map +1 -1
  200. package/dist/runtime/plugin-ops.d.ts.map +1 -1
  201. package/dist/runtime/plugin-ops.js +3 -0
  202. package/dist/runtime/plugin-ops.js.map +1 -1
  203. package/dist/runtime/runtime.d.ts.map +1 -1
  204. package/dist/runtime/runtime.js +143 -2
  205. package/dist/runtime/runtime.js.map +1 -1
  206. package/dist/runtime/session-briefing.d.ts +23 -0
  207. package/dist/runtime/session-briefing.d.ts.map +1 -0
  208. package/dist/runtime/session-briefing.js +154 -0
  209. package/dist/runtime/session-briefing.js.map +1 -0
  210. package/dist/runtime/types.d.ts +23 -0
  211. package/dist/runtime/types.d.ts.map +1 -1
  212. package/dist/runtime/vault-linking-ops.d.ts.map +1 -1
  213. package/dist/runtime/vault-linking-ops.js +3 -7
  214. package/dist/runtime/vault-linking-ops.js.map +1 -1
  215. package/dist/vault/vault.d.ts +34 -0
  216. package/dist/vault/vault.d.ts.map +1 -1
  217. package/dist/vault/vault.js +89 -3
  218. package/dist/vault/vault.js.map +1 -1
  219. package/package.json +6 -4
  220. package/src/__tests__/admin-setup-ops.test.ts +355 -0
  221. package/src/__tests__/async-infrastructure.test.ts +307 -0
  222. package/src/__tests__/cognee-client-gaps.test.ts +6 -2
  223. package/src/__tests__/cognee-hybrid-search.test.ts +49 -35
  224. package/src/__tests__/cognee-sync-manager-deep.test.ts +89 -65
  225. package/src/__tests__/curator-extra-ops.test.ts +6 -2
  226. package/src/__tests__/curator-pipeline-e2e.test.ts +545 -0
  227. package/src/__tests__/memory-extra-ops.test.ts +2 -2
  228. package/src/__tests__/module-manifest-drift.test.ts +59 -0
  229. package/src/__tests__/planning-extra-ops.test.ts +2 -2
  230. package/src/__tests__/second-brain-features.test.ts +583 -0
  231. package/src/agency/agency-manager.ts +217 -9
  232. package/src/agency/default-rules.ts +83 -0
  233. package/src/agency/types.ts +61 -0
  234. package/src/brain/brain.ts +110 -8
  235. package/src/brain/knowledge-synthesizer.ts +216 -0
  236. package/src/brain/learning-radar.ts +340 -0
  237. package/src/brain/types.ts +16 -0
  238. package/src/context/context-engine.ts +114 -15
  239. package/src/context/types.ts +5 -0
  240. package/src/control/intent-router.ts +107 -0
  241. package/src/control/types.ts +10 -0
  242. package/src/curator/classifier.ts +86 -0
  243. package/src/curator/quality-gate.ts +127 -0
  244. package/src/domain-packs/index.ts +0 -6
  245. package/src/domain-packs/loader.ts +25 -5
  246. package/src/domain-packs/pack-runtime.ts +6 -6
  247. package/src/domain-packs/types.ts +8 -2
  248. package/src/engine/bin/soleri-engine.ts +18 -2
  249. package/src/engine/index.ts +2 -0
  250. package/src/engine/module-manifest.ts +99 -0
  251. package/src/engine/register-engine.ts +21 -2
  252. package/src/events/event-bus.ts +58 -0
  253. package/src/flows/chain-runner.ts +369 -0
  254. package/src/flows/chain-types.ts +57 -0
  255. package/src/index.ts +0 -1
  256. package/src/intake/content-classifier.ts +0 -2
  257. package/src/intake/text-ingester.ts +234 -0
  258. package/src/llm/llm-client.ts +50 -7
  259. package/src/llm/oauth-discovery.ts +151 -0
  260. package/src/llm/types.ts +4 -2
  261. package/src/packs/pack-installer.ts +16 -1
  262. package/src/persistence/index.ts +0 -1
  263. package/src/persistence/types.ts +2 -6
  264. package/src/planning/evidence-collector.ts +247 -0
  265. package/src/planning/planner.ts +11 -0
  266. package/src/plugins/index.ts +4 -0
  267. package/src/plugins/plugin-registry.ts +6 -1
  268. package/src/plugins/types.ts +10 -5
  269. package/src/queue/job-queue.ts +281 -0
  270. package/src/queue/pipeline-runner.ts +149 -0
  271. package/src/runtime/admin-setup-ops.ts +664 -0
  272. package/src/runtime/chain-ops.ts +121 -0
  273. package/src/runtime/claude-md-helpers.ts +218 -0
  274. package/src/runtime/curator-extra-ops.ts +86 -3
  275. package/src/runtime/facades/admin-facade.ts +5 -2
  276. package/src/runtime/facades/agency-facade.ts +68 -0
  277. package/src/runtime/facades/brain-facade.ts +142 -1
  278. package/src/runtime/facades/control-facade.ts +45 -0
  279. package/src/runtime/facades/memory-facade.ts +20 -2
  280. package/src/runtime/facades/plan-facade.ts +2 -0
  281. package/src/runtime/facades/vault-facade.ts +28 -5
  282. package/src/runtime/intake-ops.ts +107 -5
  283. package/src/runtime/memory-extra-ops.ts +312 -4
  284. package/src/runtime/pack-ops.ts +26 -1
  285. package/src/runtime/planning-extra-ops.ts +94 -0
  286. package/src/runtime/playbook-ops.ts +1 -1
  287. package/src/runtime/plugin-ops.ts +3 -0
  288. package/src/runtime/runtime.ts +138 -2
  289. package/src/runtime/session-briefing.ts +175 -0
  290. package/src/runtime/types.ts +23 -0
  291. package/src/runtime/vault-linking-ops.ts +3 -7
  292. package/src/vault/vault.ts +105 -4
  293. package/src/__tests__/postgres-provider.test.ts +0 -116
  294. package/src/persistence/postgres-provider.ts +0 -310
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Tests for async infrastructure — event bus, job queue, pipeline runner.
3
+ * Phase 1 of #210: generic infrastructure modules.
4
+ */
5
+
6
+ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
7
+ import { TypedEventBus } from '../events/event-bus.js';
8
+ import { JobQueue } from '../queue/job-queue.js';
9
+ import { PipelineRunner } from '../queue/pipeline-runner.js';
10
+ import { Vault } from '../vault/vault.js';
11
+
12
+ // ─── Event Bus ───────────────────────────────────────────────────────
13
+
14
+ describe('TypedEventBus', () => {
15
+ type TestEvents = {
16
+ 'item:created': { id: string; title: string };
17
+ 'item:deleted': { id: string };
18
+ tick: { count: number };
19
+ };
20
+
21
+ it('emits and receives typed events', () => {
22
+ const bus = new TypedEventBus<TestEvents>();
23
+ let received: TestEvents['item:created'] | null = null;
24
+
25
+ bus.on('item:created', (payload) => {
26
+ received = payload;
27
+ });
28
+ bus.emit('item:created', { id: '1', title: 'Hello' });
29
+
30
+ expect(received).toEqual({ id: '1', title: 'Hello' });
31
+ });
32
+
33
+ it('once listener fires only once', () => {
34
+ const bus = new TypedEventBus<TestEvents>();
35
+ let count = 0;
36
+
37
+ bus.once('tick', () => {
38
+ count++;
39
+ });
40
+ bus.emit('tick', { count: 1 });
41
+ bus.emit('tick', { count: 2 });
42
+
43
+ expect(count).toBe(1);
44
+ });
45
+
46
+ it('off removes a listener', () => {
47
+ const bus = new TypedEventBus<TestEvents>();
48
+ let count = 0;
49
+ const listener = () => {
50
+ count++;
51
+ };
52
+
53
+ bus.on('tick', listener);
54
+ bus.emit('tick', { count: 1 });
55
+ bus.off('tick', listener);
56
+ bus.emit('tick', { count: 2 });
57
+
58
+ expect(count).toBe(1);
59
+ });
60
+
61
+ it('listenerCount tracks total listeners', () => {
62
+ const bus = new TypedEventBus<TestEvents>();
63
+ expect(bus.listenerCount()).toBe(0);
64
+
65
+ bus.on('item:created', () => {});
66
+ bus.on('item:deleted', () => {});
67
+ expect(bus.listenerCount()).toBe(2);
68
+ });
69
+
70
+ it('removeAllListeners clears everything', () => {
71
+ const bus = new TypedEventBus<TestEvents>();
72
+ bus.on('item:created', () => {});
73
+ bus.on('tick', () => {});
74
+ bus.removeAllListeners();
75
+ expect(bus.listenerCount()).toBe(0);
76
+ });
77
+
78
+ it('supports multiple listeners on same event', () => {
79
+ const bus = new TypedEventBus<TestEvents>();
80
+ const results: string[] = [];
81
+
82
+ bus.on('item:created', (p) => results.push('A:' + p.id));
83
+ bus.on('item:created', (p) => results.push('B:' + p.id));
84
+ bus.emit('item:created', { id: '1', title: 'Test' });
85
+
86
+ expect(results).toEqual(['A:1', 'B:1']);
87
+ });
88
+ });
89
+
90
+ // ─── Job Queue ───────────────────────────────────────────────────────
91
+
92
+ describe('JobQueue', () => {
93
+ let vault: Vault;
94
+ let queue: JobQueue;
95
+
96
+ beforeAll(() => {
97
+ vault = new Vault(':memory:');
98
+ queue = new JobQueue(vault.getProvider());
99
+ });
100
+
101
+ afterAll(() => {
102
+ vault.close();
103
+ });
104
+
105
+ it('enqueue creates a job and returns ID', () => {
106
+ const id = queue.enqueue('tag-normalize', { entryId: 'e1' });
107
+ expect(id).toBeTruthy();
108
+ expect(id.length).toBeGreaterThan(0);
109
+ });
110
+
111
+ it('dequeue returns oldest pending job', () => {
112
+ const id = queue.enqueue('dedup-check');
113
+ const job = queue.dequeue();
114
+ expect(job).not.toBeNull();
115
+ expect(job!.type).toBe('tag-normalize'); // First enqueued from previous test
116
+ });
117
+
118
+ it('dequeue marks job as running', () => {
119
+ const job = queue.get(queue.enqueue('test-job'));
120
+ expect(job!.status).toBe('pending');
121
+
122
+ const dequeued = queue.dequeue();
123
+ expect(dequeued!.status).toBe('running');
124
+ });
125
+
126
+ it('complete marks job as completed with result', () => {
127
+ const id = queue.enqueue('test-complete');
128
+ queue.dequeue(); // Mark running
129
+ queue.complete(id, { ok: true });
130
+
131
+ const job = queue.get(id);
132
+ expect(job!.status).toBe('completed');
133
+ expect(job!.result).toEqual({ ok: true });
134
+ });
135
+
136
+ it('fail marks job as failed with error', () => {
137
+ const id = queue.enqueue('test-fail');
138
+ queue.dequeue();
139
+ queue.fail(id, 'something went wrong');
140
+
141
+ const job = queue.get(id);
142
+ expect(job!.status).toBe('failed');
143
+ expect(job!.error).toBe('something went wrong');
144
+ });
145
+
146
+ it('retry resets failed job to pending', () => {
147
+ const id = queue.enqueue('test-retry');
148
+ queue.dequeue();
149
+ queue.fail(id, 'transient error');
150
+
151
+ const retried = queue.retry(id);
152
+ expect(retried).toBe(true);
153
+
154
+ const job = queue.get(id);
155
+ expect(job!.status).toBe('pending');
156
+ expect(job!.retryCount).toBe(1);
157
+ });
158
+
159
+ it('retry returns false when max retries exceeded', () => {
160
+ const id = queue.enqueue('test-max-retry', { maxRetries: 1 });
161
+ queue.dequeue();
162
+ queue.fail(id, 'fail 1');
163
+ queue.retry(id);
164
+ queue.dequeue();
165
+ queue.fail(id, 'fail 2');
166
+
167
+ const retried = queue.retry(id);
168
+ expect(retried).toBe(false);
169
+ });
170
+
171
+ it('respects DAG dependencies', () => {
172
+ const dep1 = queue.enqueue('dep-job-1');
173
+ const dep2 = queue.enqueue('dep-job-2', { dependsOn: [dep1] });
174
+
175
+ // dep2 should not dequeue because dep1 is not completed
176
+ // Clear running jobs first
177
+ const ready = queue.dequeueReady(10);
178
+ const readyIds = ready.map((j) => j.id);
179
+ expect(readyIds).toContain(dep1);
180
+ expect(readyIds).not.toContain(dep2);
181
+
182
+ // Complete dep1
183
+ queue.complete(dep1, {});
184
+
185
+ // Now dep2 should be ready
186
+ const ready2 = queue.dequeueReady(10);
187
+ const readyIds2 = ready2.map((j) => j.id);
188
+ expect(readyIds2).toContain(dep2);
189
+ });
190
+
191
+ it('groups jobs by pipeline', () => {
192
+ const pid = 'pipeline-123';
193
+ queue.enqueue('step-1', { pipelineId: pid });
194
+ queue.enqueue('step-2', { pipelineId: pid });
195
+
196
+ const jobs = queue.getByPipeline(pid);
197
+ expect(jobs.length).toBe(2);
198
+ expect(jobs.every((j) => j.pipelineId === pid)).toBe(true);
199
+ });
200
+
201
+ it('getStats returns correct counts', () => {
202
+ const stats = queue.getStats();
203
+ expect(stats.total).toBeGreaterThan(0);
204
+ expect(typeof stats.pending).toBe('number');
205
+ expect(typeof stats.running).toBe('number');
206
+ expect(typeof stats.completed).toBe('number');
207
+ expect(typeof stats.failed).toBe('number');
208
+ });
209
+
210
+ it('purge removes old completed/failed jobs', () => {
211
+ // purge with 0 days should remove all completed/failed
212
+ const purged = queue.purge(0);
213
+ expect(purged).toBeGreaterThanOrEqual(0);
214
+ });
215
+ });
216
+
217
+ // ─── Pipeline Runner ─────────────────────────────────────────────────
218
+
219
+ describe('PipelineRunner', () => {
220
+ let vault: Vault;
221
+ let queue: JobQueue;
222
+ let runner: PipelineRunner;
223
+
224
+ beforeAll(() => {
225
+ vault = new Vault(':memory:');
226
+ queue = new JobQueue(vault.getProvider());
227
+ runner = new PipelineRunner(queue, 100); // 100ms poll for tests
228
+ });
229
+
230
+ afterAll(() => {
231
+ runner.stop();
232
+ vault.close();
233
+ });
234
+
235
+ it('registerHandler stores a handler for a job type', () => {
236
+ runner.registerHandler('test-type', async () => ({ done: true }));
237
+ expect(runner.hasHandler('test-type')).toBe(true);
238
+ expect(runner.hasHandler('unknown-type')).toBe(false);
239
+ });
240
+
241
+ it('processOnce dequeues and completes jobs', async () => {
242
+ const handler = vi.fn().mockResolvedValue({ processed: true });
243
+ runner.registerHandler('process-test', handler);
244
+
245
+ queue.enqueue('process-test', { payload: { key: 'value' } });
246
+
247
+ const processed = await runner.processOnce();
248
+ expect(processed).toBe(1);
249
+ expect(handler).toHaveBeenCalledTimes(1);
250
+ });
251
+
252
+ it('processOnce fails jobs with no handler', async () => {
253
+ const id = queue.enqueue('no-handler-type');
254
+ await runner.processOnce();
255
+
256
+ const job = queue.get(id);
257
+ expect(job!.status).toBe('failed');
258
+ expect(job!.error).toContain('No handler registered');
259
+ });
260
+
261
+ it('processOnce retries on handler error', async () => {
262
+ const handler = vi.fn().mockRejectedValue(new Error('transient'));
263
+ runner.registerHandler('flaky-type', handler);
264
+
265
+ const id = queue.enqueue('flaky-type');
266
+ await runner.processOnce();
267
+
268
+ const job = queue.get(id);
269
+ // Should be pending again (retried), not failed
270
+ expect(job!.status).toBe('pending');
271
+ expect(job!.retryCount).toBe(1);
272
+ });
273
+
274
+ it('getStatus returns runner state', () => {
275
+ const status = runner.getStatus();
276
+ expect(status.running).toBe(false); // Not started yet
277
+ expect(status.pollIntervalMs).toBe(100);
278
+ expect(status.jobsProcessed).toBeGreaterThanOrEqual(1);
279
+ });
280
+
281
+ it('start/stop controls background polling', async () => {
282
+ runner.start();
283
+ expect(runner.getStatus().running).toBe(true);
284
+
285
+ runner.stop();
286
+ expect(runner.getStatus().running).toBe(false);
287
+ });
288
+
289
+ it('respects DAG in pipeline execution', async () => {
290
+ const order: string[] = [];
291
+ runner.registerHandler('dag-step', async (job) => {
292
+ order.push(job.id);
293
+ return { step: job.id };
294
+ });
295
+
296
+ const step1 = queue.enqueue('dag-step', { pipelineId: 'dag-test' });
297
+ const step2 = queue.enqueue('dag-step', { pipelineId: 'dag-test', dependsOn: [step1] });
298
+
299
+ // First batch: only step1 should process
300
+ await runner.processOnce();
301
+ expect(order).toEqual([step1]);
302
+
303
+ // Second batch: step2 should now be ready
304
+ await runner.processOnce();
305
+ expect(order).toEqual([step1, step2]);
306
+ });
307
+ });
@@ -357,7 +357,10 @@ describe('CogneeClient — gap coverage', () => {
357
357
  mockWithAuth(
358
358
  async () =>
359
359
  new Response(
360
- JSON.stringify([{ id: 'r1', text: 42, score: 0.8 }, { id: 'r2', score: 0.7 }]),
360
+ JSON.stringify([
361
+ { id: 'r1', text: 42, score: 0.8 },
362
+ { id: 'r2', score: 0.7 },
363
+ ]),
361
364
  { status: 200 },
362
365
  ),
363
366
  );
@@ -440,7 +443,8 @@ describe('CogneeClient — gap coverage', () => {
440
443
 
441
444
  it('should give single result score 1.0', async () => {
442
445
  mockWithAuth(
443
- async () => new Response(JSON.stringify([{ id: 'only', text: 'Sole result' }]), { status: 200 }),
446
+ async () =>
447
+ new Response(JSON.stringify([{ id: 'only', text: 'Sole result' }]), { status: 200 }),
444
448
  );
445
449
  const client = new CogneeClient();
446
450
  await client.healthCheck();
@@ -15,9 +15,6 @@
15
15
  */
16
16
 
17
17
  import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
18
- import { mkdirSync, rmSync } from 'node:fs';
19
- import { join } from 'node:path';
20
- import { tmpdir } from 'node:os';
21
18
  import { Brain } from '../brain/brain.js';
22
19
  import { Vault } from '../vault/vault.js';
23
20
  import { CogneeClient } from '../cognee/client.js';
@@ -46,16 +43,22 @@ function makeEntry(overrides: Partial<IntelligenceEntry> = {}): IntelligenceEntr
46
43
  * Create a mock CogneeClient that returns controlled search results.
47
44
  * isAvailable is a getter — can be toggled at runtime.
48
45
  */
49
- function makeMockCognee(opts: {
50
- available?: boolean;
51
- searchResults?: CogneeSearchResult[];
52
- } = {}): CogneeClient & { _setAvailable: (v: boolean) => void } {
46
+ function makeMockCognee(
47
+ opts: {
48
+ available?: boolean;
49
+ searchResults?: CogneeSearchResult[];
50
+ } = {},
51
+ ): CogneeClient & { _setAvailable: (v: boolean) => void } {
53
52
  let available = opts.available ?? true;
54
53
  const searchResults = opts.searchResults ?? [];
55
54
 
56
55
  return {
57
- get isAvailable() { return available; },
58
- _setAvailable(v: boolean) { available = v; },
56
+ get isAvailable() {
57
+ return available;
58
+ },
59
+ _setAvailable(v: boolean) {
60
+ available = v;
61
+ },
59
62
  healthCheck: vi.fn().mockResolvedValue({ available, url: 'mock', latencyMs: 1 }),
60
63
  addEntries: vi.fn().mockResolvedValue({ added: 0 }),
61
64
  deleteEntries: vi.fn().mockResolvedValue({ deleted: 0 }),
@@ -74,7 +77,8 @@ const SEED_ENTRIES: IntelligenceEntry[] = [
74
77
  makeEntry({
75
78
  id: 'pattern-retry-backoff',
76
79
  title: 'Retry with Exponential Backoff',
77
- description: 'Always use exponential backoff when retrying failed network requests to avoid thundering herd.',
80
+ description:
81
+ 'Always use exponential backoff when retrying failed network requests to avoid thundering herd.',
78
82
  domain: 'architecture',
79
83
  severity: 'critical',
80
84
  tags: ['networking', 'retry', 'resilience'],
@@ -83,7 +87,8 @@ const SEED_ENTRIES: IntelligenceEntry[] = [
83
87
  makeEntry({
84
88
  id: 'pattern-circuit-breaker',
85
89
  title: 'Circuit Breaker for External Services',
86
- description: 'Wrap external service calls in a circuit breaker to prevent cascade failures when downstream is unhealthy.',
90
+ description:
91
+ 'Wrap external service calls in a circuit breaker to prevent cascade failures when downstream is unhealthy.',
87
92
  domain: 'architecture',
88
93
  severity: 'critical',
89
94
  tags: ['networking', 'resilience', 'microservices'],
@@ -93,7 +98,8 @@ const SEED_ENTRIES: IntelligenceEntry[] = [
93
98
  id: 'anti-pattern-polling-no-timeout',
94
99
  type: 'anti-pattern',
95
100
  title: 'Polling Without Timeout',
96
- description: 'Never poll an external service without a maximum timeout or circuit breaker. Leads to resource exhaustion.',
101
+ description:
102
+ 'Never poll an external service without a maximum timeout or circuit breaker. Leads to resource exhaustion.',
97
103
  domain: 'architecture',
98
104
  severity: 'critical',
99
105
  tags: ['networking', 'polling', 'timeout'],
@@ -101,7 +107,8 @@ const SEED_ENTRIES: IntelligenceEntry[] = [
101
107
  makeEntry({
102
108
  id: 'pattern-token-semantic',
103
109
  title: 'Semantic Token Priority',
104
- description: 'Use semantic tokens over primitive tokens. Semantic tokens communicate intent, not just values.',
110
+ description:
111
+ 'Use semantic tokens over primitive tokens. Semantic tokens communicate intent, not just values.',
105
112
  domain: 'design',
106
113
  severity: 'warning',
107
114
  tags: ['tokens', 'design-system', 'css'],
@@ -109,7 +116,8 @@ const SEED_ENTRIES: IntelligenceEntry[] = [
109
116
  makeEntry({
110
117
  id: 'pattern-fts5-search',
111
118
  title: 'FTS5 Full-Text Search with Porter Stemming',
112
- description: 'Use SQLite FTS5 with porter tokenizer for all text search in the vault. BM25 ranking for relevance.',
119
+ description:
120
+ 'Use SQLite FTS5 with porter tokenizer for all text search in the vault. BM25 ranking for relevance.',
113
121
  domain: 'architecture',
114
122
  severity: 'suggestion',
115
123
  tags: ['search', 'sqlite', 'fts5'],
@@ -129,7 +137,11 @@ describe('Hybrid search: Cognee → FTS5 → Zettelkasten', () => {
129
137
 
130
138
  // Create Zettelkasten links between related entries
131
139
  linkManager.addLink('pattern-retry-backoff', 'pattern-circuit-breaker', 'extends');
132
- linkManager.addLink('anti-pattern-polling-no-timeout', 'pattern-circuit-breaker', 'contradicts');
140
+ linkManager.addLink(
141
+ 'anti-pattern-polling-no-timeout',
142
+ 'pattern-circuit-breaker',
143
+ 'contradicts',
144
+ );
133
145
  linkManager.addLink('pattern-retry-backoff', 'anti-pattern-polling-no-timeout', 'contradicts');
134
146
  });
135
147
 
@@ -147,14 +159,14 @@ describe('Hybrid search: Cognee → FTS5 → Zettelkasten', () => {
147
159
  const results = await brain.intelligentSearch('retry network requests');
148
160
  expect(results.length).toBeGreaterThan(0);
149
161
  // Vector score should be 0 for all results (no Cognee)
150
- expect(results.every(r => r.breakdown.vector === 0)).toBe(true);
162
+ expect(results.every((r) => r.breakdown.vector === 0)).toBe(true);
151
163
  });
152
164
 
153
165
  it('should match by keyword in FTS5', async () => {
154
166
  const brain = new Brain(vault); // No Cognee at all
155
167
 
156
168
  const results = await brain.intelligentSearch('exponential backoff');
157
- const ids = results.map(r => r.entry.id);
169
+ const ids = results.map((r) => r.entry.id);
158
170
  expect(ids).toContain('pattern-retry-backoff');
159
171
  });
160
172
 
@@ -163,8 +175,8 @@ describe('Hybrid search: Cognee → FTS5 → Zettelkasten', () => {
163
175
 
164
176
  const results = await brain.intelligentSearch('networking resilience');
165
177
  // Critical entries should rank above suggestion entries for same domain
166
- const criticalIdx = results.findIndex(r => r.entry.id === 'pattern-retry-backoff');
167
- const suggestionIdx = results.findIndex(r => r.entry.id === 'pattern-fts5-search');
178
+ const criticalIdx = results.findIndex((r) => r.entry.id === 'pattern-retry-backoff');
179
+ const suggestionIdx = results.findIndex((r) => r.entry.id === 'pattern-fts5-search');
168
180
  if (criticalIdx >= 0 && suggestionIdx >= 0) {
169
181
  expect(criticalIdx).toBeLessThan(suggestionIdx);
170
182
  }
@@ -192,7 +204,7 @@ describe('Hybrid search: Cognee → FTS5 → Zettelkasten', () => {
192
204
  const results = await brain.intelligentSearch('how to handle failing external services');
193
205
 
194
206
  // Circuit breaker should rank high due to Cognee vector boost
195
- const cbResult = results.find(r => r.entry.id === 'pattern-circuit-breaker');
207
+ const cbResult = results.find((r) => r.entry.id === 'pattern-circuit-breaker');
196
208
  expect(cbResult).toBeDefined();
197
209
  expect(cbResult!.breakdown.vector).toBeGreaterThan(0);
198
210
  });
@@ -212,7 +224,7 @@ describe('Hybrid search: Cognee → FTS5 → Zettelkasten', () => {
212
224
  const brain = new Brain(vault, mockCognee);
213
225
 
214
226
  const results = await brain.intelligentSearch('retry strategy');
215
- const retryResult = results.find(r => r.entry.id === 'pattern-retry-backoff');
227
+ const retryResult = results.find((r) => r.entry.id === 'pattern-retry-backoff');
216
228
  expect(retryResult).toBeDefined();
217
229
  // Should have vector score from Cognee cross-reference
218
230
  expect(retryResult!.breakdown.vector).toBeGreaterThan(0);
@@ -234,7 +246,7 @@ describe('Hybrid search: Cognee → FTS5 → Zettelkasten', () => {
234
246
  const brain = new Brain(vault, mockCognee);
235
247
 
236
248
  const results = await brain.intelligentSearch('design tokens priority');
237
- const tokenResult = results.find(r => r.entry.id === 'pattern-token-semantic');
249
+ const tokenResult = results.find((r) => r.entry.id === 'pattern-token-semantic');
238
250
  expect(tokenResult).toBeDefined();
239
251
  expect(tokenResult!.breakdown.vector).toBeGreaterThan(0);
240
252
  });
@@ -254,7 +266,7 @@ describe('Hybrid search: Cognee → FTS5 → Zettelkasten', () => {
254
266
  const brain = new Brain(vault, mockCognee);
255
267
 
256
268
  const results = await brain.intelligentSearch('retry');
257
- const retryResult = results.find(r => r.entry.id === 'pattern-retry-backoff');
269
+ const retryResult = results.find((r) => r.entry.id === 'pattern-retry-backoff');
258
270
  expect(retryResult).toBeDefined();
259
271
  // With COGNEE_WEIGHTS, vector component is 35% of total
260
272
  // So vector score should contribute meaningfully
@@ -271,7 +283,7 @@ describe('Hybrid search: Cognee → FTS5 → Zettelkasten', () => {
271
283
 
272
284
  const results = await brain.intelligentSearch('exponential backoff');
273
285
  // All results should have vector=0 (no Cognee matches)
274
- expect(results.every(r => r.breakdown.vector === 0)).toBe(true);
286
+ expect(results.every((r) => r.breakdown.vector === 0)).toBe(true);
275
287
  });
276
288
  });
277
289
 
@@ -280,14 +292,16 @@ describe('Hybrid search: Cognee → FTS5 → Zettelkasten', () => {
280
292
  describe('graceful degradation', () => {
281
293
  it('should fall back to FTS5 when Cognee search throws', async () => {
282
294
  const mockCognee = makeMockCognee({ available: true });
283
- (mockCognee.search as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('Cognee crashed'));
295
+ (mockCognee.search as ReturnType<typeof vi.fn>).mockRejectedValue(
296
+ new Error('Cognee crashed'),
297
+ );
284
298
  const brain = new Brain(vault, mockCognee);
285
299
 
286
300
  const results = await brain.intelligentSearch('retry backoff');
287
301
  // Should still return FTS5 results despite Cognee failure
288
302
  expect(results.length).toBeGreaterThan(0);
289
303
  // All vector scores should be 0 (Cognee failed)
290
- expect(results.every(r => r.breakdown.vector === 0)).toBe(true);
304
+ expect(results.every((r) => r.breakdown.vector === 0)).toBe(true);
291
305
  });
292
306
 
293
307
  it('should handle Cognee becoming unavailable mid-session', async () => {
@@ -306,7 +320,7 @@ describe('Hybrid search: Cognee → FTS5 → Zettelkasten', () => {
306
320
 
307
321
  // First search — Cognee available
308
322
  const results1 = await brain.intelligentSearch('retry');
309
- const hasVector1 = results1.some(r => r.breakdown.vector > 0);
323
+ const hasVector1 = results1.some((r) => r.breakdown.vector > 0);
310
324
  expect(hasVector1).toBe(true);
311
325
 
312
326
  // Cognee goes down
@@ -315,7 +329,7 @@ describe('Hybrid search: Cognee → FTS5 → Zettelkasten', () => {
315
329
  // Second search — should fall back gracefully
316
330
  const results2 = await brain.intelligentSearch('retry');
317
331
  expect(results2.length).toBeGreaterThan(0);
318
- expect(results2.every(r => r.breakdown.vector === 0)).toBe(true);
332
+ expect(results2.every((r) => r.breakdown.vector === 0)).toBe(true);
319
333
  });
320
334
  });
321
335
 
@@ -356,8 +370,8 @@ describe('Hybrid search: Cognee → FTS5 → Zettelkasten', () => {
356
370
  const withoutDomain = await brain.intelligentSearch('resilience');
357
371
 
358
372
  // When domain filter matches, domainMatch should be higher
359
- const archResult = withDomain.find(r => r.entry.domain === 'architecture');
360
- const noFilterResult = withoutDomain.find(r => r.entry.id === archResult?.entry.id);
373
+ const archResult = withDomain.find((r) => r.entry.domain === 'architecture');
374
+ const noFilterResult = withoutDomain.find((r) => r.entry.id === archResult?.entry.id);
361
375
 
362
376
  if (archResult && noFilterResult) {
363
377
  expect(archResult.breakdown.domainMatch).toBeGreaterThanOrEqual(
@@ -373,7 +387,7 @@ describe('Hybrid search: Cognee → FTS5 → Zettelkasten', () => {
373
387
  it('should find connected entries via link traversal', () => {
374
388
  // Search finds retry-backoff → traverse links → discover circuit-breaker
375
389
  const links = linkManager.getLinks('pattern-retry-backoff');
376
- const linkedIds = links.map(l =>
390
+ const linkedIds = links.map((l) =>
377
391
  l.sourceId === 'pattern-retry-backoff' ? l.targetId : l.sourceId,
378
392
  );
379
393
  expect(linkedIds).toContain('pattern-circuit-breaker');
@@ -383,14 +397,14 @@ describe('Hybrid search: Cognee → FTS5 → Zettelkasten', () => {
383
397
  it('should traverse 2 hops to discover indirect connections', () => {
384
398
  // retry-backoff → circuit-breaker → polling-no-timeout (via contradicts)
385
399
  const traversed = linkManager.traverse('pattern-retry-backoff', 2);
386
- const ids = traversed.map(e => e.id);
400
+ const ids = traversed.map((e) => e.id);
387
401
  expect(ids).toContain('pattern-circuit-breaker');
388
402
  expect(ids).toContain('anti-pattern-polling-no-timeout');
389
403
  });
390
404
 
391
405
  it('should identify contradicting anti-patterns', () => {
392
406
  const links = linkManager.getLinks('anti-pattern-polling-no-timeout');
393
- const contradictions = links.filter(l => l.linkType === 'contradicts');
407
+ const contradictions = links.filter((l) => l.linkType === 'contradicts');
394
408
  expect(contradictions.length).toBeGreaterThan(0);
395
409
  });
396
410
  });
@@ -424,7 +438,7 @@ describe('Hybrid search: Cognee → FTS5 → Zettelkasten', () => {
424
438
  type: 'anti-pattern',
425
439
  });
426
440
  if (results.length > 0) {
427
- expect(results.every(r => r.entry.type === 'anti-pattern')).toBe(true);
441
+ expect(results.every((r) => r.entry.type === 'anti-pattern')).toBe(true);
428
442
  }
429
443
  });
430
444
 
@@ -450,7 +464,7 @@ describe('Hybrid search: Cognee → FTS5 → Zettelkasten', () => {
450
464
 
451
465
  const results = await brain.intelligentSearch('retry');
452
466
  // Should not crash and should use the higher score
453
- const retryResult = results.find(r => r.entry.id === 'pattern-retry-backoff');
467
+ const retryResult = results.find((r) => r.entry.id === 'pattern-retry-backoff');
454
468
  expect(retryResult).toBeDefined();
455
469
  // Should use max score (0.9, not 0.7)
456
470
  expect(retryResult!.breakdown.vector).toBeCloseTo(0.9, 1);