@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,222 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { parseVariables, resolveIncludes } from '../prompts/parser.js';
6
+ import { TemplateManager } from '../prompts/template-manager.js';
7
+
8
+ // ─── parseVariables ───────────────────────────────────────────────────
9
+
10
+ describe('parseVariables', () => {
11
+ it('extracts required variable', () => {
12
+ const vars = parseVariables('Hello {{name}}');
13
+ expect(vars).toHaveLength(1);
14
+ expect(vars[0]).toEqual({ name: 'name', required: true, defaultValue: undefined });
15
+ });
16
+
17
+ it('extracts variable with default', () => {
18
+ const vars = parseVariables('Hello {{name:World}}');
19
+ expect(vars).toHaveLength(1);
20
+ expect(vars[0]).toEqual({ name: 'name', required: false, defaultValue: 'World' });
21
+ });
22
+
23
+ it('extracts multiple unique variables', () => {
24
+ const vars = parseVariables('{{greeting}} {{name}}, welcome to {{place}}');
25
+ expect(vars).toHaveLength(3);
26
+ expect(vars.map((v) => v.name)).toEqual(['greeting', 'name', 'place']);
27
+ });
28
+
29
+ it('deduplicates same variable', () => {
30
+ const vars = parseVariables('{{name}} and {{name}} again');
31
+ expect(vars).toHaveLength(1);
32
+ });
33
+
34
+ it('handles empty default', () => {
35
+ const vars = parseVariables('{{opt:}}');
36
+ expect(vars[0]).toEqual({ name: 'opt', required: false, defaultValue: '' });
37
+ });
38
+
39
+ it('returns empty for no variables', () => {
40
+ expect(parseVariables('plain text')).toEqual([]);
41
+ });
42
+
43
+ it('handles mixed required and optional', () => {
44
+ const vars = parseVariables('{{required}} and {{optional:fallback}}');
45
+ expect(vars).toHaveLength(2);
46
+ expect(vars[0].required).toBe(true);
47
+ expect(vars[1].required).toBe(false);
48
+ expect(vars[1].defaultValue).toBe('fallback');
49
+ });
50
+ });
51
+
52
+ // ─── resolveIncludes ──────────────────────────────────────────────────
53
+
54
+ describe('resolveIncludes', () => {
55
+ it('resolves single include', () => {
56
+ const result = resolveIncludes('before @include(header) after', (name) => {
57
+ if (name === 'header') return 'HEADER';
58
+ return '';
59
+ });
60
+ expect(result).toBe('before HEADER after');
61
+ });
62
+
63
+ it('resolves nested includes', () => {
64
+ const result = resolveIncludes('start @include(a)', (name) => {
65
+ if (name === 'a') return 'A @include(b)';
66
+ if (name === 'b') return 'B';
67
+ return '';
68
+ });
69
+ expect(result).toBe('start A B');
70
+ });
71
+
72
+ it('detects circular includes', () => {
73
+ expect(() =>
74
+ resolveIncludes('@include(a)', (name) => {
75
+ if (name === 'a') return '@include(b)';
76
+ if (name === 'b') return '@include(a)';
77
+ return '';
78
+ }),
79
+ ).toThrow(/Circular include detected/);
80
+ });
81
+
82
+ it('throws on depth exceeding 10', () => {
83
+ // Each level includes a unique name so cycle detection doesn't fire
84
+ let counter = 0;
85
+ expect(() =>
86
+ resolveIncludes('@include(level0)', (name) => {
87
+ counter++;
88
+ return `@include(level${counter})`;
89
+ }),
90
+ ).toThrow(/Include depth exceeded/);
91
+ });
92
+
93
+ it('handles no includes', () => {
94
+ const result = resolveIncludes('no includes here', () => '');
95
+ expect(result).toBe('no includes here');
96
+ });
97
+
98
+ it('handles multiple includes', () => {
99
+ const result = resolveIncludes('@include(a) + @include(b)', (name) => name.toUpperCase());
100
+ expect(result).toBe('A + B');
101
+ });
102
+ });
103
+
104
+ // ─── TemplateManager ──────────────────────────────────────────────────
105
+
106
+ describe('TemplateManager', () => {
107
+ let tempDir: string;
108
+
109
+ beforeEach(() => {
110
+ tempDir = join(tmpdir(), `templates-test-${Date.now()}`);
111
+ mkdirSync(tempDir, { recursive: true });
112
+ });
113
+
114
+ afterEach(() => {
115
+ rmSync(tempDir, { recursive: true, force: true });
116
+ });
117
+
118
+ it('loads .prompt files from directory', () => {
119
+ writeFileSync(join(tempDir, 'greeting.prompt'), 'Hello {{name}}!');
120
+ writeFileSync(join(tempDir, 'farewell.prompt'), 'Goodbye {{name}}.');
121
+ writeFileSync(join(tempDir, 'not-a-template.txt'), 'ignored');
122
+
123
+ const mgr = new TemplateManager(tempDir);
124
+ mgr.load();
125
+
126
+ expect(mgr.listTemplates().sort()).toEqual(['farewell', 'greeting']);
127
+ });
128
+
129
+ it('handles nonexistent directory gracefully', () => {
130
+ const mgr = new TemplateManager('/nonexistent/path');
131
+ mgr.load(); // should not throw
132
+ expect(mgr.listTemplates()).toEqual([]);
133
+ });
134
+
135
+ it('renders template with variables', () => {
136
+ writeFileSync(join(tempDir, 'hello.prompt'), 'Hello {{name}}, you are {{role}}!');
137
+ const mgr = new TemplateManager(tempDir);
138
+ mgr.load();
139
+
140
+ const result = mgr.render('hello', { name: 'Atlas', role: 'advisor' });
141
+ expect(result).toBe('Hello Atlas, you are advisor!');
142
+ });
143
+
144
+ it('uses default values', () => {
145
+ writeFileSync(join(tempDir, 'hello.prompt'), 'Hello {{name:World}}!');
146
+ const mgr = new TemplateManager(tempDir);
147
+ mgr.load();
148
+
149
+ expect(mgr.render('hello', {})).toBe('Hello World!');
150
+ expect(mgr.render('hello', { name: 'Custom' })).toBe('Hello Custom!');
151
+ });
152
+
153
+ it('throws on missing required variable in strict mode', () => {
154
+ writeFileSync(join(tempDir, 'strict.prompt'), '{{required}}');
155
+ const mgr = new TemplateManager(tempDir);
156
+ mgr.load();
157
+
158
+ expect(() => mgr.render('strict', {})).toThrow(/Missing required variable: required/);
159
+ });
160
+
161
+ it('leaves placeholder in non-strict mode', () => {
162
+ writeFileSync(join(tempDir, 'lax.prompt'), 'Hello {{name}}!');
163
+ const mgr = new TemplateManager(tempDir);
164
+ mgr.load();
165
+
166
+ const result = mgr.render('lax', {}, { strict: false });
167
+ expect(result).toBe('Hello {{name}}!');
168
+ });
169
+
170
+ it('resolves @include directives', () => {
171
+ writeFileSync(join(tempDir, 'header.prompt'), '--- HEADER ---');
172
+ writeFileSync(join(tempDir, 'page.prompt'), '@include(header)\nContent here.');
173
+ const mgr = new TemplateManager(tempDir);
174
+ mgr.load();
175
+
176
+ const result = mgr.render('page', {});
177
+ expect(result).toBe('--- HEADER ---\nContent here.');
178
+ });
179
+
180
+ it('throws on missing include', () => {
181
+ writeFileSync(join(tempDir, 'broken.prompt'), '@include(nonexistent)');
182
+ const mgr = new TemplateManager(tempDir);
183
+ mgr.load();
184
+
185
+ expect(() => mgr.render('broken', {})).toThrow(/Include not found: nonexistent/);
186
+ });
187
+
188
+ it('throws on nonexistent template name', () => {
189
+ const mgr = new TemplateManager(tempDir);
190
+ mgr.load();
191
+
192
+ expect(() => mgr.render('missing', {})).toThrow(/Template not found: missing/);
193
+ });
194
+
195
+ it('getTemplate returns raw template', () => {
196
+ writeFileSync(join(tempDir, 'raw.prompt'), 'Raw {{content}}');
197
+ const mgr = new TemplateManager(tempDir);
198
+ mgr.load();
199
+
200
+ const tmpl = mgr.getTemplate('raw');
201
+ expect(tmpl).not.toBeNull();
202
+ expect(tmpl!.name).toBe('raw');
203
+ expect(tmpl!.content).toBe('Raw {{content}}');
204
+ expect(tmpl!.variables).toHaveLength(1);
205
+ });
206
+
207
+ it('getTemplate returns null for nonexistent', () => {
208
+ const mgr = new TemplateManager(tempDir);
209
+ mgr.load();
210
+ expect(mgr.getTemplate('nope')).toBeNull();
211
+ });
212
+
213
+ it('variables in included templates get substituted', () => {
214
+ writeFileSync(join(tempDir, 'partial.prompt'), 'I am {{name}}');
215
+ writeFileSync(join(tempDir, 'main.prompt'), 'Hello: @include(partial)!');
216
+ const mgr = new TemplateManager(tempDir);
217
+ mgr.load();
218
+
219
+ const result = mgr.render('main', { name: 'Atlas' });
220
+ expect(result).toBe('Hello: I am Atlas!');
221
+ });
222
+ });
@@ -47,8 +47,8 @@ describe('createVaultExtraOps', () => {
47
47
  return op;
48
48
  }
49
49
 
50
- it('should return 12 ops', () => {
51
- expect(ops.length).toBe(12);
50
+ it('should return 23 ops', () => {
51
+ expect(ops.length).toBe(23);
52
52
  });
53
53
 
54
54
  it('should have all expected op names', () => {
@@ -65,6 +65,17 @@ describe('createVaultExtraOps', () => {
65
65
  expect(names).toContain('vault_seed');
66
66
  expect(names).toContain('vault_backup');
67
67
  expect(names).toContain('vault_age_report');
68
+ // #153: Seed canonical
69
+ expect(names).toContain('vault_seed_canonical');
70
+ // #155: Knowledge lifecycle
71
+ expect(names).toContain('knowledge_audit');
72
+ expect(names).toContain('knowledge_health');
73
+ expect(names).toContain('knowledge_merge');
74
+ expect(names).toContain('knowledge_reorganize');
75
+ // #89: Bi-temporal
76
+ expect(names).toContain('vault_set_temporal');
77
+ expect(names).toContain('vault_find_expiring');
78
+ expect(names).toContain('vault_find_expired');
68
79
  });
69
80
 
70
81
  // ─── vault_get ────────────────────────────────────────────────────
@@ -357,10 +368,7 @@ describe('createVaultExtraOps', () => {
357
368
  // ─── vault_age_report ─────────────────────────────────────────────
358
369
 
359
370
  it('vault_age_report should return age distribution', async () => {
360
- runtime.vault.seed([
361
- makeEntry({ id: 'va-1' }),
362
- makeEntry({ id: 'va-2' }),
363
- ]);
371
+ runtime.vault.seed([makeEntry({ id: 'va-1' }), makeEntry({ id: 'va-2' })]);
364
372
  const result = (await findOp('vault_age_report').handler({})) as {
365
373
  total: number;
366
374
  buckets: Array<{ label: string; count: number; minDays: number; maxDays: number }>;
@@ -390,7 +398,14 @@ describe('createVaultExtraOps', () => {
390
398
  // ─── Auth levels ──────────────────────────────────────────────────
391
399
 
392
400
  it('should assign correct auth levels', () => {
393
- const readOps = ['vault_get', 'vault_tags', 'vault_domains', 'vault_recent', 'vault_backup', 'vault_age_report'];
401
+ const readOps = [
402
+ 'vault_get',
403
+ 'vault_tags',
404
+ 'vault_domains',
405
+ 'vault_recent',
406
+ 'vault_backup',
407
+ 'vault_age_report',
408
+ ];
394
409
  const writeOps = ['vault_update', 'vault_bulk_add', 'vault_import', 'vault_seed'];
395
410
  const adminOps = ['vault_remove', 'vault_bulk_remove'];
396
411
 
@@ -404,4 +419,64 @@ describe('createVaultExtraOps', () => {
404
419
  expect(findOp(name).auth).toBe('admin');
405
420
  }
406
421
  });
422
+
423
+ // ─── vault_set_temporal ─────────────────────────────────────────
424
+
425
+ describe('vault_set_temporal', () => {
426
+ it('should set validUntil on an entry', async () => {
427
+ runtime.vault.seed([makeEntry({ id: 'temporal-1' })]);
428
+ const result = (await findOp('vault_set_temporal').handler({
429
+ id: 'temporal-1',
430
+ validUntil: Math.floor(Date.now() / 1000) + 86400 * 30,
431
+ })) as { updated: boolean; id: string; validUntil: number | null };
432
+ expect(result.updated).toBe(true);
433
+ expect(result.id).toBe('temporal-1');
434
+ expect(result.validUntil).toBeGreaterThan(0);
435
+ });
436
+
437
+ it('should return error for missing entry', async () => {
438
+ const result = (await findOp('vault_set_temporal').handler({
439
+ id: 'nonexistent',
440
+ validUntil: 1000,
441
+ })) as { error: string };
442
+ expect(result.error).toBeDefined();
443
+ });
444
+ });
445
+
446
+ // ─── vault_find_expiring ────────────────────────────────────────
447
+
448
+ describe('vault_find_expiring', () => {
449
+ it('should find entries expiring within N days', async () => {
450
+ const now = Math.floor(Date.now() / 1000);
451
+ runtime.vault.seed([
452
+ makeEntry({ id: 'exp-1' }),
453
+ makeEntry({ id: 'exp-2' }),
454
+ makeEntry({ id: 'exp-3' }),
455
+ ]);
456
+ runtime.vault.setTemporal('exp-1', undefined, now + 86400 * 5);
457
+ runtime.vault.setTemporal('exp-2', undefined, now + 86400 * 60);
458
+
459
+ const result = (await findOp('vault_find_expiring').handler({
460
+ withinDays: 10,
461
+ })) as { entries: unknown[]; count: number };
462
+ expect(result.count).toBe(1);
463
+ });
464
+ });
465
+
466
+ // ─── vault_find_expired ─────────────────────────────────────────
467
+
468
+ describe('vault_find_expired', () => {
469
+ it('should find expired entries', async () => {
470
+ const now = Math.floor(Date.now() / 1000);
471
+ runtime.vault.seed([makeEntry({ id: 'past-1' }), makeEntry({ id: 'past-2' })]);
472
+ runtime.vault.setTemporal('past-1', undefined, now - 86400);
473
+ runtime.vault.setTemporal('past-2', undefined, now + 86400);
474
+
475
+ const result = (await findOp('vault_find_expired').handler({})) as {
476
+ entries: unknown[];
477
+ count: number;
478
+ };
479
+ expect(result.count).toBe(1);
480
+ });
481
+ });
407
482
  });
@@ -491,4 +491,188 @@ describe('Vault', () => {
491
491
  expect(stats.byProject).toEqual({ '/a': 2, '/b': 1 });
492
492
  });
493
493
  });
494
+
495
+ describe('Vault archival and optimization', () => {
496
+ it('archive moves old entries', () => {
497
+ const v = new Vault(':memory:');
498
+ v.seed([
499
+ {
500
+ id: 'old-1',
501
+ type: 'pattern',
502
+ domain: 'test',
503
+ title: 'Old Pattern',
504
+ severity: 'suggestion',
505
+ description: 'Old entry',
506
+ tags: ['test'],
507
+ },
508
+ ]);
509
+ // Manually set the updated_at to 200 days ago
510
+ v.getProvider().run('UPDATE entries SET updated_at = ? WHERE id = ?', [
511
+ Math.floor(Date.now() / 1000) - 200 * 86400,
512
+ 'old-1',
513
+ ]);
514
+ v.seed([
515
+ {
516
+ id: 'new-1',
517
+ type: 'pattern',
518
+ domain: 'test',
519
+ title: 'New Pattern',
520
+ severity: 'suggestion',
521
+ description: 'New entry',
522
+ tags: ['test'],
523
+ },
524
+ ]);
525
+
526
+ const result = v.archive({ olderThanDays: 90 });
527
+ expect(result.archived).toBe(1);
528
+ expect(v.get('old-1')).toBeNull();
529
+ expect(v.get('new-1')).not.toBeNull();
530
+ v.close();
531
+ });
532
+
533
+ it('archive respects olderThanDays', () => {
534
+ const v = new Vault(':memory:');
535
+ v.seed([
536
+ {
537
+ id: 'e1',
538
+ type: 'pattern',
539
+ domain: 'test',
540
+ title: 'Entry',
541
+ severity: 'suggestion',
542
+ description: 'Not old enough',
543
+ tags: ['test'],
544
+ },
545
+ ]);
546
+ // Entry is fresh (just created), archive with 1 day threshold
547
+ const result = v.archive({ olderThanDays: 1 });
548
+ expect(result.archived).toBe(0);
549
+ expect(v.get('e1')).not.toBeNull();
550
+ v.close();
551
+ });
552
+
553
+ it('archive returns 0 when no candidates', () => {
554
+ const v = new Vault(':memory:');
555
+ const result = v.archive({ olderThanDays: 30 });
556
+ expect(result.archived).toBe(0);
557
+ v.close();
558
+ });
559
+
560
+ it('archived entries excluded from search', () => {
561
+ const v = new Vault(':memory:');
562
+ v.seed([
563
+ {
564
+ id: 'search-1',
565
+ type: 'pattern',
566
+ domain: 'test',
567
+ title: 'Searchable Pattern',
568
+ severity: 'suggestion',
569
+ description: 'Should be archived',
570
+ tags: ['search'],
571
+ },
572
+ ]);
573
+ v.getProvider().run('UPDATE entries SET updated_at = ? WHERE id = ?', [
574
+ Math.floor(Date.now() / 1000) - 200 * 86400,
575
+ 'search-1',
576
+ ]);
577
+
578
+ // Before archive: should appear in search
579
+ const before = v.search('searchable');
580
+ expect(before.length).toBeGreaterThan(0);
581
+
582
+ v.archive({ olderThanDays: 90 });
583
+
584
+ // After archive: should not appear
585
+ const after = v.search('searchable');
586
+ expect(after.length).toBe(0);
587
+ v.close();
588
+ });
589
+
590
+ it('restore brings entry back', () => {
591
+ const v = new Vault(':memory:');
592
+ v.seed([
593
+ {
594
+ id: 'restore-1',
595
+ type: 'pattern',
596
+ domain: 'test',
597
+ title: 'Restore Me',
598
+ severity: 'suggestion',
599
+ description: 'Will be restored',
600
+ tags: ['test'],
601
+ },
602
+ ]);
603
+ v.getProvider().run('UPDATE entries SET updated_at = ? WHERE id = ?', [
604
+ Math.floor(Date.now() / 1000) - 200 * 86400,
605
+ 'restore-1',
606
+ ]);
607
+ v.archive({ olderThanDays: 90 });
608
+ expect(v.get('restore-1')).toBeNull();
609
+
610
+ const restored = v.restore('restore-1');
611
+ expect(restored).toBe(true);
612
+ expect(v.get('restore-1')).not.toBeNull();
613
+ expect(v.get('restore-1')!.title).toBe('Restore Me');
614
+ v.close();
615
+ });
616
+
617
+ it('restore returns false for missing id', () => {
618
+ const v = new Vault(':memory:');
619
+ const result = v.restore('nonexistent');
620
+ expect(result).toBe(false);
621
+ v.close();
622
+ });
623
+
624
+ it('restored entry appears in search', () => {
625
+ const v = new Vault(':memory:');
626
+ v.seed([
627
+ {
628
+ id: 'search-restore',
629
+ type: 'pattern',
630
+ domain: 'test',
631
+ title: 'Unique Findable Pattern',
632
+ severity: 'suggestion',
633
+ description: 'Will be found after restore',
634
+ tags: ['test'],
635
+ },
636
+ ]);
637
+ v.getProvider().run('UPDATE entries SET updated_at = ? WHERE id = ?', [
638
+ Math.floor(Date.now() / 1000) - 200 * 86400,
639
+ 'search-restore',
640
+ ]);
641
+ v.archive({ olderThanDays: 90 });
642
+ v.restore('search-restore');
643
+
644
+ const results = v.search('unique findable');
645
+ expect(results.length).toBeGreaterThan(0);
646
+ v.close();
647
+ });
648
+
649
+ it('optimize runs without error', () => {
650
+ const v = new Vault(':memory:');
651
+ v.seed([
652
+ {
653
+ id: 'opt-1',
654
+ type: 'pattern',
655
+ domain: 'test',
656
+ title: 'Optimize Test',
657
+ severity: 'suggestion',
658
+ description: 'For optimization',
659
+ tags: ['test'],
660
+ },
661
+ ]);
662
+
663
+ expect(() => v.optimize()).not.toThrow();
664
+ v.close();
665
+ });
666
+
667
+ it('optimize returns status', () => {
668
+ const v = new Vault(':memory:');
669
+ const status = v.optimize();
670
+ expect(status).toHaveProperty('vacuumed');
671
+ expect(status).toHaveProperty('analyzed');
672
+ expect(status).toHaveProperty('ftsRebuilt');
673
+ // SQLite backend should analyze and rebuild FTS
674
+ expect(status.analyzed).toBe(true);
675
+ v.close();
676
+ });
677
+ });
494
678
  });
@@ -47,7 +47,7 @@ const DEFAULT_WEIGHTS: ScoringWeights = {
47
47
  semantic: 0.4,
48
48
  vector: 0.0,
49
49
  severity: 0.15,
50
- recency: 0.15,
50
+ temporalDecay: 0.15,
51
51
  tagOverlap: 0.15,
52
52
  domainMatch: 0.15,
53
53
  };
@@ -56,7 +56,7 @@ const COGNEE_WEIGHTS: ScoringWeights = {
56
56
  semantic: 0.25,
57
57
  vector: 0.35,
58
58
  severity: 0.1,
59
- recency: 0.1,
59
+ temporalDecay: 0.1,
60
60
  tagOverlap: 0.1,
61
61
  domainMatch: 0.1,
62
62
  };
@@ -454,6 +454,42 @@ export class Brain {
454
454
  return this.vocabulary.size;
455
455
  }
456
456
 
457
+ async getDecayReport(
458
+ query: string,
459
+ limit: number = 10,
460
+ ): Promise<
461
+ Array<{
462
+ id: string;
463
+ title: string;
464
+ decayScore: number;
465
+ validUntil: number | null;
466
+ status: 'active' | 'expiring' | 'expired';
467
+ }>
468
+ > {
469
+ const results = await this.intelligentSearch(query, { limit });
470
+ const now = Math.floor(Date.now() / 1000);
471
+ return results.map((r) => {
472
+ const validUntil = r.entry.validUntil ?? null;
473
+ let status: 'active' | 'expiring' | 'expired' = 'active';
474
+ if (validUntil) {
475
+ if (validUntil <= now) status = 'expired';
476
+ else {
477
+ const validFrom = r.entry.validFrom ?? now;
478
+ const totalWindow = validUntil - validFrom;
479
+ const remaining = validUntil - now;
480
+ if (remaining <= totalWindow * 0.25) status = 'expiring';
481
+ }
482
+ }
483
+ return {
484
+ id: r.entry.id,
485
+ title: r.entry.title,
486
+ decayScore: r.breakdown.temporalDecay,
487
+ validUntil,
488
+ status,
489
+ };
490
+ });
491
+ }
492
+
457
493
  // ─── Private methods ─────────────────────────────────────────────
458
494
 
459
495
  private scoreEntry(
@@ -483,9 +519,7 @@ export class Brain {
483
519
 
484
520
  const severity = SEVERITY_SCORES[entry.severity] ?? 0.4;
485
521
 
486
- const entryAge = now - (entry as unknown as { created_at?: number }).created_at!;
487
- const halfLifeSeconds = RECENCY_HALF_LIFE_DAYS * 86400;
488
- const recency = entryAge > 0 ? Math.exp((-Math.LN2 * entryAge) / halfLifeSeconds) : 1;
522
+ const temporalDecay = computeTemporalDecay(entry, now);
489
523
 
490
524
  const tagOverlap = queryTags.length > 0 ? jaccardSimilarity(queryTags, entry.tags) : 0;
491
525
 
@@ -497,11 +531,11 @@ export class Brain {
497
531
  w.semantic * semantic +
498
532
  w.vector * vector +
499
533
  w.severity * severity +
500
- w.recency * recency +
534
+ w.temporalDecay * temporalDecay +
501
535
  w.tagOverlap * tagOverlap +
502
536
  w.domainMatch * domainMatch;
503
537
 
504
- return { semantic, vector, severity, recency, tagOverlap, domainMatch, total };
538
+ return { semantic, vector, severity, temporalDecay, tagOverlap, domainMatch, total };
505
539
  }
506
540
 
507
541
  private generateTags(title: string, description: string, context?: string): string[] {
@@ -640,12 +674,12 @@ export class Brain {
640
674
  const remaining = 1.0 - newWeights.semantic - newWeights.vector;
641
675
  const otherSum =
642
676
  DEFAULT_WEIGHTS.severity +
643
- DEFAULT_WEIGHTS.recency +
677
+ DEFAULT_WEIGHTS.temporalDecay +
644
678
  DEFAULT_WEIGHTS.tagOverlap +
645
679
  DEFAULT_WEIGHTS.domainMatch;
646
680
  const scale = remaining / otherSum;
647
681
  newWeights.severity = DEFAULT_WEIGHTS.severity * scale;
648
- newWeights.recency = DEFAULT_WEIGHTS.recency * scale;
682
+ newWeights.temporalDecay = DEFAULT_WEIGHTS.temporalDecay * scale;
649
683
  newWeights.tagOverlap = DEFAULT_WEIGHTS.tagOverlap * scale;
650
684
  newWeights.domainMatch = DEFAULT_WEIGHTS.domainMatch * scale;
651
685
 
@@ -653,6 +687,34 @@ export class Brain {
653
687
  }
654
688
  }
655
689
 
690
+ function computeTemporalDecay(entry: IntelligenceEntry, now: number): number {
691
+ const entryRecord = entry as unknown as {
692
+ created_at?: number;
693
+ updated_at?: number;
694
+ valid_until?: number;
695
+ valid_from?: number;
696
+ };
697
+ const validUntil = entry.validUntil ?? entryRecord.valid_until;
698
+
699
+ if (!validUntil) {
700
+ // No expiry — use existing age-based exponential decay
701
+ const updatedAt = entryRecord.updated_at ?? entryRecord.created_at ?? now;
702
+ const ageSeconds = now - updatedAt;
703
+ const halfLifeSeconds = RECENCY_HALF_LIFE_DAYS * 86400;
704
+ return ageSeconds > 0 ? Math.exp((-Math.LN2 * ageSeconds) / halfLifeSeconds) : 1;
705
+ }
706
+
707
+ // With valid_until: linear ramp-down in last 25% of validity window
708
+ const validFrom = entry.validFrom ?? entryRecord.valid_from ?? entryRecord.created_at ?? now;
709
+ const totalWindow = validUntil - validFrom;
710
+ const remaining = validUntil - now;
711
+ if (remaining <= 0) return 0; // expired
712
+ if (totalWindow <= 0) return 1; // edge case: bad data
713
+ const decayZone = totalWindow * 0.25;
714
+ if (remaining > decayZone) return 1.0; // fully valid
715
+ return remaining / decayZone; // linear decay in last quarter
716
+ }
717
+
656
718
  function clamp(value: number, min: number, max: number): number {
657
719
  return Math.max(min, Math.min(max, value));
658
720
  }