@soleri/core 2.1.0 → 2.4.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 (207) hide show
  1. package/dist/brain/brain.d.ts +3 -1
  2. package/dist/brain/brain.d.ts.map +1 -1
  3. package/dist/brain/brain.js +60 -4
  4. package/dist/brain/brain.js.map +1 -1
  5. package/dist/brain/intelligence.d.ts +36 -1
  6. package/dist/brain/intelligence.d.ts.map +1 -1
  7. package/dist/brain/intelligence.js +119 -14
  8. package/dist/brain/intelligence.js.map +1 -1
  9. package/dist/brain/types.d.ts +32 -0
  10. package/dist/brain/types.d.ts.map +1 -1
  11. package/dist/control/identity-manager.d.ts +22 -0
  12. package/dist/control/identity-manager.d.ts.map +1 -0
  13. package/dist/control/identity-manager.js +233 -0
  14. package/dist/control/identity-manager.js.map +1 -0
  15. package/dist/control/intent-router.d.ts +32 -0
  16. package/dist/control/intent-router.d.ts.map +1 -0
  17. package/dist/control/intent-router.js +242 -0
  18. package/dist/control/intent-router.js.map +1 -0
  19. package/dist/control/types.d.ts +68 -0
  20. package/dist/control/types.d.ts.map +1 -0
  21. package/dist/control/types.js +9 -0
  22. package/dist/control/types.js.map +1 -0
  23. package/dist/curator/curator.d.ts +29 -0
  24. package/dist/curator/curator.d.ts.map +1 -1
  25. package/dist/curator/curator.js +135 -0
  26. package/dist/curator/curator.js.map +1 -1
  27. package/dist/facades/types.d.ts +1 -1
  28. package/dist/governance/governance.d.ts +42 -0
  29. package/dist/governance/governance.d.ts.map +1 -0
  30. package/dist/governance/governance.js +488 -0
  31. package/dist/governance/governance.js.map +1 -0
  32. package/dist/governance/index.d.ts +3 -0
  33. package/dist/governance/index.d.ts.map +1 -0
  34. package/dist/governance/index.js +2 -0
  35. package/dist/governance/index.js.map +1 -0
  36. package/dist/governance/types.d.ts +102 -0
  37. package/dist/governance/types.d.ts.map +1 -0
  38. package/dist/governance/types.js +3 -0
  39. package/dist/governance/types.js.map +1 -0
  40. package/dist/index.d.ts +32 -3
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +29 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/logging/logger.d.ts +37 -0
  45. package/dist/logging/logger.d.ts.map +1 -0
  46. package/dist/logging/logger.js +145 -0
  47. package/dist/logging/logger.js.map +1 -0
  48. package/dist/logging/types.d.ts +19 -0
  49. package/dist/logging/types.d.ts.map +1 -0
  50. package/dist/logging/types.js +2 -0
  51. package/dist/logging/types.js.map +1 -0
  52. package/dist/loop/loop-manager.d.ts +49 -0
  53. package/dist/loop/loop-manager.d.ts.map +1 -0
  54. package/dist/loop/loop-manager.js +105 -0
  55. package/dist/loop/loop-manager.js.map +1 -0
  56. package/dist/loop/types.d.ts +35 -0
  57. package/dist/loop/types.d.ts.map +1 -0
  58. package/dist/loop/types.js +8 -0
  59. package/dist/loop/types.js.map +1 -0
  60. package/dist/planning/gap-analysis.d.ts +29 -0
  61. package/dist/planning/gap-analysis.d.ts.map +1 -0
  62. package/dist/planning/gap-analysis.js +265 -0
  63. package/dist/planning/gap-analysis.js.map +1 -0
  64. package/dist/planning/gap-types.d.ts +29 -0
  65. package/dist/planning/gap-types.d.ts.map +1 -0
  66. package/dist/planning/gap-types.js +28 -0
  67. package/dist/planning/gap-types.js.map +1 -0
  68. package/dist/planning/planner.d.ts +150 -1
  69. package/dist/planning/planner.d.ts.map +1 -1
  70. package/dist/planning/planner.js +365 -2
  71. package/dist/planning/planner.js.map +1 -1
  72. package/dist/project/project-registry.d.ts +79 -0
  73. package/dist/project/project-registry.d.ts.map +1 -0
  74. package/dist/project/project-registry.js +276 -0
  75. package/dist/project/project-registry.js.map +1 -0
  76. package/dist/project/types.d.ts +28 -0
  77. package/dist/project/types.d.ts.map +1 -0
  78. package/dist/project/types.js +5 -0
  79. package/dist/project/types.js.map +1 -0
  80. package/dist/runtime/admin-extra-ops.d.ts +13 -0
  81. package/dist/runtime/admin-extra-ops.d.ts.map +1 -0
  82. package/dist/runtime/admin-extra-ops.js +284 -0
  83. package/dist/runtime/admin-extra-ops.js.map +1 -0
  84. package/dist/runtime/admin-ops.d.ts +15 -0
  85. package/dist/runtime/admin-ops.d.ts.map +1 -0
  86. package/dist/runtime/admin-ops.js +322 -0
  87. package/dist/runtime/admin-ops.js.map +1 -0
  88. package/dist/runtime/capture-ops.d.ts +15 -0
  89. package/dist/runtime/capture-ops.d.ts.map +1 -0
  90. package/dist/runtime/capture-ops.js +345 -0
  91. package/dist/runtime/capture-ops.js.map +1 -0
  92. package/dist/runtime/core-ops.d.ts +7 -3
  93. package/dist/runtime/core-ops.d.ts.map +1 -1
  94. package/dist/runtime/core-ops.js +474 -8
  95. package/dist/runtime/core-ops.js.map +1 -1
  96. package/dist/runtime/curator-extra-ops.d.ts +9 -0
  97. package/dist/runtime/curator-extra-ops.d.ts.map +1 -0
  98. package/dist/runtime/curator-extra-ops.js +59 -0
  99. package/dist/runtime/curator-extra-ops.js.map +1 -0
  100. package/dist/runtime/domain-ops.d.ts.map +1 -1
  101. package/dist/runtime/domain-ops.js +59 -13
  102. package/dist/runtime/domain-ops.js.map +1 -1
  103. package/dist/runtime/grading-ops.d.ts +14 -0
  104. package/dist/runtime/grading-ops.d.ts.map +1 -0
  105. package/dist/runtime/grading-ops.js +105 -0
  106. package/dist/runtime/grading-ops.js.map +1 -0
  107. package/dist/runtime/loop-ops.d.ts +13 -0
  108. package/dist/runtime/loop-ops.d.ts.map +1 -0
  109. package/dist/runtime/loop-ops.js +179 -0
  110. package/dist/runtime/loop-ops.js.map +1 -0
  111. package/dist/runtime/memory-cross-project-ops.d.ts +12 -0
  112. package/dist/runtime/memory-cross-project-ops.d.ts.map +1 -0
  113. package/dist/runtime/memory-cross-project-ops.js +165 -0
  114. package/dist/runtime/memory-cross-project-ops.js.map +1 -0
  115. package/dist/runtime/memory-extra-ops.d.ts +13 -0
  116. package/dist/runtime/memory-extra-ops.d.ts.map +1 -0
  117. package/dist/runtime/memory-extra-ops.js +173 -0
  118. package/dist/runtime/memory-extra-ops.js.map +1 -0
  119. package/dist/runtime/orchestrate-ops.d.ts +17 -0
  120. package/dist/runtime/orchestrate-ops.d.ts.map +1 -0
  121. package/dist/runtime/orchestrate-ops.js +240 -0
  122. package/dist/runtime/orchestrate-ops.js.map +1 -0
  123. package/dist/runtime/planning-extra-ops.d.ts +17 -0
  124. package/dist/runtime/planning-extra-ops.d.ts.map +1 -0
  125. package/dist/runtime/planning-extra-ops.js +300 -0
  126. package/dist/runtime/planning-extra-ops.js.map +1 -0
  127. package/dist/runtime/project-ops.d.ts +15 -0
  128. package/dist/runtime/project-ops.d.ts.map +1 -0
  129. package/dist/runtime/project-ops.js +181 -0
  130. package/dist/runtime/project-ops.js.map +1 -0
  131. package/dist/runtime/runtime.d.ts.map +1 -1
  132. package/dist/runtime/runtime.js +44 -1
  133. package/dist/runtime/runtime.js.map +1 -1
  134. package/dist/runtime/types.d.ts +21 -0
  135. package/dist/runtime/types.d.ts.map +1 -1
  136. package/dist/runtime/vault-extra-ops.d.ts +9 -0
  137. package/dist/runtime/vault-extra-ops.d.ts.map +1 -0
  138. package/dist/runtime/vault-extra-ops.js +195 -0
  139. package/dist/runtime/vault-extra-ops.js.map +1 -0
  140. package/dist/telemetry/telemetry.d.ts +48 -0
  141. package/dist/telemetry/telemetry.d.ts.map +1 -0
  142. package/dist/telemetry/telemetry.js +87 -0
  143. package/dist/telemetry/telemetry.js.map +1 -0
  144. package/dist/vault/vault.d.ts +94 -0
  145. package/dist/vault/vault.d.ts.map +1 -1
  146. package/dist/vault/vault.js +340 -1
  147. package/dist/vault/vault.js.map +1 -1
  148. package/package.json +1 -1
  149. package/src/__tests__/admin-extra-ops.test.ts +420 -0
  150. package/src/__tests__/admin-ops.test.ts +271 -0
  151. package/src/__tests__/brain-intelligence.test.ts +205 -0
  152. package/src/__tests__/brain.test.ts +131 -0
  153. package/src/__tests__/capture-ops.test.ts +509 -0
  154. package/src/__tests__/core-ops.test.ts +266 -2
  155. package/src/__tests__/curator-extra-ops.test.ts +359 -0
  156. package/src/__tests__/domain-ops.test.ts +66 -0
  157. package/src/__tests__/governance.test.ts +522 -0
  158. package/src/__tests__/grading-ops.test.ts +340 -0
  159. package/src/__tests__/identity-manager.test.ts +243 -0
  160. package/src/__tests__/intent-router.test.ts +222 -0
  161. package/src/__tests__/logger.test.ts +200 -0
  162. package/src/__tests__/loop-ops.test.ts +398 -0
  163. package/src/__tests__/memory-cross-project-ops.test.ts +246 -0
  164. package/src/__tests__/memory-extra-ops.test.ts +352 -0
  165. package/src/__tests__/orchestrate-ops.test.ts +284 -0
  166. package/src/__tests__/planner.test.ts +331 -0
  167. package/src/__tests__/planning-extra-ops.test.ts +548 -0
  168. package/src/__tests__/project-ops.test.ts +367 -0
  169. package/src/__tests__/vault-extra-ops.test.ts +407 -0
  170. package/src/brain/brain.ts +114 -7
  171. package/src/brain/intelligence.ts +179 -10
  172. package/src/brain/types.ts +38 -0
  173. package/src/control/identity-manager.ts +354 -0
  174. package/src/control/intent-router.ts +326 -0
  175. package/src/control/types.ts +102 -0
  176. package/src/curator/curator.ts +213 -0
  177. package/src/governance/governance.ts +698 -0
  178. package/src/governance/index.ts +18 -0
  179. package/src/governance/types.ts +111 -0
  180. package/src/index.ts +102 -2
  181. package/src/logging/logger.ts +154 -0
  182. package/src/logging/types.ts +21 -0
  183. package/src/loop/loop-manager.ts +130 -0
  184. package/src/loop/types.ts +44 -0
  185. package/src/planning/gap-analysis.ts +506 -0
  186. package/src/planning/gap-types.ts +58 -0
  187. package/src/planning/planner.ts +478 -2
  188. package/src/project/project-registry.ts +358 -0
  189. package/src/project/types.ts +31 -0
  190. package/src/runtime/admin-extra-ops.ts +307 -0
  191. package/src/runtime/admin-ops.ts +329 -0
  192. package/src/runtime/capture-ops.ts +385 -0
  193. package/src/runtime/core-ops.ts +535 -7
  194. package/src/runtime/curator-extra-ops.ts +71 -0
  195. package/src/runtime/domain-ops.ts +65 -13
  196. package/src/runtime/grading-ops.ts +121 -0
  197. package/src/runtime/loop-ops.ts +194 -0
  198. package/src/runtime/memory-cross-project-ops.ts +192 -0
  199. package/src/runtime/memory-extra-ops.ts +186 -0
  200. package/src/runtime/orchestrate-ops.ts +272 -0
  201. package/src/runtime/planning-extra-ops.ts +327 -0
  202. package/src/runtime/project-ops.ts +196 -0
  203. package/src/runtime/runtime.ts +49 -1
  204. package/src/runtime/types.ts +21 -0
  205. package/src/runtime/vault-extra-ops.ts +225 -0
  206. package/src/telemetry/telemetry.ts +118 -0
  207. package/src/vault/vault.ts +412 -1
@@ -0,0 +1,407 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdirSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { createAgentRuntime } from '../runtime/runtime.js';
6
+ import { createVaultExtraOps } from '../runtime/vault-extra-ops.js';
7
+ import type { AgentRuntime } from '../runtime/types.js';
8
+ import type { OpDefinition } from '../facades/types.js';
9
+ import type { IntelligenceEntry } from '../intelligence/types.js';
10
+
11
+ function makeEntry(overrides: Partial<IntelligenceEntry> & { id: string }): IntelligenceEntry {
12
+ return {
13
+ type: 'pattern',
14
+ domain: 'testing',
15
+ title: 'Test entry',
16
+ severity: 'warning',
17
+ description: 'A test entry.',
18
+ tags: ['test'],
19
+ ...overrides,
20
+ };
21
+ }
22
+
23
+ describe('createVaultExtraOps', () => {
24
+ let runtime: AgentRuntime;
25
+ let ops: OpDefinition[];
26
+ let plannerDir: string;
27
+
28
+ beforeEach(() => {
29
+ plannerDir = join(tmpdir(), 'vault-extra-ops-test-' + Date.now());
30
+ mkdirSync(plannerDir, { recursive: true });
31
+ runtime = createAgentRuntime({
32
+ agentId: 'test-vault-extra',
33
+ vaultPath: ':memory:',
34
+ plansPath: join(plannerDir, 'plans.json'),
35
+ });
36
+ ops = createVaultExtraOps(runtime);
37
+ });
38
+
39
+ afterEach(() => {
40
+ runtime.close();
41
+ rmSync(plannerDir, { recursive: true, force: true });
42
+ });
43
+
44
+ function findOp(name: string): OpDefinition {
45
+ const op = ops.find((o) => o.name === name);
46
+ if (!op) throw new Error(`Op "${name}" not found`);
47
+ return op;
48
+ }
49
+
50
+ it('should return 12 ops', () => {
51
+ expect(ops.length).toBe(12);
52
+ });
53
+
54
+ it('should have all expected op names', () => {
55
+ const names = ops.map((o) => o.name);
56
+ expect(names).toContain('vault_get');
57
+ expect(names).toContain('vault_update');
58
+ expect(names).toContain('vault_remove');
59
+ expect(names).toContain('vault_bulk_add');
60
+ expect(names).toContain('vault_bulk_remove');
61
+ expect(names).toContain('vault_tags');
62
+ expect(names).toContain('vault_domains');
63
+ expect(names).toContain('vault_recent');
64
+ expect(names).toContain('vault_import');
65
+ expect(names).toContain('vault_seed');
66
+ expect(names).toContain('vault_backup');
67
+ expect(names).toContain('vault_age_report');
68
+ });
69
+
70
+ // ─── vault_get ────────────────────────────────────────────────────
71
+
72
+ it('vault_get should return entry when found', async () => {
73
+ runtime.vault.seed([makeEntry({ id: 'vg-1', title: 'Get test' })]);
74
+ const result = (await findOp('vault_get').handler({ id: 'vg-1' })) as IntelligenceEntry;
75
+ expect(result.id).toBe('vg-1');
76
+ expect(result.title).toBe('Get test');
77
+ });
78
+
79
+ it('vault_get should return error when not found', async () => {
80
+ const result = (await findOp('vault_get').handler({ id: 'nonexistent' })) as {
81
+ error: string;
82
+ };
83
+ expect(result.error).toContain('nonexistent');
84
+ });
85
+
86
+ // ─── vault_update ─────────────────────────────────────────────────
87
+
88
+ it('vault_update should update title', async () => {
89
+ runtime.vault.seed([makeEntry({ id: 'vu-1', title: 'Original' })]);
90
+ const result = (await findOp('vault_update').handler({
91
+ id: 'vu-1',
92
+ title: 'Updated',
93
+ })) as { updated: boolean; entry: IntelligenceEntry };
94
+ expect(result.updated).toBe(true);
95
+ expect(result.entry.title).toBe('Updated');
96
+ // Verify in vault directly
97
+ expect(runtime.vault.get('vu-1')!.title).toBe('Updated');
98
+ });
99
+
100
+ it('vault_update should update tags', async () => {
101
+ runtime.vault.seed([makeEntry({ id: 'vu-2', tags: ['old'] })]);
102
+ const result = (await findOp('vault_update').handler({
103
+ id: 'vu-2',
104
+ tags: ['new', 'shiny'],
105
+ })) as { updated: boolean; entry: IntelligenceEntry };
106
+ expect(result.entry.tags).toEqual(['new', 'shiny']);
107
+ });
108
+
109
+ it('vault_update should return error when entry not found', async () => {
110
+ const result = (await findOp('vault_update').handler({
111
+ id: 'missing',
112
+ title: 'x',
113
+ })) as { error: string };
114
+ expect(result.error).toContain('missing');
115
+ });
116
+
117
+ it('vault_update should return error when no fields provided', async () => {
118
+ runtime.vault.seed([makeEntry({ id: 'vu-3' })]);
119
+ const result = (await findOp('vault_update').handler({ id: 'vu-3' })) as { error: string };
120
+ expect(result.error).toContain('No fields');
121
+ });
122
+
123
+ // ─── vault_remove ─────────────────────────────────────────────────
124
+
125
+ it('vault_remove should delete entry', async () => {
126
+ runtime.vault.seed([makeEntry({ id: 'vr-1' })]);
127
+ const result = (await findOp('vault_remove').handler({ id: 'vr-1' })) as {
128
+ removed: boolean;
129
+ id: string;
130
+ };
131
+ expect(result.removed).toBe(true);
132
+ expect(result.id).toBe('vr-1');
133
+ expect(runtime.vault.get('vr-1')).toBeNull();
134
+ });
135
+
136
+ it('vault_remove should return removed=false for missing entry', async () => {
137
+ const result = (await findOp('vault_remove').handler({ id: 'nope' })) as { removed: boolean };
138
+ expect(result.removed).toBe(false);
139
+ });
140
+
141
+ // ─── vault_bulk_add ───────────────────────────────────────────────
142
+
143
+ it('vault_bulk_add should add multiple entries', async () => {
144
+ const entries = [
145
+ makeEntry({ id: 'ba-1', title: 'Bulk 1' }),
146
+ makeEntry({ id: 'ba-2', title: 'Bulk 2' }),
147
+ makeEntry({ id: 'ba-3', title: 'Bulk 3' }),
148
+ ];
149
+ const result = (await findOp('vault_bulk_add').handler({ entries })) as {
150
+ added: number;
151
+ total: number;
152
+ };
153
+ expect(result.added).toBe(3);
154
+ expect(result.total).toBe(3);
155
+ expect(runtime.vault.get('ba-2')!.title).toBe('Bulk 2');
156
+ });
157
+
158
+ it('vault_bulk_add should upsert existing entries', async () => {
159
+ runtime.vault.seed([makeEntry({ id: 'ba-u1', title: 'Old' })]);
160
+ const result = (await findOp('vault_bulk_add').handler({
161
+ entries: [makeEntry({ id: 'ba-u1', title: 'New' })],
162
+ })) as { added: number; total: number };
163
+ expect(result.added).toBe(1);
164
+ expect(result.total).toBe(1); // no new entry, just update
165
+ expect(runtime.vault.get('ba-u1')!.title).toBe('New');
166
+ });
167
+
168
+ // ─── vault_bulk_remove ────────────────────────────────────────────
169
+
170
+ it('vault_bulk_remove should remove multiple entries', async () => {
171
+ runtime.vault.seed([
172
+ makeEntry({ id: 'br-1' }),
173
+ makeEntry({ id: 'br-2' }),
174
+ makeEntry({ id: 'br-3' }),
175
+ ]);
176
+ const result = (await findOp('vault_bulk_remove').handler({
177
+ ids: ['br-1', 'br-3'],
178
+ })) as { removed: number; requested: number; total: number };
179
+ expect(result.removed).toBe(2);
180
+ expect(result.requested).toBe(2);
181
+ expect(result.total).toBe(1); // br-2 remains
182
+ expect(runtime.vault.get('br-2')).not.toBeNull();
183
+ expect(runtime.vault.get('br-1')).toBeNull();
184
+ });
185
+
186
+ it('vault_bulk_remove should handle missing IDs gracefully', async () => {
187
+ runtime.vault.seed([makeEntry({ id: 'br-4' })]);
188
+ const result = (await findOp('vault_bulk_remove').handler({
189
+ ids: ['br-4', 'br-missing'],
190
+ })) as { removed: number; requested: number };
191
+ expect(result.removed).toBe(1);
192
+ expect(result.requested).toBe(2);
193
+ });
194
+
195
+ // ─── vault_tags ───────────────────────────────────────────────────
196
+
197
+ it('vault_tags should return unique tags with counts', async () => {
198
+ runtime.vault.seed([
199
+ makeEntry({ id: 'vt-1', tags: ['alpha', 'beta'] }),
200
+ makeEntry({ id: 'vt-2', tags: ['beta', 'gamma'] }),
201
+ makeEntry({ id: 'vt-3', tags: ['beta'] }),
202
+ ]);
203
+ const result = (await findOp('vault_tags').handler({})) as {
204
+ tags: Array<{ tag: string; count: number }>;
205
+ count: number;
206
+ };
207
+ expect(result.count).toBe(3); // alpha, beta, gamma
208
+ const beta = result.tags.find((t) => t.tag === 'beta');
209
+ expect(beta!.count).toBe(3);
210
+ const alpha = result.tags.find((t) => t.tag === 'alpha');
211
+ expect(alpha!.count).toBe(1);
212
+ // beta should be first (highest count)
213
+ expect(result.tags[0].tag).toBe('beta');
214
+ });
215
+
216
+ it('vault_tags should return empty when vault is empty', async () => {
217
+ const result = (await findOp('vault_tags').handler({})) as {
218
+ tags: Array<{ tag: string; count: number }>;
219
+ count: number;
220
+ };
221
+ expect(result.tags).toEqual([]);
222
+ expect(result.count).toBe(0);
223
+ });
224
+
225
+ // ─── vault_domains ────────────────────────────────────────────────
226
+
227
+ it('vault_domains should return domains with counts', async () => {
228
+ runtime.vault.seed([
229
+ makeEntry({ id: 'vd-1', domain: 'security' }),
230
+ makeEntry({ id: 'vd-2', domain: 'security' }),
231
+ makeEntry({ id: 'vd-3', domain: 'a11y' }),
232
+ ]);
233
+ const result = (await findOp('vault_domains').handler({})) as {
234
+ domains: Array<{ domain: string; count: number }>;
235
+ count: number;
236
+ };
237
+ expect(result.count).toBe(2);
238
+ const security = result.domains.find((d) => d.domain === 'security');
239
+ expect(security!.count).toBe(2);
240
+ const a11y = result.domains.find((d) => d.domain === 'a11y');
241
+ expect(a11y!.count).toBe(1);
242
+ });
243
+
244
+ // ─── vault_recent ─────────────────────────────────────────────────
245
+
246
+ it('vault_recent should return entries ordered by most recent', async () => {
247
+ runtime.vault.seed([
248
+ makeEntry({ id: 'rec-1', title: 'First' }),
249
+ makeEntry({ id: 'rec-2', title: 'Second' }),
250
+ ]);
251
+ const result = (await findOp('vault_recent').handler({})) as {
252
+ entries: IntelligenceEntry[];
253
+ count: number;
254
+ };
255
+ expect(result.count).toBe(2);
256
+ // Both entries should be present
257
+ expect(result.entries.map((e) => e.id)).toContain('rec-1');
258
+ expect(result.entries.map((e) => e.id)).toContain('rec-2');
259
+ });
260
+
261
+ it('vault_recent should respect limit', async () => {
262
+ runtime.vault.seed([
263
+ makeEntry({ id: 'rl-1' }),
264
+ makeEntry({ id: 'rl-2' }),
265
+ makeEntry({ id: 'rl-3' }),
266
+ ]);
267
+ const result = (await findOp('vault_recent').handler({ limit: 2 })) as {
268
+ entries: IntelligenceEntry[];
269
+ count: number;
270
+ };
271
+ expect(result.count).toBe(2);
272
+ });
273
+
274
+ // ─── vault_import ─────────────────────────────────────────────────
275
+
276
+ it('vault_import should import new entries', async () => {
277
+ const entries = [
278
+ makeEntry({ id: 'vi-1', title: 'Import 1' }),
279
+ makeEntry({ id: 'vi-2', title: 'Import 2' }),
280
+ ];
281
+ const result = (await findOp('vault_import').handler({ entries })) as {
282
+ imported: number;
283
+ newEntries: number;
284
+ updatedEntries: number;
285
+ total: number;
286
+ };
287
+ expect(result.imported).toBe(2);
288
+ expect(result.newEntries).toBe(2);
289
+ expect(result.updatedEntries).toBe(0);
290
+ expect(result.total).toBe(2);
291
+ });
292
+
293
+ it('vault_import should track updated vs new entries', async () => {
294
+ runtime.vault.seed([makeEntry({ id: 'vi-3', title: 'Existing' })]);
295
+ const entries = [
296
+ makeEntry({ id: 'vi-3', title: 'Updated' }),
297
+ makeEntry({ id: 'vi-4', title: 'Brand New' }),
298
+ ];
299
+ const result = (await findOp('vault_import').handler({ entries })) as {
300
+ imported: number;
301
+ newEntries: number;
302
+ updatedEntries: number;
303
+ total: number;
304
+ };
305
+ expect(result.imported).toBe(2);
306
+ expect(result.newEntries).toBe(1);
307
+ expect(result.updatedEntries).toBe(1);
308
+ expect(result.total).toBe(2);
309
+ });
310
+
311
+ // ─── vault_seed ───────────────────────────────────────────────────
312
+
313
+ it('vault_seed should be idempotent', async () => {
314
+ const entries = [makeEntry({ id: 'vs-1', title: 'Seed' })];
315
+ const r1 = (await findOp('vault_seed').handler({ entries })) as {
316
+ seeded: number;
317
+ total: number;
318
+ };
319
+ expect(r1.seeded).toBe(1);
320
+ expect(r1.total).toBe(1);
321
+ // Seed again
322
+ const r2 = (await findOp('vault_seed').handler({ entries })) as {
323
+ seeded: number;
324
+ total: number;
325
+ };
326
+ expect(r2.seeded).toBe(1);
327
+ expect(r2.total).toBe(1); // still 1, not 2
328
+ });
329
+
330
+ // ─── vault_backup ─────────────────────────────────────────────────
331
+
332
+ it('vault_backup should export all entries', async () => {
333
+ runtime.vault.seed([
334
+ makeEntry({ id: 'vb-1', title: 'Backup 1' }),
335
+ makeEntry({ id: 'vb-2', title: 'Backup 2' }),
336
+ ]);
337
+ const result = (await findOp('vault_backup').handler({})) as {
338
+ entries: IntelligenceEntry[];
339
+ exportedAt: number;
340
+ count: number;
341
+ };
342
+ expect(result.count).toBe(2);
343
+ expect(result.entries.length).toBe(2);
344
+ expect(result.exportedAt).toBeGreaterThan(0);
345
+ expect(result.entries.map((e) => e.id).sort()).toEqual(['vb-1', 'vb-2']);
346
+ });
347
+
348
+ it('vault_backup should return empty bundle when vault is empty', async () => {
349
+ const result = (await findOp('vault_backup').handler({})) as {
350
+ entries: IntelligenceEntry[];
351
+ count: number;
352
+ };
353
+ expect(result.entries).toEqual([]);
354
+ expect(result.count).toBe(0);
355
+ });
356
+
357
+ // ─── vault_age_report ─────────────────────────────────────────────
358
+
359
+ it('vault_age_report should return age distribution', async () => {
360
+ runtime.vault.seed([
361
+ makeEntry({ id: 'va-1' }),
362
+ makeEntry({ id: 'va-2' }),
363
+ ]);
364
+ const result = (await findOp('vault_age_report').handler({})) as {
365
+ total: number;
366
+ buckets: Array<{ label: string; count: number; minDays: number; maxDays: number }>;
367
+ oldestTimestamp: number | null;
368
+ newestTimestamp: number | null;
369
+ };
370
+ expect(result.total).toBe(2);
371
+ expect(result.buckets.length).toBe(5);
372
+ // Entries just created should be in the 'today' bucket
373
+ const today = result.buckets.find((b) => b.label === 'today');
374
+ expect(today!.count).toBe(2);
375
+ expect(result.oldestTimestamp).toBeGreaterThan(0);
376
+ expect(result.newestTimestamp).toBeGreaterThan(0);
377
+ });
378
+
379
+ it('vault_age_report should handle empty vault', async () => {
380
+ const result = (await findOp('vault_age_report').handler({})) as {
381
+ total: number;
382
+ oldestTimestamp: number | null;
383
+ newestTimestamp: number | null;
384
+ };
385
+ expect(result.total).toBe(0);
386
+ expect(result.oldestTimestamp).toBeNull();
387
+ expect(result.newestTimestamp).toBeNull();
388
+ });
389
+
390
+ // ─── Auth levels ──────────────────────────────────────────────────
391
+
392
+ it('should assign correct auth levels', () => {
393
+ const readOps = ['vault_get', 'vault_tags', 'vault_domains', 'vault_recent', 'vault_backup', 'vault_age_report'];
394
+ const writeOps = ['vault_update', 'vault_bulk_add', 'vault_import', 'vault_seed'];
395
+ const adminOps = ['vault_remove', 'vault_bulk_remove'];
396
+
397
+ for (const name of readOps) {
398
+ expect(findOp(name).auth).toBe('read');
399
+ }
400
+ for (const name of writeOps) {
401
+ expect(findOp(name).auth).toBe('write');
402
+ }
403
+ for (const name of adminOps) {
404
+ expect(findOp(name).auth).toBe('admin');
405
+ }
406
+ });
407
+ });
@@ -17,6 +17,9 @@ import type {
17
17
  CaptureResult,
18
18
  BrainStats,
19
19
  QueryContext,
20
+ FeedbackInput,
21
+ FeedbackEntry,
22
+ FeedbackStats,
20
23
  } from './types.js';
21
24
 
22
25
  // Re-export types for backward compatibility
@@ -268,14 +271,109 @@ export class Brain {
268
271
  return result;
269
272
  }
270
273
 
271
- recordFeedback(query: string, entryId: string, action: 'accepted' | 'dismissed'): void {
274
+ recordFeedback(query: string, entryId: string, action: 'accepted' | 'dismissed'): void;
275
+ recordFeedback(input: FeedbackInput): FeedbackEntry;
276
+ recordFeedback(
277
+ queryOrInput: string | FeedbackInput,
278
+ entryId?: string,
279
+ action?: 'accepted' | 'dismissed',
280
+ ): void | FeedbackEntry {
272
281
  const db = this.vault.getDb();
273
- db.prepare('INSERT INTO brain_feedback (query, entry_id, action) VALUES (?, ?, ?)').run(
274
- query,
275
- entryId,
276
- action,
282
+
283
+ // Normalize to FeedbackInput
284
+ const input: FeedbackInput =
285
+ typeof queryOrInput === 'string'
286
+ ? { query: queryOrInput, entryId: entryId!, action: action! }
287
+ : queryOrInput;
288
+
289
+ db.prepare(
290
+ `INSERT INTO brain_feedback (query, entry_id, action, source, confidence, duration, context, reason)
291
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
292
+ ).run(
293
+ input.query,
294
+ input.entryId,
295
+ input.action,
296
+ input.source ?? 'search',
297
+ input.confidence ?? 0.6,
298
+ input.duration ?? null,
299
+ input.context ?? '{}',
300
+ input.reason ?? null,
277
301
  );
278
302
  this.recomputeWeights();
303
+
304
+ // Return FeedbackEntry only for the object overload
305
+ if (typeof queryOrInput !== 'string') {
306
+ const row = db
307
+ .prepare(
308
+ 'SELECT * FROM brain_feedback WHERE query = ? AND entry_id = ? ORDER BY id DESC LIMIT 1',
309
+ )
310
+ .get(input.query, input.entryId) as {
311
+ id: number;
312
+ query: string;
313
+ entry_id: string;
314
+ action: string;
315
+ source: string;
316
+ confidence: number;
317
+ duration: number | null;
318
+ context: string;
319
+ reason: string | null;
320
+ created_at: number;
321
+ };
322
+ return {
323
+ id: row.id,
324
+ query: row.query,
325
+ entryId: row.entry_id,
326
+ action: row.action as FeedbackEntry['action'],
327
+ source: row.source as FeedbackEntry['source'],
328
+ confidence: row.confidence,
329
+ duration: row.duration,
330
+ context: row.context,
331
+ reason: row.reason,
332
+ createdAt: row.created_at,
333
+ };
334
+ }
335
+ }
336
+
337
+ getFeedbackStats(): FeedbackStats {
338
+ const db = this.vault.getDb();
339
+
340
+ const total = (
341
+ db.prepare('SELECT COUNT(*) as count FROM brain_feedback').get() as { count: number }
342
+ ).count;
343
+
344
+ const byAction: Record<string, number> = {};
345
+ const actionRows = db
346
+ .prepare('SELECT action, COUNT(*) as count FROM brain_feedback GROUP BY action')
347
+ .all() as Array<{ action: string; count: number }>;
348
+ for (const row of actionRows) {
349
+ byAction[row.action] = row.count;
350
+ }
351
+
352
+ const bySource: Record<string, number> = {};
353
+ const sourceRows = db
354
+ .prepare('SELECT source, COUNT(*) as count FROM brain_feedback GROUP BY source')
355
+ .all() as Array<{ source: string; count: number }>;
356
+ for (const row of sourceRows) {
357
+ bySource[row.source] = row.count;
358
+ }
359
+
360
+ const accepted = byAction['accepted'] ?? 0;
361
+ const acceptanceRate = total > 0 ? accepted / total : 0;
362
+
363
+ const avgConf =
364
+ (
365
+ db.prepare('SELECT AVG(confidence) as avg FROM brain_feedback').get() as {
366
+ avg: number | null;
367
+ }
368
+ ).avg ?? 0;
369
+
370
+ return {
371
+ total,
372
+ byAction,
373
+ bySource,
374
+ acceptanceRate,
375
+ averageConfidence: avgConf,
376
+ };
279
377
  }
280
378
 
281
379
  async getRelevantPatterns(context: QueryContext): Promise<RankedResult[]> {
@@ -503,8 +601,11 @@ export class Brain {
503
601
 
504
602
  private recomputeWeights(): void {
505
603
  const db = this.vault.getDb();
604
+ // Exclude 'failed' from weight computation — system errors don't indicate relevance
506
605
  const feedbackCount = (
507
- db.prepare('SELECT COUNT(*) as count FROM brain_feedback').get() as { count: number }
606
+ db.prepare("SELECT COUNT(*) as count FROM brain_feedback WHERE action != 'failed'").get() as {
607
+ count: number;
608
+ }
508
609
  ).count;
509
610
  if (feedbackCount < FEEDBACK_THRESHOLD) {
510
611
  this.weights = { ...DEFAULT_WEIGHTS };
@@ -516,7 +617,13 @@ export class Brain {
516
617
  .prepare("SELECT COUNT(*) as count FROM brain_feedback WHERE action = 'accepted'")
517
618
  .get() as { count: number }
518
619
  ).count;
519
- const acceptRate = feedbackCount > 0 ? accepted / feedbackCount : 0.5;
620
+ // 'modified' counts as 0.5 positive user adjusted but didn't dismiss
621
+ const modified = (
622
+ db
623
+ .prepare("SELECT COUNT(*) as count FROM brain_feedback WHERE action = 'modified'")
624
+ .get() as { count: number }
625
+ ).count;
626
+ const acceptRate = feedbackCount > 0 ? (accepted + modified * 0.5) / feedbackCount : 0.5;
520
627
 
521
628
  const semanticDelta = (acceptRate - 0.5) * WEIGHT_BOUND * 2;
522
629