@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.
- package/dist/brain/brain.d.ts +3 -1
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +60 -4
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts +36 -1
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +119 -14
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/types.d.ts +32 -0
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/control/identity-manager.d.ts +22 -0
- package/dist/control/identity-manager.d.ts.map +1 -0
- package/dist/control/identity-manager.js +233 -0
- package/dist/control/identity-manager.js.map +1 -0
- package/dist/control/intent-router.d.ts +32 -0
- package/dist/control/intent-router.d.ts.map +1 -0
- package/dist/control/intent-router.js +242 -0
- package/dist/control/intent-router.js.map +1 -0
- package/dist/control/types.d.ts +68 -0
- package/dist/control/types.d.ts.map +1 -0
- package/dist/control/types.js +9 -0
- package/dist/control/types.js.map +1 -0
- package/dist/curator/curator.d.ts +29 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +135 -0
- package/dist/curator/curator.js.map +1 -1
- package/dist/facades/types.d.ts +1 -1
- package/dist/governance/governance.d.ts +42 -0
- package/dist/governance/governance.d.ts.map +1 -0
- package/dist/governance/governance.js +488 -0
- package/dist/governance/governance.js.map +1 -0
- package/dist/governance/index.d.ts +3 -0
- package/dist/governance/index.d.ts.map +1 -0
- package/dist/governance/index.js +2 -0
- package/dist/governance/index.js.map +1 -0
- package/dist/governance/types.d.ts +102 -0
- package/dist/governance/types.d.ts.map +1 -0
- package/dist/governance/types.js +3 -0
- package/dist/governance/types.js.map +1 -0
- package/dist/index.d.ts +32 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -1
- package/dist/index.js.map +1 -1
- package/dist/logging/logger.d.ts +37 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +145 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/logging/types.d.ts +19 -0
- package/dist/logging/types.d.ts.map +1 -0
- package/dist/logging/types.js +2 -0
- package/dist/logging/types.js.map +1 -0
- package/dist/loop/loop-manager.d.ts +49 -0
- package/dist/loop/loop-manager.d.ts.map +1 -0
- package/dist/loop/loop-manager.js +105 -0
- package/dist/loop/loop-manager.js.map +1 -0
- package/dist/loop/types.d.ts +35 -0
- package/dist/loop/types.d.ts.map +1 -0
- package/dist/loop/types.js +8 -0
- package/dist/loop/types.js.map +1 -0
- package/dist/planning/gap-analysis.d.ts +29 -0
- package/dist/planning/gap-analysis.d.ts.map +1 -0
- package/dist/planning/gap-analysis.js +265 -0
- package/dist/planning/gap-analysis.js.map +1 -0
- package/dist/planning/gap-types.d.ts +29 -0
- package/dist/planning/gap-types.d.ts.map +1 -0
- package/dist/planning/gap-types.js +28 -0
- package/dist/planning/gap-types.js.map +1 -0
- package/dist/planning/planner.d.ts +150 -1
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +365 -2
- package/dist/planning/planner.js.map +1 -1
- package/dist/project/project-registry.d.ts +79 -0
- package/dist/project/project-registry.d.ts.map +1 -0
- package/dist/project/project-registry.js +276 -0
- package/dist/project/project-registry.js.map +1 -0
- package/dist/project/types.d.ts +28 -0
- package/dist/project/types.d.ts.map +1 -0
- package/dist/project/types.js +5 -0
- package/dist/project/types.js.map +1 -0
- package/dist/runtime/admin-extra-ops.d.ts +13 -0
- package/dist/runtime/admin-extra-ops.d.ts.map +1 -0
- package/dist/runtime/admin-extra-ops.js +284 -0
- package/dist/runtime/admin-extra-ops.js.map +1 -0
- package/dist/runtime/admin-ops.d.ts +15 -0
- package/dist/runtime/admin-ops.d.ts.map +1 -0
- package/dist/runtime/admin-ops.js +322 -0
- package/dist/runtime/admin-ops.js.map +1 -0
- package/dist/runtime/capture-ops.d.ts +15 -0
- package/dist/runtime/capture-ops.d.ts.map +1 -0
- package/dist/runtime/capture-ops.js +345 -0
- package/dist/runtime/capture-ops.js.map +1 -0
- package/dist/runtime/core-ops.d.ts +7 -3
- package/dist/runtime/core-ops.d.ts.map +1 -1
- package/dist/runtime/core-ops.js +474 -8
- package/dist/runtime/core-ops.js.map +1 -1
- package/dist/runtime/curator-extra-ops.d.ts +9 -0
- package/dist/runtime/curator-extra-ops.d.ts.map +1 -0
- package/dist/runtime/curator-extra-ops.js +59 -0
- package/dist/runtime/curator-extra-ops.js.map +1 -0
- package/dist/runtime/domain-ops.d.ts.map +1 -1
- package/dist/runtime/domain-ops.js +59 -13
- package/dist/runtime/domain-ops.js.map +1 -1
- package/dist/runtime/grading-ops.d.ts +14 -0
- package/dist/runtime/grading-ops.d.ts.map +1 -0
- package/dist/runtime/grading-ops.js +105 -0
- package/dist/runtime/grading-ops.js.map +1 -0
- package/dist/runtime/loop-ops.d.ts +13 -0
- package/dist/runtime/loop-ops.d.ts.map +1 -0
- package/dist/runtime/loop-ops.js +179 -0
- package/dist/runtime/loop-ops.js.map +1 -0
- package/dist/runtime/memory-cross-project-ops.d.ts +12 -0
- package/dist/runtime/memory-cross-project-ops.d.ts.map +1 -0
- package/dist/runtime/memory-cross-project-ops.js +165 -0
- package/dist/runtime/memory-cross-project-ops.js.map +1 -0
- package/dist/runtime/memory-extra-ops.d.ts +13 -0
- package/dist/runtime/memory-extra-ops.d.ts.map +1 -0
- package/dist/runtime/memory-extra-ops.js +173 -0
- package/dist/runtime/memory-extra-ops.js.map +1 -0
- package/dist/runtime/orchestrate-ops.d.ts +17 -0
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -0
- package/dist/runtime/orchestrate-ops.js +240 -0
- package/dist/runtime/orchestrate-ops.js.map +1 -0
- package/dist/runtime/planning-extra-ops.d.ts +17 -0
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -0
- package/dist/runtime/planning-extra-ops.js +300 -0
- package/dist/runtime/planning-extra-ops.js.map +1 -0
- package/dist/runtime/project-ops.d.ts +15 -0
- package/dist/runtime/project-ops.d.ts.map +1 -0
- package/dist/runtime/project-ops.js +181 -0
- package/dist/runtime/project-ops.js.map +1 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +44 -1
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +21 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/vault-extra-ops.d.ts +9 -0
- package/dist/runtime/vault-extra-ops.d.ts.map +1 -0
- package/dist/runtime/vault-extra-ops.js +195 -0
- package/dist/runtime/vault-extra-ops.js.map +1 -0
- package/dist/telemetry/telemetry.d.ts +48 -0
- package/dist/telemetry/telemetry.d.ts.map +1 -0
- package/dist/telemetry/telemetry.js +87 -0
- package/dist/telemetry/telemetry.js.map +1 -0
- package/dist/vault/vault.d.ts +94 -0
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +340 -1
- package/dist/vault/vault.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/admin-extra-ops.test.ts +420 -0
- package/src/__tests__/admin-ops.test.ts +271 -0
- package/src/__tests__/brain-intelligence.test.ts +205 -0
- package/src/__tests__/brain.test.ts +131 -0
- package/src/__tests__/capture-ops.test.ts +509 -0
- package/src/__tests__/core-ops.test.ts +266 -2
- package/src/__tests__/curator-extra-ops.test.ts +359 -0
- package/src/__tests__/domain-ops.test.ts +66 -0
- package/src/__tests__/governance.test.ts +522 -0
- package/src/__tests__/grading-ops.test.ts +340 -0
- package/src/__tests__/identity-manager.test.ts +243 -0
- package/src/__tests__/intent-router.test.ts +222 -0
- package/src/__tests__/logger.test.ts +200 -0
- package/src/__tests__/loop-ops.test.ts +398 -0
- package/src/__tests__/memory-cross-project-ops.test.ts +246 -0
- package/src/__tests__/memory-extra-ops.test.ts +352 -0
- package/src/__tests__/orchestrate-ops.test.ts +284 -0
- package/src/__tests__/planner.test.ts +331 -0
- package/src/__tests__/planning-extra-ops.test.ts +548 -0
- package/src/__tests__/project-ops.test.ts +367 -0
- package/src/__tests__/vault-extra-ops.test.ts +407 -0
- package/src/brain/brain.ts +114 -7
- package/src/brain/intelligence.ts +179 -10
- package/src/brain/types.ts +38 -0
- package/src/control/identity-manager.ts +354 -0
- package/src/control/intent-router.ts +326 -0
- package/src/control/types.ts +102 -0
- package/src/curator/curator.ts +213 -0
- package/src/governance/governance.ts +698 -0
- package/src/governance/index.ts +18 -0
- package/src/governance/types.ts +111 -0
- package/src/index.ts +102 -2
- package/src/logging/logger.ts +154 -0
- package/src/logging/types.ts +21 -0
- package/src/loop/loop-manager.ts +130 -0
- package/src/loop/types.ts +44 -0
- package/src/planning/gap-analysis.ts +506 -0
- package/src/planning/gap-types.ts +58 -0
- package/src/planning/planner.ts +478 -2
- package/src/project/project-registry.ts +358 -0
- package/src/project/types.ts +31 -0
- package/src/runtime/admin-extra-ops.ts +307 -0
- package/src/runtime/admin-ops.ts +329 -0
- package/src/runtime/capture-ops.ts +385 -0
- package/src/runtime/core-ops.ts +535 -7
- package/src/runtime/curator-extra-ops.ts +71 -0
- package/src/runtime/domain-ops.ts +65 -13
- package/src/runtime/grading-ops.ts +121 -0
- package/src/runtime/loop-ops.ts +194 -0
- package/src/runtime/memory-cross-project-ops.ts +192 -0
- package/src/runtime/memory-extra-ops.ts +186 -0
- package/src/runtime/orchestrate-ops.ts +272 -0
- package/src/runtime/planning-extra-ops.ts +327 -0
- package/src/runtime/project-ops.ts +196 -0
- package/src/runtime/runtime.ts +49 -1
- package/src/runtime/types.ts +21 -0
- package/src/runtime/vault-extra-ops.ts +225 -0
- package/src/telemetry/telemetry.ts +118 -0
- 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
|
+
});
|
package/src/brain/brain.ts
CHANGED
|
@@ -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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|