@soleri/core 2.4.0 → 2.6.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 (328) hide show
  1. package/dist/brain/brain.d.ts +7 -0
  2. package/dist/brain/brain.d.ts.map +1 -1
  3. package/dist/brain/brain.js +56 -9
  4. package/dist/brain/brain.js.map +1 -1
  5. package/dist/brain/intelligence.d.ts +1 -0
  6. package/dist/brain/intelligence.d.ts.map +1 -1
  7. package/dist/brain/intelligence.js +164 -148
  8. package/dist/brain/intelligence.js.map +1 -1
  9. package/dist/brain/types.d.ts +2 -2
  10. package/dist/brain/types.d.ts.map +1 -1
  11. package/dist/cognee/client.d.ts +3 -0
  12. package/dist/cognee/client.d.ts.map +1 -1
  13. package/dist/cognee/client.js +17 -0
  14. package/dist/cognee/client.js.map +1 -1
  15. package/dist/cognee/sync-manager.d.ts +94 -0
  16. package/dist/cognee/sync-manager.d.ts.map +1 -0
  17. package/dist/cognee/sync-manager.js +293 -0
  18. package/dist/cognee/sync-manager.js.map +1 -0
  19. package/dist/control/identity-manager.d.ts +3 -1
  20. package/dist/control/identity-manager.d.ts.map +1 -1
  21. package/dist/control/identity-manager.js +49 -51
  22. package/dist/control/identity-manager.js.map +1 -1
  23. package/dist/control/intent-router.d.ts +1 -0
  24. package/dist/control/intent-router.d.ts.map +1 -1
  25. package/dist/control/intent-router.js +32 -32
  26. package/dist/control/intent-router.js.map +1 -1
  27. package/dist/curator/curator.d.ts +9 -1
  28. package/dist/curator/curator.d.ts.map +1 -1
  29. package/dist/curator/curator.js +104 -92
  30. package/dist/curator/curator.js.map +1 -1
  31. package/dist/errors/classify.d.ts +13 -0
  32. package/dist/errors/classify.d.ts.map +1 -0
  33. package/dist/errors/classify.js +97 -0
  34. package/dist/errors/classify.js.map +1 -0
  35. package/dist/errors/index.d.ts +6 -0
  36. package/dist/errors/index.d.ts.map +1 -0
  37. package/dist/errors/index.js +4 -0
  38. package/dist/errors/index.js.map +1 -0
  39. package/dist/errors/retry.d.ts +40 -0
  40. package/dist/errors/retry.d.ts.map +1 -0
  41. package/dist/errors/retry.js +97 -0
  42. package/dist/errors/retry.js.map +1 -0
  43. package/dist/errors/types.d.ts +48 -0
  44. package/dist/errors/types.d.ts.map +1 -0
  45. package/dist/errors/types.js +59 -0
  46. package/dist/errors/types.js.map +1 -0
  47. package/dist/governance/governance.d.ts +1 -0
  48. package/dist/governance/governance.d.ts.map +1 -1
  49. package/dist/governance/governance.js +51 -68
  50. package/dist/governance/governance.js.map +1 -1
  51. package/dist/index.d.ts +26 -5
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +22 -3
  54. package/dist/index.js.map +1 -1
  55. package/dist/intake/content-classifier.d.ts +14 -0
  56. package/dist/intake/content-classifier.d.ts.map +1 -0
  57. package/dist/intake/content-classifier.js +125 -0
  58. package/dist/intake/content-classifier.js.map +1 -0
  59. package/dist/intake/dedup-gate.d.ts +17 -0
  60. package/dist/intake/dedup-gate.d.ts.map +1 -0
  61. package/dist/intake/dedup-gate.js +66 -0
  62. package/dist/intake/dedup-gate.js.map +1 -0
  63. package/dist/intake/intake-pipeline.d.ts +63 -0
  64. package/dist/intake/intake-pipeline.d.ts.map +1 -0
  65. package/dist/intake/intake-pipeline.js +373 -0
  66. package/dist/intake/intake-pipeline.js.map +1 -0
  67. package/dist/intake/types.d.ts +65 -0
  68. package/dist/intake/types.d.ts.map +1 -0
  69. package/dist/intake/types.js +3 -0
  70. package/dist/intake/types.js.map +1 -0
  71. package/dist/intelligence/loader.js +1 -1
  72. package/dist/intelligence/loader.js.map +1 -1
  73. package/dist/intelligence/types.d.ts +3 -1
  74. package/dist/intelligence/types.d.ts.map +1 -1
  75. package/dist/loop/loop-manager.d.ts +58 -7
  76. package/dist/loop/loop-manager.d.ts.map +1 -1
  77. package/dist/loop/loop-manager.js +280 -6
  78. package/dist/loop/loop-manager.js.map +1 -1
  79. package/dist/loop/types.d.ts +69 -1
  80. package/dist/loop/types.d.ts.map +1 -1
  81. package/dist/loop/types.js +4 -1
  82. package/dist/loop/types.js.map +1 -1
  83. package/dist/persistence/index.d.ts +4 -0
  84. package/dist/persistence/index.d.ts.map +1 -0
  85. package/dist/persistence/index.js +3 -0
  86. package/dist/persistence/index.js.map +1 -0
  87. package/dist/persistence/postgres-provider.d.ts +46 -0
  88. package/dist/persistence/postgres-provider.d.ts.map +1 -0
  89. package/dist/persistence/postgres-provider.js +115 -0
  90. package/dist/persistence/postgres-provider.js.map +1 -0
  91. package/dist/persistence/sqlite-provider.d.ts +28 -0
  92. package/dist/persistence/sqlite-provider.d.ts.map +1 -0
  93. package/dist/persistence/sqlite-provider.js +97 -0
  94. package/dist/persistence/sqlite-provider.js.map +1 -0
  95. package/dist/persistence/types.d.ts +58 -0
  96. package/dist/persistence/types.d.ts.map +1 -0
  97. package/dist/persistence/types.js +8 -0
  98. package/dist/persistence/types.js.map +1 -0
  99. package/dist/planning/gap-analysis.d.ts +47 -4
  100. package/dist/planning/gap-analysis.d.ts.map +1 -1
  101. package/dist/planning/gap-analysis.js +190 -13
  102. package/dist/planning/gap-analysis.js.map +1 -1
  103. package/dist/planning/gap-types.d.ts +1 -1
  104. package/dist/planning/gap-types.d.ts.map +1 -1
  105. package/dist/planning/gap-types.js.map +1 -1
  106. package/dist/planning/planner.d.ts +277 -9
  107. package/dist/planning/planner.d.ts.map +1 -1
  108. package/dist/planning/planner.js +611 -46
  109. package/dist/planning/planner.js.map +1 -1
  110. package/dist/playbooks/generic/brainstorming.d.ts +9 -0
  111. package/dist/playbooks/generic/brainstorming.d.ts.map +1 -0
  112. package/dist/playbooks/generic/brainstorming.js +105 -0
  113. package/dist/playbooks/generic/brainstorming.js.map +1 -0
  114. package/dist/playbooks/generic/code-review.d.ts +11 -0
  115. package/dist/playbooks/generic/code-review.d.ts.map +1 -0
  116. package/dist/playbooks/generic/code-review.js +176 -0
  117. package/dist/playbooks/generic/code-review.js.map +1 -0
  118. package/dist/playbooks/generic/subagent-execution.d.ts +9 -0
  119. package/dist/playbooks/generic/subagent-execution.d.ts.map +1 -0
  120. package/dist/playbooks/generic/subagent-execution.js +68 -0
  121. package/dist/playbooks/generic/subagent-execution.js.map +1 -0
  122. package/dist/playbooks/generic/systematic-debugging.d.ts +9 -0
  123. package/dist/playbooks/generic/systematic-debugging.d.ts.map +1 -0
  124. package/dist/playbooks/generic/systematic-debugging.js +87 -0
  125. package/dist/playbooks/generic/systematic-debugging.js.map +1 -0
  126. package/dist/playbooks/generic/tdd.d.ts +9 -0
  127. package/dist/playbooks/generic/tdd.d.ts.map +1 -0
  128. package/dist/playbooks/generic/tdd.js +70 -0
  129. package/dist/playbooks/generic/tdd.js.map +1 -0
  130. package/dist/playbooks/generic/verification.d.ts +9 -0
  131. package/dist/playbooks/generic/verification.d.ts.map +1 -0
  132. package/dist/playbooks/generic/verification.js +74 -0
  133. package/dist/playbooks/generic/verification.js.map +1 -0
  134. package/dist/playbooks/index.d.ts +4 -0
  135. package/dist/playbooks/index.d.ts.map +1 -0
  136. package/dist/playbooks/index.js +5 -0
  137. package/dist/playbooks/index.js.map +1 -0
  138. package/dist/playbooks/playbook-registry.d.ts +42 -0
  139. package/dist/playbooks/playbook-registry.d.ts.map +1 -0
  140. package/dist/playbooks/playbook-registry.js +227 -0
  141. package/dist/playbooks/playbook-registry.js.map +1 -0
  142. package/dist/playbooks/playbook-seeder.d.ts +47 -0
  143. package/dist/playbooks/playbook-seeder.d.ts.map +1 -0
  144. package/dist/playbooks/playbook-seeder.js +104 -0
  145. package/dist/playbooks/playbook-seeder.js.map +1 -0
  146. package/dist/playbooks/playbook-types.d.ts +132 -0
  147. package/dist/playbooks/playbook-types.d.ts.map +1 -0
  148. package/dist/playbooks/playbook-types.js +12 -0
  149. package/dist/playbooks/playbook-types.js.map +1 -0
  150. package/dist/project/project-registry.d.ts +4 -4
  151. package/dist/project/project-registry.d.ts.map +1 -1
  152. package/dist/project/project-registry.js +30 -57
  153. package/dist/project/project-registry.js.map +1 -1
  154. package/dist/prompts/index.d.ts +4 -0
  155. package/dist/prompts/index.d.ts.map +1 -0
  156. package/dist/prompts/index.js +3 -0
  157. package/dist/prompts/index.js.map +1 -0
  158. package/dist/prompts/parser.d.ts +17 -0
  159. package/dist/prompts/parser.d.ts.map +1 -0
  160. package/dist/prompts/parser.js +47 -0
  161. package/dist/prompts/parser.js.map +1 -0
  162. package/dist/prompts/template-manager.d.ts +25 -0
  163. package/dist/prompts/template-manager.d.ts.map +1 -0
  164. package/dist/prompts/template-manager.js +71 -0
  165. package/dist/prompts/template-manager.js.map +1 -0
  166. package/dist/prompts/types.d.ts +26 -0
  167. package/dist/prompts/types.d.ts.map +1 -0
  168. package/dist/prompts/types.js +5 -0
  169. package/dist/prompts/types.js.map +1 -0
  170. package/dist/runtime/admin-extra-ops.d.ts +5 -3
  171. package/dist/runtime/admin-extra-ops.d.ts.map +1 -1
  172. package/dist/runtime/admin-extra-ops.js +348 -11
  173. package/dist/runtime/admin-extra-ops.js.map +1 -1
  174. package/dist/runtime/admin-ops.d.ts.map +1 -1
  175. package/dist/runtime/admin-ops.js +10 -3
  176. package/dist/runtime/admin-ops.js.map +1 -1
  177. package/dist/runtime/capture-ops.d.ts.map +1 -1
  178. package/dist/runtime/capture-ops.js +20 -2
  179. package/dist/runtime/capture-ops.js.map +1 -1
  180. package/dist/runtime/cognee-sync-ops.d.ts +12 -0
  181. package/dist/runtime/cognee-sync-ops.d.ts.map +1 -0
  182. package/dist/runtime/cognee-sync-ops.js +55 -0
  183. package/dist/runtime/cognee-sync-ops.js.map +1 -0
  184. package/dist/runtime/core-ops.d.ts +8 -6
  185. package/dist/runtime/core-ops.d.ts.map +1 -1
  186. package/dist/runtime/core-ops.js +226 -9
  187. package/dist/runtime/core-ops.js.map +1 -1
  188. package/dist/runtime/curator-extra-ops.d.ts +2 -2
  189. package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
  190. package/dist/runtime/curator-extra-ops.js +15 -3
  191. package/dist/runtime/curator-extra-ops.js.map +1 -1
  192. package/dist/runtime/domain-ops.js +2 -2
  193. package/dist/runtime/domain-ops.js.map +1 -1
  194. package/dist/runtime/grading-ops.d.ts.map +1 -1
  195. package/dist/runtime/grading-ops.js.map +1 -1
  196. package/dist/runtime/intake-ops.d.ts +14 -0
  197. package/dist/runtime/intake-ops.d.ts.map +1 -0
  198. package/dist/runtime/intake-ops.js +110 -0
  199. package/dist/runtime/intake-ops.js.map +1 -0
  200. package/dist/runtime/loop-ops.d.ts +5 -4
  201. package/dist/runtime/loop-ops.d.ts.map +1 -1
  202. package/dist/runtime/loop-ops.js +84 -12
  203. package/dist/runtime/loop-ops.js.map +1 -1
  204. package/dist/runtime/memory-cross-project-ops.d.ts.map +1 -1
  205. package/dist/runtime/memory-cross-project-ops.js.map +1 -1
  206. package/dist/runtime/memory-extra-ops.js +5 -5
  207. package/dist/runtime/memory-extra-ops.js.map +1 -1
  208. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  209. package/dist/runtime/orchestrate-ops.js +8 -2
  210. package/dist/runtime/orchestrate-ops.js.map +1 -1
  211. package/dist/runtime/planning-extra-ops.d.ts +13 -5
  212. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  213. package/dist/runtime/planning-extra-ops.js +381 -18
  214. package/dist/runtime/planning-extra-ops.js.map +1 -1
  215. package/dist/runtime/playbook-ops.d.ts +14 -0
  216. package/dist/runtime/playbook-ops.d.ts.map +1 -0
  217. package/dist/runtime/playbook-ops.js +141 -0
  218. package/dist/runtime/playbook-ops.js.map +1 -0
  219. package/dist/runtime/project-ops.d.ts.map +1 -1
  220. package/dist/runtime/project-ops.js +7 -2
  221. package/dist/runtime/project-ops.js.map +1 -1
  222. package/dist/runtime/runtime.d.ts.map +1 -1
  223. package/dist/runtime/runtime.js +28 -9
  224. package/dist/runtime/runtime.js.map +1 -1
  225. package/dist/runtime/types.d.ts +8 -0
  226. package/dist/runtime/types.d.ts.map +1 -1
  227. package/dist/runtime/vault-extra-ops.d.ts +4 -2
  228. package/dist/runtime/vault-extra-ops.d.ts.map +1 -1
  229. package/dist/runtime/vault-extra-ops.js +383 -4
  230. package/dist/runtime/vault-extra-ops.js.map +1 -1
  231. package/dist/vault/playbook.d.ts +34 -0
  232. package/dist/vault/playbook.d.ts.map +1 -0
  233. package/dist/vault/playbook.js +60 -0
  234. package/dist/vault/playbook.js.map +1 -0
  235. package/dist/vault/vault.d.ts +52 -32
  236. package/dist/vault/vault.d.ts.map +1 -1
  237. package/dist/vault/vault.js +300 -181
  238. package/dist/vault/vault.js.map +1 -1
  239. package/package.json +9 -3
  240. package/src/__tests__/admin-extra-ops.test.ts +62 -15
  241. package/src/__tests__/admin-ops.test.ts +2 -2
  242. package/src/__tests__/brain.test.ts +3 -3
  243. package/src/__tests__/cognee-integration.test.ts +80 -0
  244. package/src/__tests__/cognee-sync-manager.test.ts +103 -0
  245. package/src/__tests__/core-ops.test.ts +36 -4
  246. package/src/__tests__/curator-extra-ops.test.ts +24 -2
  247. package/src/__tests__/errors.test.ts +388 -0
  248. package/src/__tests__/grading-ops.test.ts +28 -7
  249. package/src/__tests__/intake-pipeline.test.ts +162 -0
  250. package/src/__tests__/loop-ops.test.ts +74 -3
  251. package/src/__tests__/memory-cross-project-ops.test.ts +3 -1
  252. package/src/__tests__/orchestrate-ops.test.ts +8 -3
  253. package/src/__tests__/persistence.test.ts +291 -0
  254. package/src/__tests__/planner.test.ts +99 -21
  255. package/src/__tests__/planning-extra-ops.test.ts +168 -10
  256. package/src/__tests__/playbook-registry.test.ts +326 -0
  257. package/src/__tests__/playbook-seeder.test.ts +163 -0
  258. package/src/__tests__/playbook.test.ts +389 -0
  259. package/src/__tests__/postgres-provider.test.ts +58 -0
  260. package/src/__tests__/project-ops.test.ts +18 -4
  261. package/src/__tests__/template-manager.test.ts +222 -0
  262. package/src/__tests__/vault-extra-ops.test.ts +82 -7
  263. package/src/__tests__/vault.test.ts +184 -0
  264. package/src/brain/brain.ts +71 -9
  265. package/src/brain/intelligence.ts +258 -307
  266. package/src/brain/types.ts +2 -2
  267. package/src/cognee/client.ts +18 -0
  268. package/src/cognee/sync-manager.ts +389 -0
  269. package/src/control/identity-manager.ts +77 -75
  270. package/src/control/intent-router.ts +55 -57
  271. package/src/curator/curator.ts +199 -139
  272. package/src/errors/classify.ts +102 -0
  273. package/src/errors/index.ts +5 -0
  274. package/src/errors/retry.ts +132 -0
  275. package/src/errors/types.ts +81 -0
  276. package/src/governance/governance.ts +90 -107
  277. package/src/index.ts +116 -3
  278. package/src/intake/content-classifier.ts +146 -0
  279. package/src/intake/dedup-gate.ts +92 -0
  280. package/src/intake/intake-pipeline.ts +503 -0
  281. package/src/intake/types.ts +69 -0
  282. package/src/intelligence/loader.ts +1 -1
  283. package/src/intelligence/types.ts +3 -1
  284. package/src/loop/loop-manager.ts +325 -7
  285. package/src/loop/types.ts +72 -1
  286. package/src/persistence/index.ts +9 -0
  287. package/src/persistence/postgres-provider.ts +157 -0
  288. package/src/persistence/sqlite-provider.ts +115 -0
  289. package/src/persistence/types.ts +74 -0
  290. package/src/planning/gap-analysis.ts +286 -17
  291. package/src/planning/gap-types.ts +4 -1
  292. package/src/planning/planner.ts +828 -55
  293. package/src/playbooks/generic/brainstorming.ts +110 -0
  294. package/src/playbooks/generic/code-review.ts +181 -0
  295. package/src/playbooks/generic/subagent-execution.ts +74 -0
  296. package/src/playbooks/generic/systematic-debugging.ts +92 -0
  297. package/src/playbooks/generic/tdd.ts +75 -0
  298. package/src/playbooks/generic/verification.ts +79 -0
  299. package/src/playbooks/index.ts +27 -0
  300. package/src/playbooks/playbook-registry.ts +284 -0
  301. package/src/playbooks/playbook-seeder.ts +119 -0
  302. package/src/playbooks/playbook-types.ts +162 -0
  303. package/src/project/project-registry.ts +81 -74
  304. package/src/prompts/index.ts +3 -0
  305. package/src/prompts/parser.ts +59 -0
  306. package/src/prompts/template-manager.ts +77 -0
  307. package/src/prompts/types.ts +28 -0
  308. package/src/runtime/admin-extra-ops.ts +391 -13
  309. package/src/runtime/admin-ops.ts +17 -6
  310. package/src/runtime/capture-ops.ts +25 -6
  311. package/src/runtime/cognee-sync-ops.ts +63 -0
  312. package/src/runtime/core-ops.ts +258 -8
  313. package/src/runtime/curator-extra-ops.ts +17 -3
  314. package/src/runtime/domain-ops.ts +2 -2
  315. package/src/runtime/grading-ops.ts +11 -2
  316. package/src/runtime/intake-ops.ts +126 -0
  317. package/src/runtime/loop-ops.ts +96 -13
  318. package/src/runtime/memory-cross-project-ops.ts +1 -2
  319. package/src/runtime/memory-extra-ops.ts +5 -5
  320. package/src/runtime/orchestrate-ops.ts +8 -2
  321. package/src/runtime/planning-extra-ops.ts +414 -23
  322. package/src/runtime/playbook-ops.ts +169 -0
  323. package/src/runtime/project-ops.ts +9 -3
  324. package/src/runtime/runtime.ts +36 -10
  325. package/src/runtime/types.ts +8 -0
  326. package/src/runtime/vault-extra-ops.ts +425 -4
  327. package/src/vault/playbook.ts +87 -0
  328. package/src/vault/vault.ts +419 -235
@@ -0,0 +1,388 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import {
3
+ SoleriErrorCode,
4
+ SoleriError,
5
+ ok,
6
+ err,
7
+ isOk,
8
+ isErr,
9
+ classifyError,
10
+ shouldRetry,
11
+ getRetryDelay,
12
+ retryWithPreset,
13
+ RETRY_PRESETS,
14
+ } from '../errors/index.js';
15
+
16
+ // ─── SoleriError ──────────────────────────────────────────────────────
17
+
18
+ describe('SoleriError', () => {
19
+ it('classifies RATE_LIMIT as retryable', () => {
20
+ const e = new SoleriError('rate limited', SoleriErrorCode.RATE_LIMIT);
21
+ expect(e.code).toBe(SoleriErrorCode.RATE_LIMIT);
22
+ expect(e.classification).toBe('retryable');
23
+ expect(e.retryable).toBe(true);
24
+ expect(e.name).toBe('SoleriError');
25
+ });
26
+
27
+ it('classifies AUTH as permanent', () => {
28
+ const e = new SoleriError('unauthorized', SoleriErrorCode.AUTH);
29
+ expect(e.classification).toBe('permanent');
30
+ expect(e.retryable).toBe(false);
31
+ });
32
+
33
+ it('classifies VALIDATION as fixable', () => {
34
+ const e = new SoleriError('bad input', SoleriErrorCode.VALIDATION);
35
+ expect(e.classification).toBe('fixable');
36
+ expect(e.retryable).toBe(false);
37
+ });
38
+
39
+ it('classifies NETWORK as retryable', () => {
40
+ const e = new SoleriError('connection refused', SoleriErrorCode.NETWORK);
41
+ expect(e.classification).toBe('retryable');
42
+ expect(e.retryable).toBe(true);
43
+ });
44
+
45
+ it('classifies RESOURCE_NOT_FOUND as permanent', () => {
46
+ const e = new SoleriError('not found', SoleriErrorCode.RESOURCE_NOT_FOUND);
47
+ expect(e.classification).toBe('permanent');
48
+ expect(e.retryable).toBe(false);
49
+ });
50
+
51
+ it('classifies CONFIG_ERROR as permanent', () => {
52
+ const e = new SoleriError('missing key', SoleriErrorCode.CONFIG_ERROR);
53
+ expect(e.classification).toBe('permanent');
54
+ expect(e.retryable).toBe(false);
55
+ });
56
+
57
+ it('preserves cause and context', () => {
58
+ const cause = new Error('original');
59
+ const e = new SoleriError('wrapped', SoleriErrorCode.INTERNAL, {
60
+ cause,
61
+ context: { attempt: 3, url: 'http://example.com' },
62
+ });
63
+ expect(e.cause).toBe(cause);
64
+ expect(e.context).toEqual({ attempt: 3, url: 'http://example.com' });
65
+ });
66
+
67
+ it('extends Error', () => {
68
+ const e = new SoleriError('test', SoleriErrorCode.INTERNAL);
69
+ expect(e).toBeInstanceOf(Error);
70
+ expect(e).toBeInstanceOf(SoleriError);
71
+ });
72
+ });
73
+
74
+ // ─── classifyError ────────────────────────────────────────────────────
75
+
76
+ describe('classifyError', () => {
77
+ it('passes through SoleriError unchanged', () => {
78
+ const original = new SoleriError('test', SoleriErrorCode.AUTH);
79
+ expect(classifyError(original)).toBe(original);
80
+ });
81
+
82
+ it('classifies HTTP 429 as RATE_LIMIT', () => {
83
+ const e = classifyError({ status: 429, message: 'Too many requests' });
84
+ expect(e.code).toBe(SoleriErrorCode.RATE_LIMIT);
85
+ expect(e.retryable).toBe(true);
86
+ });
87
+
88
+ it('classifies HTTP 401 as AUTH', () => {
89
+ const e = classifyError({ status: 401, message: 'Unauthorized' });
90
+ expect(e.code).toBe(SoleriErrorCode.AUTH);
91
+ expect(e.retryable).toBe(false);
92
+ });
93
+
94
+ it('classifies HTTP 403 as AUTH', () => {
95
+ const e = classifyError({ status: 403, message: 'Forbidden' });
96
+ expect(e.code).toBe(SoleriErrorCode.AUTH);
97
+ });
98
+
99
+ it('classifies HTTP 404 as RESOURCE_NOT_FOUND', () => {
100
+ const e = classifyError({ status: 404, message: 'Not Found' });
101
+ expect(e.code).toBe(SoleriErrorCode.RESOURCE_NOT_FOUND);
102
+ });
103
+
104
+ it('classifies HTTP 503 as INTERNAL (retryable)', () => {
105
+ const e = classifyError({ status: 503, message: 'Service Unavailable' });
106
+ expect(e.code).toBe(SoleriErrorCode.INTERNAL);
107
+ expect(e.retryable).toBe(true);
108
+ });
109
+
110
+ it('classifies HTTP 408 as TIMEOUT', () => {
111
+ const e = classifyError({ status: 408, message: 'Request Timeout' });
112
+ expect(e.code).toBe(SoleriErrorCode.TIMEOUT);
113
+ });
114
+
115
+ it('classifies HTTP 422 as VALIDATION', () => {
116
+ const e = classifyError({ status: 422, message: 'Unprocessable Entity' });
117
+ expect(e.code).toBe(SoleriErrorCode.VALIDATION);
118
+ });
119
+
120
+ it('classifies ECONNREFUSED as NETWORK', () => {
121
+ const error = new Error('connect ECONNREFUSED');
122
+ (error as unknown as Record<string, string>).code = 'ECONNREFUSED';
123
+ const e = classifyError(error);
124
+ expect(e.code).toBe(SoleriErrorCode.NETWORK);
125
+ });
126
+
127
+ it('classifies ETIMEDOUT as TIMEOUT', () => {
128
+ const error = new Error('connect ETIMEDOUT');
129
+ (error as unknown as Record<string, string>).code = 'ETIMEDOUT';
130
+ const e = classifyError(error);
131
+ expect(e.code).toBe(SoleriErrorCode.TIMEOUT);
132
+ });
133
+
134
+ it('classifies "model overloaded" message as LLM_OVERLOAD', () => {
135
+ const e = classifyError(new Error('model overloaded, please retry'));
136
+ expect(e.code).toBe(SoleriErrorCode.LLM_OVERLOAD);
137
+ expect(e.retryable).toBe(true);
138
+ });
139
+
140
+ it('classifies "capacity" message as LLM_OVERLOAD', () => {
141
+ const e = classifyError(new Error('server at capacity'));
142
+ expect(e.code).toBe(SoleriErrorCode.LLM_OVERLOAD);
143
+ });
144
+
145
+ it('classifies "vault" message as VAULT_UNREACHABLE', () => {
146
+ const e = classifyError(new Error('vault connection lost'));
147
+ expect(e.code).toBe(SoleriErrorCode.VAULT_UNREACHABLE);
148
+ });
149
+
150
+ it('classifies "invalid" message as VALIDATION', () => {
151
+ const e = classifyError(new Error('invalid input format'));
152
+ expect(e.code).toBe(SoleriErrorCode.VALIDATION);
153
+ });
154
+
155
+ it('classifies "configuration" message as CONFIG_ERROR', () => {
156
+ const e = classifyError(new Error('missing configuration'));
157
+ expect(e.code).toBe(SoleriErrorCode.CONFIG_ERROR);
158
+ });
159
+
160
+ it('defaults unknown error to INTERNAL (permanent)', () => {
161
+ const e = classifyError(new Error('something weird happened'));
162
+ expect(e.code).toBe(SoleriErrorCode.INTERNAL);
163
+ });
164
+
165
+ it('handles string input', () => {
166
+ const e = classifyError('plain string error');
167
+ expect(e).toBeInstanceOf(SoleriError);
168
+ expect(e.message).toBe('plain string error');
169
+ });
170
+
171
+ it('handles null input', () => {
172
+ const e = classifyError(null);
173
+ expect(e).toBeInstanceOf(SoleriError);
174
+ expect(e.message).toBe('null');
175
+ });
176
+
177
+ it('handles undefined input', () => {
178
+ const e = classifyError(undefined);
179
+ expect(e).toBeInstanceOf(SoleriError);
180
+ expect(e.message).toBe('undefined');
181
+ });
182
+
183
+ it('prefers HTTP status over message pattern', () => {
184
+ // status=401 should win over "invalid" in message
185
+ const e = classifyError({ status: 401, message: 'invalid credentials' });
186
+ expect(e.code).toBe(SoleriErrorCode.AUTH);
187
+ });
188
+
189
+ it('preserves original error as cause', () => {
190
+ const original = new Error('original');
191
+ const e = classifyError(original);
192
+ expect(e.cause).toBe(original);
193
+ });
194
+
195
+ it('uses statusCode if status is absent', () => {
196
+ const e = classifyError({ statusCode: 429, message: 'rate limited' });
197
+ expect(e.code).toBe(SoleriErrorCode.RATE_LIMIT);
198
+ });
199
+ });
200
+
201
+ // ─── shouldRetry ──────────────────────────────────────────────────────
202
+
203
+ describe('shouldRetry', () => {
204
+ it('returns true for retryable error below max attempts', () => {
205
+ const e = new SoleriError('net', SoleriErrorCode.NETWORK);
206
+ expect(shouldRetry(e, 1, 'fast')).toBe(true); // max=3, attempt=1
207
+ });
208
+
209
+ it('returns false for retryable error at max attempts', () => {
210
+ const e = new SoleriError('net', SoleriErrorCode.NETWORK);
211
+ expect(shouldRetry(e, 3, 'fast')).toBe(false); // max=3, attempt=3
212
+ });
213
+
214
+ it('returns false for permanent error regardless', () => {
215
+ const e = new SoleriError('auth', SoleriErrorCode.AUTH);
216
+ expect(shouldRetry(e, 0, 'patient')).toBe(false);
217
+ });
218
+
219
+ it('returns false for fixable error regardless', () => {
220
+ const e = new SoleriError('val', SoleriErrorCode.VALIDATION);
221
+ expect(shouldRetry(e, 0, 'normal')).toBe(false);
222
+ });
223
+ });
224
+
225
+ // ─── getRetryDelay ────────────────────────────────────────────────────
226
+
227
+ describe('getRetryDelay', () => {
228
+ it('increases with attempt number', () => {
229
+ const delays = Array.from({ length: 5 }, (_, i) => getRetryDelay(i, 'normal'));
230
+ // Check trend is generally increasing (jitter may cause minor inversions)
231
+ const avg0 = getAvgDelay(0, 'normal');
232
+ const avg2 = getAvgDelay(2, 'normal');
233
+ expect(avg2).toBeGreaterThan(avg0);
234
+ });
235
+
236
+ it('caps at maxInterval', () => {
237
+ const delay = getRetryDelay(100, 'fast');
238
+ // maxInterval for fast is 10_000, with 25% jitter max = 12_500
239
+ expect(delay).toBeLessThanOrEqual(12_500);
240
+ });
241
+
242
+ it('returns non-negative values', () => {
243
+ for (let i = 0; i < 50; i++) {
244
+ expect(getRetryDelay(i, 'patient')).toBeGreaterThanOrEqual(0);
245
+ }
246
+ });
247
+ });
248
+
249
+ // Average over multiple calls to smooth jitter
250
+ function getAvgDelay(attempt: number, preset: 'fast' | 'normal' | 'patient'): number {
251
+ let sum = 0;
252
+ const runs = 100;
253
+ for (let i = 0; i < runs; i++) sum += getRetryDelay(attempt, preset);
254
+ return sum / runs;
255
+ }
256
+
257
+ // ─── RETRY_PRESETS ────────────────────────────────────────────────────
258
+
259
+ describe('RETRY_PRESETS', () => {
260
+ it('has fast preset (1s/10s/3)', () => {
261
+ expect(RETRY_PRESETS.fast).toEqual({
262
+ initialIntervalMs: 1_000,
263
+ maxIntervalMs: 10_000,
264
+ maxAttempts: 3,
265
+ backoffMultiplier: 2,
266
+ });
267
+ });
268
+
269
+ it('has normal preset (10s/2min/10)', () => {
270
+ expect(RETRY_PRESETS.normal).toEqual({
271
+ initialIntervalMs: 10_000,
272
+ maxIntervalMs: 120_000,
273
+ maxAttempts: 10,
274
+ backoffMultiplier: 2,
275
+ });
276
+ });
277
+
278
+ it('has patient preset (1min/15min/25)', () => {
279
+ expect(RETRY_PRESETS.patient).toEqual({
280
+ initialIntervalMs: 60_000,
281
+ maxIntervalMs: 900_000,
282
+ maxAttempts: 25,
283
+ backoffMultiplier: 1.5,
284
+ });
285
+ });
286
+ });
287
+
288
+ // ─── retryWithPreset ──────────────────────────────────────────────────
289
+
290
+ describe('retryWithPreset', () => {
291
+ it('succeeds on first try', async () => {
292
+ const result = await retryWithPreset(() => Promise.resolve(42), 'fast');
293
+ expect(result).toEqual({ ok: true, value: 42 });
294
+ });
295
+
296
+ it('retries and eventually succeeds', async () => {
297
+ let calls = 0;
298
+ const fn = async () => {
299
+ calls++;
300
+ if (calls < 3) throw Object.assign(new Error('timeout'), { code: 'ETIMEDOUT' });
301
+ return 'done';
302
+ };
303
+ const result = await retryWithPreset(fn, 'fast', {
304
+ onRetry: vi.fn(),
305
+ });
306
+ expect(result).toEqual({ ok: true, value: 'done' });
307
+ expect(calls).toBe(3);
308
+ }, 30_000);
309
+
310
+ it('returns err immediately for permanent error', async () => {
311
+ const fn = async () => {
312
+ throw Object.assign(new Error('forbidden'), { status: 403 });
313
+ };
314
+ const result = await retryWithPreset(fn, 'fast');
315
+ expect(result.ok).toBe(false);
316
+ if (!result.ok) {
317
+ expect(result.error.code).toBe(SoleriErrorCode.AUTH);
318
+ }
319
+ });
320
+
321
+ it('returns err after exhausting max attempts', async () => {
322
+ let calls = 0;
323
+ const fn = async () => {
324
+ calls++;
325
+ throw Object.assign(new Error('timeout'), { code: 'ETIMEDOUT' });
326
+ };
327
+ const result = await retryWithPreset(fn, 'fast');
328
+ expect(result.ok).toBe(false);
329
+ expect(calls).toBe(3); // fast preset has maxAttempts=3
330
+ }, 30_000);
331
+
332
+ it('calls onRetry callback', async () => {
333
+ const onRetry = vi.fn();
334
+ let calls = 0;
335
+ const fn = async () => {
336
+ calls++;
337
+ if (calls < 2) throw Object.assign(new Error('net'), { code: 'ECONNRESET' });
338
+ return 'ok';
339
+ };
340
+ await retryWithPreset(fn, 'fast', { onRetry });
341
+ expect(onRetry).toHaveBeenCalledTimes(1);
342
+ expect(onRetry).toHaveBeenCalledWith(expect.any(SoleriError), 1, expect.any(Number));
343
+ }, 30_000);
344
+
345
+ it('respects abort signal', async () => {
346
+ const controller = new AbortController();
347
+ let calls = 0;
348
+ const fn = async () => {
349
+ calls++;
350
+ if (calls === 1) {
351
+ controller.abort();
352
+ throw Object.assign(new Error('net'), { code: 'ECONNREFUSED' });
353
+ }
354
+ return 'ok';
355
+ };
356
+ const result = await retryWithPreset(fn, 'fast', { signal: controller.signal });
357
+ expect(result.ok).toBe(false);
358
+ });
359
+ });
360
+
361
+ // ─── Result helpers ───────────────────────────────────────────────────
362
+
363
+ describe('Result helpers', () => {
364
+ it('ok() creates success result', () => {
365
+ const r = ok(42);
366
+ expect(r.ok).toBe(true);
367
+ if (r.ok) expect(r.value).toBe(42);
368
+ });
369
+
370
+ it('err() creates failure result', () => {
371
+ const error = new SoleriError('fail', SoleriErrorCode.INTERNAL);
372
+ const r = err(error);
373
+ expect(r.ok).toBe(false);
374
+ if (!r.ok) expect(r.error).toBe(error);
375
+ });
376
+
377
+ it('isOk() type guard works', () => {
378
+ const r = ok('hello');
379
+ expect(isOk(r)).toBe(true);
380
+ expect(isErr(r)).toBe(false);
381
+ });
382
+
383
+ it('isErr() type guard works', () => {
384
+ const r = err(new SoleriError('x', SoleriErrorCode.AUTH));
385
+ expect(isErr(r)).toBe(true);
386
+ expect(isOk(r)).toBe(false);
387
+ });
388
+ });
@@ -56,7 +56,12 @@ describe('Grading Ops', () => {
56
56
  const check = (await findOp('plan_grade').handler({ planId })) as {
57
57
  score: number;
58
58
  grade: string;
59
- gaps: Array<{ severity: string; category: string; description: string; recommendation: string }>;
59
+ gaps: Array<{
60
+ severity: string;
61
+ category: string;
62
+ description: string;
63
+ recommendation: string;
64
+ }>;
60
65
  iteration: number;
61
66
  };
62
67
  // 3 critical gaps: no objective, no scope, no tasks = -90
@@ -77,9 +82,15 @@ describe('Grading Ops', () => {
77
82
  'Set TTL to 5 minutes since average data freshness requirement is 10 minutes',
78
83
  ],
79
84
  tasks: [
80
- { title: 'Setup Redis client', description: 'Install and configure Redis connection pool' },
85
+ {
86
+ title: 'Setup Redis client',
87
+ description: 'Install and configure Redis connection pool',
88
+ },
81
89
  { title: 'Add middleware', description: 'Express transparent caching middleware layer' },
82
- { title: 'Add invalidation', description: 'Cache invalidation on writes for consistency' },
90
+ {
91
+ title: 'Add invalidation',
92
+ description: 'Cache invalidation on writes for consistency',
93
+ },
83
94
  { title: 'Add tests', description: 'Integration tests for cache hit/miss scenarios' },
84
95
  { title: 'Add metrics', description: 'Track and verify cache hit rate monitoring' },
85
96
  ],
@@ -97,7 +108,9 @@ describe('Grading Ops', () => {
97
108
  const planId = await createPlan({
98
109
  objective: 'Test duplicate title detection in the grading engine',
99
110
  scope: 'Testing only, does not include production changes',
100
- decisions: ['Use assertions because they provide clear error messages due to descriptive output'],
111
+ decisions: [
112
+ 'Use assertions because they provide clear error messages due to descriptive output',
113
+ ],
101
114
  tasks: [
102
115
  { title: 'Same name', description: 'First task implementation' },
103
116
  { title: 'Same name', description: 'Second task implementation' },
@@ -228,7 +241,9 @@ describe('Grading Ops', () => {
228
241
  const planId = await createPlan({
229
242
  objective: 'Build a comprehensive feature for the testing module',
230
243
  scope: 'Testing module only, does not include deployment or infrastructure',
231
- decisions: ['Use vitest because it integrates natively with TypeScript due to built-in support'],
244
+ decisions: [
245
+ 'Use vitest because it integrates natively with TypeScript due to built-in support',
246
+ ],
232
247
  tasks: [
233
248
  { title: 'Write unit tests', description: 'Cover all edge cases in the module' },
234
249
  { title: 'Write integration tests', description: 'End-to-end API tests for the flow' },
@@ -262,7 +277,10 @@ describe('Grading Ops', () => {
262
277
  score: number;
263
278
  iteration: number;
264
279
  totalGaps: number;
265
- gapsBySeverity: Record<string, Array<{ category: string; description: string; recommendation: string }>>;
280
+ gapsBySeverity: Record<
281
+ string,
282
+ Array<{ category: string; description: string; recommendation: string }>
283
+ >;
266
284
  nextAction: string;
267
285
  };
268
286
  expect(result.score).toBeLessThan(100);
@@ -285,7 +303,10 @@ describe('Grading Ops', () => {
285
303
  tasks: [
286
304
  { title: 'Setup Redis', description: 'Install and configure Redis connection pool' },
287
305
  { title: 'Add middleware', description: 'Express transparent caching middleware layer' },
288
- { title: 'Add invalidation', description: 'Cache invalidation on writes for consistency' },
306
+ {
307
+ title: 'Add invalidation',
308
+ description: 'Cache invalidation on writes for consistency',
309
+ },
289
310
  { title: 'Add tests', description: 'Integration tests for cache hit/miss scenarios' },
290
311
  { title: 'Add metrics', description: 'Track and verify cache hit rate monitoring' },
291
312
  ],
@@ -0,0 +1,162 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { dedupItems, DEDUP_THRESHOLD } from '../intake/dedup-gate.js';
3
+ import { Vault } from '../vault/vault.js';
4
+ import type { ClassifiedItem } from '../intake/types.js';
5
+
6
+ function makeItem(overrides: Partial<ClassifiedItem> = {}): ClassifiedItem {
7
+ return {
8
+ type: overrides.type ?? 'pattern',
9
+ title: overrides.title ?? 'Test Pattern',
10
+ description: overrides.description ?? 'A generic test description.',
11
+ tags: overrides.tags ?? ['test'],
12
+ severity: overrides.severity ?? 'suggestion',
13
+ citation: overrides.citation ?? 'p.1',
14
+ };
15
+ }
16
+
17
+ describe('Dedup Gate', () => {
18
+ let vault: Vault;
19
+
20
+ beforeEach(() => {
21
+ vault = new Vault(':memory:');
22
+ });
23
+
24
+ afterEach(() => {
25
+ vault.close();
26
+ });
27
+
28
+ it('should mark items as non-duplicate when vault is empty', () => {
29
+ const items: ClassifiedItem[] = [
30
+ makeItem({ title: 'Brand New Pattern', description: 'Something entirely new.' }),
31
+ ];
32
+
33
+ const results = dedupItems(items, vault);
34
+
35
+ expect(results).toHaveLength(1);
36
+ expect(results[0].isDuplicate).toBe(false);
37
+ expect(results[0].similarity).toBe(0);
38
+ expect(results[0].bestMatchId).toBeUndefined();
39
+ expect(results[0].item).toBe(items[0]);
40
+ });
41
+
42
+ it('should detect near-duplicates with high similarity', () => {
43
+ vault.seed([
44
+ {
45
+ id: 'existing-1',
46
+ type: 'pattern',
47
+ domain: 'design-patterns',
48
+ title: 'Singleton Pattern Implementation',
49
+ severity: 'suggestion',
50
+ description:
51
+ 'The singleton pattern ensures a class has only one instance and provides a global point of access to it.',
52
+ tags: ['singleton', 'design-pattern', 'creational'],
53
+ },
54
+ ]);
55
+
56
+ const items: ClassifiedItem[] = [
57
+ makeItem({
58
+ title: 'Singleton Pattern Implementation',
59
+ description:
60
+ 'The singleton pattern ensures a class has only one instance and provides a global point of access to it.',
61
+ tags: ['singleton', 'design-pattern', 'creational'],
62
+ }),
63
+ ];
64
+
65
+ const results = dedupItems(items, vault);
66
+
67
+ expect(results).toHaveLength(1);
68
+ expect(results[0].similarity).toBeGreaterThan(0.5);
69
+ expect(results[0].bestMatchId).toBe('existing-1');
70
+ });
71
+
72
+ it('should not flag dissimilar entries as duplicates', () => {
73
+ vault.seed([
74
+ {
75
+ id: 'existing-2',
76
+ type: 'pattern',
77
+ domain: 'design-patterns',
78
+ title: 'Observer Pattern',
79
+ severity: 'suggestion',
80
+ description:
81
+ 'The observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified.',
82
+ tags: ['observer', 'design-pattern', 'behavioral'],
83
+ },
84
+ ]);
85
+
86
+ const items: ClassifiedItem[] = [
87
+ makeItem({
88
+ title: 'God Object Anti-Pattern',
89
+ description:
90
+ 'A god object is an anti-pattern where a single class knows too much or does too much, violating the single responsibility principle.',
91
+ tags: ['anti-pattern', 'code-smell'],
92
+ }),
93
+ ];
94
+
95
+ const results = dedupItems(items, vault);
96
+
97
+ expect(results).toHaveLength(1);
98
+ expect(results[0].isDuplicate).toBe(false);
99
+ });
100
+
101
+ it('should handle multiple items at once', () => {
102
+ vault.seed([
103
+ {
104
+ id: 'existing-3',
105
+ type: 'pattern',
106
+ domain: 'design-patterns',
107
+ title: 'Factory Method Pattern',
108
+ severity: 'suggestion',
109
+ description:
110
+ 'The factory method pattern defines an interface for creating an object but lets subclasses decide which class to instantiate.',
111
+ tags: ['factory', 'design-pattern', 'creational'],
112
+ },
113
+ ]);
114
+
115
+ const items: ClassifiedItem[] = [
116
+ // Near-duplicate of vault entry
117
+ makeItem({
118
+ title: 'Factory Method Pattern',
119
+ description:
120
+ 'The factory method pattern defines an interface for creating an object but lets subclasses decide which class to instantiate.',
121
+ tags: ['factory', 'design-pattern', 'creational'],
122
+ }),
123
+ // Completely different
124
+ makeItem({
125
+ title: 'Dependency Injection',
126
+ description:
127
+ 'Dependency injection is a technique where an object receives other objects that it depends on, called dependencies, rather than creating them internally.',
128
+ tags: ['dependency-injection', 'inversion-of-control'],
129
+ }),
130
+ // Also different
131
+ makeItem({
132
+ title: 'Circuit Breaker Pattern',
133
+ description:
134
+ 'The circuit breaker pattern prevents an application from repeatedly trying to execute an operation that is likely to fail, allowing it to recover gracefully.',
135
+ tags: ['resilience', 'distributed-systems'],
136
+ }),
137
+ ];
138
+
139
+ const results = dedupItems(items, vault);
140
+
141
+ expect(results).toHaveLength(3);
142
+
143
+ // First item is a near-duplicate — should have high similarity
144
+ expect(results[0].similarity).toBeGreaterThan(0.5);
145
+ expect(results[0].bestMatchId).toBe('existing-3');
146
+
147
+ // Second and third items are different — should not be duplicates
148
+ expect(results[1].isDuplicate).toBe(false);
149
+ expect(results[2].isDuplicate).toBe(false);
150
+ });
151
+
152
+ it('DEDUP_THRESHOLD should be 0.85', () => {
153
+ expect(DEDUP_THRESHOLD).toBe(0.85);
154
+ });
155
+ });
156
+
157
+ describe('Intake Types', () => {
158
+ it('should import all type definitions', async () => {
159
+ const types = await import('../intake/types.js');
160
+ expect(types).toBeDefined();
161
+ });
162
+ });