@osmosis-ai/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.d.ts +12 -0
- package/dist/api/index.js +87 -0
- package/dist/api/index.js.map +1 -0
- package/dist/capture/index.d.ts +10 -0
- package/dist/capture/index.js +52 -0
- package/dist/capture/index.js.map +1 -0
- package/dist/distill/index.d.ts +35 -0
- package/dist/distill/index.js +41 -0
- package/dist/distill/index.js.map +1 -0
- package/dist/fitness/index.d.ts +14 -0
- package/dist/fitness/index.js +39 -0
- package/dist/fitness/index.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/retrieval/index.d.ts +11 -0
- package/dist/retrieval/index.js +51 -0
- package/dist/retrieval/index.js.map +1 -0
- package/dist/seeds/index.d.ts +5 -0
- package/dist/seeds/index.js +131 -0
- package/dist/seeds/index.js.map +1 -0
- package/dist/store/index.d.ts +43 -0
- package/dist/store/index.js +230 -0
- package/dist/store/index.js.map +1 -0
- package/dist/types/index.d.ts +53 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/validation/index.d.ts +119 -0
- package/dist/validation/index.js +55 -0
- package/dist/validation/index.js.map +1 -0
- package/package.json +23 -0
- package/src/__tests__/api.test.ts +103 -0
- package/src/__tests__/capture.test.ts +113 -0
- package/src/__tests__/distill.test.ts +83 -0
- package/src/__tests__/fitness.test.ts +58 -0
- package/src/__tests__/retrieval.test.ts +83 -0
- package/src/__tests__/store.test.ts +262 -0
- package/src/__tests__/types.test.ts +92 -0
- package/src/api/index.ts +94 -0
- package/src/capture/index.ts +71 -0
- package/src/distill/index.ts +64 -0
- package/src/fitness/index.ts +56 -0
- package/src/index.ts +51 -0
- package/src/retrieval/index.ts +61 -0
- package/src/seeds/index.ts +146 -0
- package/src/store/index.ts +250 -0
- package/src/types/index.ts +76 -0
- package/src/validation/index.ts +63 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { AtomStore } from '../store/index.js';
|
|
3
|
+
import { captureToolCall, captureOutcome } from '../capture/index.js';
|
|
4
|
+
import type { OutcomeSignals } from '../types/index.js';
|
|
5
|
+
|
|
6
|
+
describe('captureToolCall', () => {
|
|
7
|
+
let store: AtomStore;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => { store = new AtomStore(':memory:'); });
|
|
10
|
+
afterEach(() => { store.close(); });
|
|
11
|
+
|
|
12
|
+
it('captures a successful tool call', () => {
|
|
13
|
+
const id = captureToolCall(store, 'browser.screenshot', { url: 'https://example.com' }, { data: 'png' });
|
|
14
|
+
const atom = store.getById(id)!;
|
|
15
|
+
expect(atom.type).toBe('tool');
|
|
16
|
+
expect((atom as any).tool_name).toBe('browser.screenshot');
|
|
17
|
+
expect((atom as any).outcome).toBe('success');
|
|
18
|
+
expect(atom.confidence).toBe(0.7);
|
|
19
|
+
expect(atom.fitness_score).toBe(0.8);
|
|
20
|
+
expect((atom as any).error_signature).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('captures a failed tool call', () => {
|
|
24
|
+
const id = captureToolCall(store, 'exec.run', { cmd: 'bad' }, null, 'timeout error', 5000);
|
|
25
|
+
const atom = store.getById(id)!;
|
|
26
|
+
expect((atom as any).outcome).toBe('failure');
|
|
27
|
+
expect(atom.confidence).toBe(0.3);
|
|
28
|
+
expect((atom as any).error_signature).toBe('timeout error');
|
|
29
|
+
expect((atom as any).latency_ms).toBe(5000);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('captures with null latency', () => {
|
|
33
|
+
const id = captureToolCall(store, 'test.tool', {}, 'ok');
|
|
34
|
+
const atom = store.getById(id)!;
|
|
35
|
+
expect((atom as any).latency_ms).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('captureOutcome', () => {
|
|
40
|
+
let store: AtomStore;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => { store = new AtomStore(':memory:'); });
|
|
43
|
+
afterEach(() => { store.close(); });
|
|
44
|
+
|
|
45
|
+
it('scores a perfect outcome', () => {
|
|
46
|
+
const signals: OutcomeSignals = {
|
|
47
|
+
completed_without_error: true,
|
|
48
|
+
revisited_within_1hr: false,
|
|
49
|
+
human_accepted: true,
|
|
50
|
+
convergence_steps: 0,
|
|
51
|
+
error_free: true,
|
|
52
|
+
};
|
|
53
|
+
const id = captureOutcome(store, 'task-1', signals);
|
|
54
|
+
const atom = store.getById(id)!;
|
|
55
|
+
// 0.3 + 0.2 + 0.3 + 0.1 + 0.1 = 1.0
|
|
56
|
+
expect(atom.confidence).toBeCloseTo(1.0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('scores a complete failure', () => {
|
|
60
|
+
const signals: OutcomeSignals = {
|
|
61
|
+
completed_without_error: false,
|
|
62
|
+
revisited_within_1hr: true,
|
|
63
|
+
human_accepted: false,
|
|
64
|
+
convergence_steps: 100,
|
|
65
|
+
error_free: false,
|
|
66
|
+
};
|
|
67
|
+
const id = captureOutcome(store, 'task-2', signals);
|
|
68
|
+
const atom = store.getById(id)!;
|
|
69
|
+
// 0 + 0 + 0 + max(0, 0.1*(1-100/20)) + 0 = 0
|
|
70
|
+
expect(atom.confidence).toBeCloseTo(0.0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('handles null human_accepted as partial credit', () => {
|
|
74
|
+
const signals: OutcomeSignals = {
|
|
75
|
+
completed_without_error: true,
|
|
76
|
+
revisited_within_1hr: false,
|
|
77
|
+
human_accepted: null,
|
|
78
|
+
convergence_steps: 5,
|
|
79
|
+
error_free: true,
|
|
80
|
+
};
|
|
81
|
+
const id = captureOutcome(store, 'task-3', signals);
|
|
82
|
+
const atom = store.getById(id)!;
|
|
83
|
+
// 0.3 + 0.2 + 0.15 + 0.1*(1-5/20) + 0.1 = 0.3+0.2+0.15+0.075+0.1 = 0.825
|
|
84
|
+
expect(atom.confidence).toBeCloseTo(0.825);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('caps convergence contribution at 0', () => {
|
|
88
|
+
const signals: OutcomeSignals = {
|
|
89
|
+
completed_without_error: false,
|
|
90
|
+
revisited_within_1hr: false,
|
|
91
|
+
human_accepted: null,
|
|
92
|
+
convergence_steps: 50,
|
|
93
|
+
error_free: false,
|
|
94
|
+
};
|
|
95
|
+
const id = captureOutcome(store, 'task-4', signals);
|
|
96
|
+
const atom = store.getById(id)!;
|
|
97
|
+
// 0 + 0.2 + 0.15 + max(0, ...) + 0 = 0.35
|
|
98
|
+
expect(atom.confidence).toBeCloseTo(0.35);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('stores outcome atoms as context type', () => {
|
|
102
|
+
const signals: OutcomeSignals = {
|
|
103
|
+
completed_without_error: true,
|
|
104
|
+
revisited_within_1hr: false,
|
|
105
|
+
human_accepted: true,
|
|
106
|
+
convergence_steps: 0,
|
|
107
|
+
error_free: true,
|
|
108
|
+
};
|
|
109
|
+
const id = captureOutcome(store, 'task-5', signals);
|
|
110
|
+
const atom = store.getById(id)!;
|
|
111
|
+
expect(atom.type).toBe('context');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { distillTrace, BatchDistiller } from '../distill/index.js';
|
|
3
|
+
import type { ToolTrace } from '../distill/index.js';
|
|
4
|
+
import type { KnowledgeAtom } from '../types/index.js';
|
|
5
|
+
|
|
6
|
+
function makeTrace(overrides?: Partial<ToolTrace>): ToolTrace {
|
|
7
|
+
return {
|
|
8
|
+
toolName: 'test_tool',
|
|
9
|
+
params: { key: 'value' },
|
|
10
|
+
result: { ok: true },
|
|
11
|
+
error: null,
|
|
12
|
+
latencyMs: 100,
|
|
13
|
+
outcome: 'success',
|
|
14
|
+
agentId: 'agent-1',
|
|
15
|
+
timestamp: new Date().toISOString(),
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('distillTrace', () => {
|
|
21
|
+
it('returns null (stub)', async () => {
|
|
22
|
+
const result = await distillTrace(makeTrace());
|
|
23
|
+
expect(result).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('BatchDistiller', () => {
|
|
28
|
+
it('collects traces up to batch size', () => {
|
|
29
|
+
const bd = new BatchDistiller({ batchSize: 3 });
|
|
30
|
+
expect(bd.pending).toBe(0);
|
|
31
|
+
expect(bd.ready).toBe(false);
|
|
32
|
+
|
|
33
|
+
bd.add(makeTrace());
|
|
34
|
+
bd.add(makeTrace());
|
|
35
|
+
expect(bd.pending).toBe(2);
|
|
36
|
+
expect(bd.ready).toBe(false);
|
|
37
|
+
|
|
38
|
+
const full = bd.add(makeTrace());
|
|
39
|
+
expect(full).toBe(true);
|
|
40
|
+
expect(bd.ready).toBe(true);
|
|
41
|
+
expect(bd.pending).toBe(3);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('flush processes and clears buffer', async () => {
|
|
45
|
+
const bd = new BatchDistiller({ batchSize: 2 });
|
|
46
|
+
bd.add(makeTrace());
|
|
47
|
+
bd.add(makeTrace());
|
|
48
|
+
|
|
49
|
+
// Default stub returns null, so results should be empty
|
|
50
|
+
const atoms = await bd.flush();
|
|
51
|
+
expect(atoms).toHaveLength(0);
|
|
52
|
+
expect(bd.pending).toBe(0);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('flush with custom distillFn returns atoms', async () => {
|
|
56
|
+
const fakeAtom: KnowledgeAtom = {
|
|
57
|
+
id: 'fake-id', type: 'tool', observation: 'test', context: 'test',
|
|
58
|
+
confidence: 0.9, fitness_score: 0.8, trust_tier: 'local',
|
|
59
|
+
source_agent_hash: 'test', created_at: '', updated_at: '', decay_rate: 0.99,
|
|
60
|
+
};
|
|
61
|
+
const bd = new BatchDistiller({
|
|
62
|
+
batchSize: 2,
|
|
63
|
+
distillFn: async () => fakeAtom,
|
|
64
|
+
});
|
|
65
|
+
bd.add(makeTrace());
|
|
66
|
+
bd.add(makeTrace());
|
|
67
|
+
|
|
68
|
+
const atoms = await bd.flush();
|
|
69
|
+
expect(atoms).toHaveLength(2);
|
|
70
|
+
expect(atoms[0]).toEqual(fakeAtom);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('drain returns traces without processing', () => {
|
|
74
|
+
const bd = new BatchDistiller({ batchSize: 5 });
|
|
75
|
+
bd.add(makeTrace({ toolName: 'a' }));
|
|
76
|
+
bd.add(makeTrace({ toolName: 'b' }));
|
|
77
|
+
|
|
78
|
+
const traces = bd.drain();
|
|
79
|
+
expect(traces).toHaveLength(2);
|
|
80
|
+
expect(traces[0]!.toolName).toBe('a');
|
|
81
|
+
expect(bd.pending).toBe(0);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { AtomStore } from '../store/index.js';
|
|
3
|
+
import { computeFitness, recalculateFitness } from '../fitness/index.js';
|
|
4
|
+
|
|
5
|
+
describe('computeFitness', () => {
|
|
6
|
+
it('returns 0 for unused atoms', () => {
|
|
7
|
+
expect(computeFitness(0, 0, 0, null, 10)).toBe(0);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('returns high score for recently used successful atoms', () => {
|
|
11
|
+
const now = new Date();
|
|
12
|
+
const score = computeFitness(10, 9, 1, now.toISOString(), 10, now);
|
|
13
|
+
expect(score).toBeGreaterThan(0.8);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('decays with time', () => {
|
|
17
|
+
const now = new Date();
|
|
18
|
+
const recent = computeFitness(5, 5, 0, now.toISOString(), 10, now);
|
|
19
|
+
const old = computeFitness(5, 5, 0, new Date(now.getTime() - 60 * 86400000).toISOString(), 10, now);
|
|
20
|
+
expect(recent).toBeGreaterThan(old);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('clamps to [0,1]', () => {
|
|
24
|
+
const score = computeFitness(100, 100, 0, new Date().toISOString(), 100);
|
|
25
|
+
expect(score).toBeLessThanOrEqual(1);
|
|
26
|
+
expect(score).toBeGreaterThanOrEqual(0);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('recalculateFitness', () => {
|
|
31
|
+
let store: AtomStore;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => { store = new AtomStore(':memory:'); });
|
|
34
|
+
afterEach(() => { store.close(); });
|
|
35
|
+
|
|
36
|
+
it('updates fitness scores for all atoms', () => {
|
|
37
|
+
const atom = store.createAtom({
|
|
38
|
+
type: 'context',
|
|
39
|
+
observation: 'test recalc atom',
|
|
40
|
+
context: '{}',
|
|
41
|
+
confidence: 0.8,
|
|
42
|
+
fitness_score: 0.9,
|
|
43
|
+
trust_tier: 'local',
|
|
44
|
+
source_agent_hash: 'test',
|
|
45
|
+
decay_rate: 0.99,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
store.recordUsage(atom.id, true);
|
|
49
|
+
store.recordUsage(atom.id, false);
|
|
50
|
+
store.recordUsage(atom.id, false);
|
|
51
|
+
recalculateFitness(store);
|
|
52
|
+
|
|
53
|
+
const updated = store.getById(atom.id)!;
|
|
54
|
+
// 3 uses, 1 success / 2 failures → success_ratio ≈ 0.33, so score < 0.9
|
|
55
|
+
expect(updated.fitness_score).toBeLessThan(0.5);
|
|
56
|
+
expect(updated.fitness_score).toBeGreaterThan(0);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { AtomStore } from '../store/index.js';
|
|
3
|
+
import { searchAtoms, getTopAtoms } from '../retrieval/index.js';
|
|
4
|
+
|
|
5
|
+
describe('retrieval', () => {
|
|
6
|
+
let store: AtomStore;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
store = new AtomStore(':memory:');
|
|
10
|
+
// Seed atoms
|
|
11
|
+
store.createToolAtom({
|
|
12
|
+
type: 'tool', observation: 'Git push to remote origin succeeded',
|
|
13
|
+
context: 'CI pipeline', confidence: 0.9, fitness_score: 0.95,
|
|
14
|
+
trust_tier: 'local', source_agent_hash: 'test', decay_rate: 0.99,
|
|
15
|
+
tool_name: 'git_push', params_hash: 'abc', outcome: 'success',
|
|
16
|
+
error_signature: null, latency_ms: 200, reliability_score: 1.0,
|
|
17
|
+
});
|
|
18
|
+
store.createToolAtom({
|
|
19
|
+
type: 'tool', observation: 'NPM install failed with ERESOLVE peer dependency conflict',
|
|
20
|
+
context: 'package management', confidence: 0.8, fitness_score: 0.3,
|
|
21
|
+
trust_tier: 'local', source_agent_hash: 'test', decay_rate: 0.99,
|
|
22
|
+
tool_name: 'npm_install', params_hash: 'def', outcome: 'failure',
|
|
23
|
+
error_signature: 'ERESOLVE', latency_ms: 5000, reliability_score: 0.0,
|
|
24
|
+
});
|
|
25
|
+
store.createToolAtom({
|
|
26
|
+
type: 'tool', observation: 'Docker build completed with layer caching',
|
|
27
|
+
context: 'containerization', confidence: 0.85, fitness_score: 0.7,
|
|
28
|
+
trust_tier: 'local', source_agent_hash: 'test', decay_rate: 0.99,
|
|
29
|
+
tool_name: 'docker_build', params_hash: 'ghi', outcome: 'success',
|
|
30
|
+
error_signature: null, latency_ms: 30000, reliability_score: 0.9,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('searchAtoms', () => {
|
|
35
|
+
it('finds atoms matching query', () => {
|
|
36
|
+
const results = searchAtoms(store, 'git push');
|
|
37
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
38
|
+
expect(results[0]!.observation).toContain('Git push');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('finds atoms by error text', () => {
|
|
42
|
+
const results = searchAtoms(store, 'ERESOLVE');
|
|
43
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
44
|
+
expect(results[0]!.observation).toContain('ERESOLVE');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('returns empty for no match', () => {
|
|
48
|
+
const results = searchAtoms(store, 'xyznonexistent');
|
|
49
|
+
expect(results).toHaveLength(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('respects limit', () => {
|
|
53
|
+
const results = searchAtoms(store, 'tool', 1);
|
|
54
|
+
expect(results.length).toBeLessThanOrEqual(1);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('getTopAtoms', () => {
|
|
59
|
+
it('returns atoms sorted by fitness_score desc', () => {
|
|
60
|
+
const results = getTopAtoms(store);
|
|
61
|
+
expect(results.length).toBe(3);
|
|
62
|
+
expect(results[0]!.fitness_score).toBeGreaterThanOrEqual(results[1]!.fitness_score);
|
|
63
|
+
expect(results[1]!.fitness_score).toBeGreaterThanOrEqual(results[2]!.fitness_score);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('filters by type', () => {
|
|
67
|
+
// Add a non-tool atom
|
|
68
|
+
store.createAtom({
|
|
69
|
+
type: 'context', observation: 'Some context note',
|
|
70
|
+
context: 'test', confidence: 0.5, fitness_score: 0.5,
|
|
71
|
+
trust_tier: 'local', source_agent_hash: 'test', decay_rate: 0.99,
|
|
72
|
+
});
|
|
73
|
+
const tools = getTopAtoms(store, 'tool');
|
|
74
|
+
expect(tools.every(a => a.type === 'tool')).toBe(true);
|
|
75
|
+
expect(tools.length).toBe(3);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('respects limit', () => {
|
|
79
|
+
const results = getTopAtoms(store, undefined, 2);
|
|
80
|
+
expect(results.length).toBe(2);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { AtomStore, jaccardSimilarity } from '../store/index.js';
|
|
3
|
+
import type { CreateAtom, CreateToolAtom, CreateNegativeAtom } from '../types/index.js';
|
|
4
|
+
|
|
5
|
+
function makeBaseAtom(overrides: Partial<CreateAtom> = {}): CreateAtom {
|
|
6
|
+
return {
|
|
7
|
+
type: 'context',
|
|
8
|
+
observation: 'Test observation',
|
|
9
|
+
context: '{}',
|
|
10
|
+
confidence: 0.8,
|
|
11
|
+
fitness_score: 0.7,
|
|
12
|
+
trust_tier: 'local',
|
|
13
|
+
source_agent_hash: 'test-agent',
|
|
14
|
+
decay_rate: 0.99,
|
|
15
|
+
...overrides,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function makeToolAtom(overrides: Partial<CreateToolAtom> = {}): CreateToolAtom {
|
|
20
|
+
return {
|
|
21
|
+
type: 'tool',
|
|
22
|
+
observation: 'Tool browser.screenshot succeeded',
|
|
23
|
+
context: '{}',
|
|
24
|
+
confidence: 0.8,
|
|
25
|
+
fitness_score: 0.7,
|
|
26
|
+
trust_tier: 'local',
|
|
27
|
+
source_agent_hash: 'test-agent',
|
|
28
|
+
decay_rate: 0.99,
|
|
29
|
+
tool_name: 'browser.screenshot',
|
|
30
|
+
params_hash: 'abc123',
|
|
31
|
+
outcome: 'success',
|
|
32
|
+
error_signature: null,
|
|
33
|
+
latency_ms: 150,
|
|
34
|
+
reliability_score: 0.9,
|
|
35
|
+
...overrides,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeNegativeAtom(overrides: Partial<CreateNegativeAtom> = {}): CreateNegativeAtom {
|
|
40
|
+
return {
|
|
41
|
+
type: 'negative',
|
|
42
|
+
observation: 'Never use rm -rf on root',
|
|
43
|
+
context: '{}',
|
|
44
|
+
confidence: 0.95,
|
|
45
|
+
fitness_score: 0.9,
|
|
46
|
+
trust_tier: 'local',
|
|
47
|
+
source_agent_hash: 'test-agent',
|
|
48
|
+
decay_rate: 0.99,
|
|
49
|
+
anti_pattern: 'rm -rf /',
|
|
50
|
+
failure_cluster_size: 5,
|
|
51
|
+
error_type: 'destructive_command',
|
|
52
|
+
severity: 'critical',
|
|
53
|
+
...overrides,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('AtomStore', () => {
|
|
58
|
+
let store: AtomStore;
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
store = new AtomStore(':memory:');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
store.close();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('CRUD', () => {
|
|
69
|
+
it('creates and retrieves a base atom', () => {
|
|
70
|
+
const atom = store.createAtom(makeBaseAtom());
|
|
71
|
+
expect(atom.id).toBeTruthy();
|
|
72
|
+
expect(atom.type).toBe('context');
|
|
73
|
+
expect(atom.created_at).toBeTruthy();
|
|
74
|
+
|
|
75
|
+
const fetched = store.getById(atom.id);
|
|
76
|
+
expect(fetched).toBeTruthy();
|
|
77
|
+
expect(fetched!.observation).toBe('Test observation');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('creates and retrieves a ToolAtom', () => {
|
|
81
|
+
const atom = store.createToolAtom(makeToolAtom());
|
|
82
|
+
expect(atom.tool_name).toBe('browser.screenshot');
|
|
83
|
+
expect(atom.outcome).toBe('success');
|
|
84
|
+
|
|
85
|
+
const fetched = store.getById(atom.id);
|
|
86
|
+
expect(fetched).toBeTruthy();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('creates and retrieves a NegativeAtom', () => {
|
|
90
|
+
const atom = store.createNegativeAtom(makeNegativeAtom());
|
|
91
|
+
expect(atom.anti_pattern).toBe('rm -rf /');
|
|
92
|
+
expect(atom.severity).toBe('critical');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('deletes an atom', () => {
|
|
96
|
+
const atom = store.createAtom(makeBaseAtom());
|
|
97
|
+
expect(store.deleteAtom(atom.id)).toBe(true);
|
|
98
|
+
expect(store.getById(atom.id)).toBeNull();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('returns false when deleting non-existent', () => {
|
|
102
|
+
expect(store.deleteAtom('non-existent')).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('returns null for non-existent getById', () => {
|
|
106
|
+
expect(store.getById('nope')).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('Queries', () => {
|
|
111
|
+
it('queryByType returns atoms of correct type', () => {
|
|
112
|
+
store.createAtom(makeBaseAtom({ type: 'context', observation: 'ctx 1' }));
|
|
113
|
+
store.createAtom(makeBaseAtom({ type: 'pattern', observation: 'pat 1' }));
|
|
114
|
+
store.createToolAtom(makeToolAtom({ observation: 'tool unique obs 1' }));
|
|
115
|
+
|
|
116
|
+
expect(store.queryByType('context')).toHaveLength(1);
|
|
117
|
+
expect(store.queryByType('tool')).toHaveLength(1);
|
|
118
|
+
expect(store.queryByType('pattern')).toHaveLength(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('queryByToolName filters correctly', () => {
|
|
122
|
+
store.createToolAtom(makeToolAtom({ tool_name: 'browser.screenshot', observation: 'unique obs A' }));
|
|
123
|
+
store.createToolAtom(makeToolAtom({ tool_name: 'exec.run', observation: 'unique obs B' }));
|
|
124
|
+
|
|
125
|
+
expect(store.queryByToolName('browser.screenshot')).toHaveLength(1);
|
|
126
|
+
expect(store.queryByToolName('exec.run')).toHaveLength(1);
|
|
127
|
+
expect(store.queryByToolName('nonexistent')).toHaveLength(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('queryByConfidence returns atoms above threshold', () => {
|
|
131
|
+
store.createAtom(makeBaseAtom({ confidence: 0.3, observation: 'low conf unique' }));
|
|
132
|
+
store.createAtom(makeBaseAtom({ confidence: 0.9, observation: 'high conf unique' }));
|
|
133
|
+
|
|
134
|
+
const high = store.queryByConfidence(0.8);
|
|
135
|
+
expect(high).toHaveLength(1);
|
|
136
|
+
expect(high[0]!.confidence).toBe(0.9);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('search matches observation text', () => {
|
|
140
|
+
store.createAtom(makeBaseAtom({ observation: 'screenshot fails on lazy loaded pages' }));
|
|
141
|
+
store.createAtom(makeBaseAtom({ observation: 'exec works fine always' }));
|
|
142
|
+
|
|
143
|
+
const results = store.search('screenshot');
|
|
144
|
+
expect(results).toHaveLength(1);
|
|
145
|
+
expect(results[0]!.observation).toContain('screenshot');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('getAll returns all atoms', () => {
|
|
149
|
+
store.createAtom(makeBaseAtom({ observation: 'unique atom one' }));
|
|
150
|
+
store.createAtom(makeBaseAtom({ observation: 'unique atom two' }));
|
|
151
|
+
expect(store.getAll()).toHaveLength(2);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('Decay', () => {
|
|
156
|
+
it('applyDecay reduces fitness scores', () => {
|
|
157
|
+
const atom = store.createAtom(makeBaseAtom({ fitness_score: 1.0 }));
|
|
158
|
+
const changed = store.applyDecay();
|
|
159
|
+
expect(changed).toBe(1);
|
|
160
|
+
|
|
161
|
+
const updated = store.getById(atom.id)!;
|
|
162
|
+
expect(updated.fitness_score).toBeCloseTo(0.99);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('applyDecay does not affect zero-fitness atoms', () => {
|
|
166
|
+
store.createAtom(makeBaseAtom({ fitness_score: 0.0, observation: 'zero fitness unique' }));
|
|
167
|
+
const changed = store.applyDecay();
|
|
168
|
+
expect(changed).toBe(0);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('Fitness & Usage', () => {
|
|
173
|
+
it('updateFitnessScore changes score', () => {
|
|
174
|
+
const atom = store.createAtom(makeBaseAtom());
|
|
175
|
+
store.updateFitnessScore(atom.id, 0.5);
|
|
176
|
+
expect(store.getById(atom.id)!.fitness_score).toBe(0.5);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('recordUsage increments counters', () => {
|
|
180
|
+
const atom = store.createAtom(makeBaseAtom());
|
|
181
|
+
store.recordUsage(atom.id, true);
|
|
182
|
+
store.recordUsage(atom.id, true);
|
|
183
|
+
store.recordUsage(atom.id, false);
|
|
184
|
+
|
|
185
|
+
const row = store.getById(atom.id) as any;
|
|
186
|
+
expect(row.use_count).toBe(3);
|
|
187
|
+
expect(row.success_after_use).toBe(2);
|
|
188
|
+
expect(row.failure_after_use).toBe(1);
|
|
189
|
+
expect(row.last_used).toBeTruthy();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('Dedup', () => {
|
|
194
|
+
it('merges atoms with very similar observations', () => {
|
|
195
|
+
const a1 = store.createAtom(makeBaseAtom({ observation: 'browser screenshot fails on lazy loaded pages', fitness_score: 0.5 }));
|
|
196
|
+
const a2 = store.createAtom(makeBaseAtom({ observation: 'browser screenshot fails on lazy loaded pages', fitness_score: 0.8 }));
|
|
197
|
+
|
|
198
|
+
// Should have merged — same ID returned
|
|
199
|
+
expect(a2.id).toBe(a1.id);
|
|
200
|
+
const row = store.getById(a1.id) as any;
|
|
201
|
+
expect(row.evidence_count).toBe(2);
|
|
202
|
+
expect(row.fitness_score).toBe(0.8); // keeps higher
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('does not merge dissimilar observations', () => {
|
|
206
|
+
store.createAtom(makeBaseAtom({ observation: 'browser screenshot fails on lazy loaded pages' }));
|
|
207
|
+
store.createAtom(makeBaseAtom({ observation: 'exec command runs perfectly fine every time' }));
|
|
208
|
+
expect(store.getAll()).toHaveLength(2);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('findSimilar returns matches above threshold', () => {
|
|
212
|
+
store.createAtom(makeBaseAtom({ observation: 'the quick brown fox jumps' }));
|
|
213
|
+
store.createAtom(makeBaseAtom({ observation: 'completely different text about cooking recipes' }));
|
|
214
|
+
|
|
215
|
+
const similar = store.findSimilar('the quick brown fox jumps over', 0.5);
|
|
216
|
+
expect(similar.length).toBeGreaterThanOrEqual(1);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('Validation', () => {
|
|
221
|
+
it('rejects invalid confidence', () => {
|
|
222
|
+
expect(() => store.createAtom(makeBaseAtom({ confidence: 1.5 }))).toThrow();
|
|
223
|
+
expect(() => store.createAtom(makeBaseAtom({ confidence: -0.1 }))).toThrow();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('rejects invalid fitness_score', () => {
|
|
227
|
+
expect(() => store.createAtom(makeBaseAtom({ fitness_score: 2.0 }))).toThrow();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('rejects empty observation', () => {
|
|
231
|
+
expect(() => store.createAtom(makeBaseAtom({ observation: '' }))).toThrow();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('rejects invalid tool atom', () => {
|
|
235
|
+
expect(() => store.createToolAtom({ ...makeToolAtom(), tool_name: '' })).toThrow();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('rejects invalid negative atom', () => {
|
|
239
|
+
expect(() => store.createNegativeAtom({ ...makeNegativeAtom(), severity: 'invalid' as any })).toThrow();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('jaccardSimilarity', () => {
|
|
245
|
+
it('identical strings return 1', () => {
|
|
246
|
+
expect(jaccardSimilarity('hello world', 'hello world')).toBe(1);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('completely different strings return low score', () => {
|
|
250
|
+
expect(jaccardSimilarity('apple banana cherry', 'xray yankee zulu')).toBe(0);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('empty strings return 1', () => {
|
|
254
|
+
expect(jaccardSimilarity('', '')).toBe(1);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('partially overlapping strings return intermediate score', () => {
|
|
258
|
+
const score = jaccardSimilarity('the quick brown fox', 'the quick red fox');
|
|
259
|
+
expect(score).toBeGreaterThan(0.2);
|
|
260
|
+
expect(score).toBeLessThan(1);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { CreateAtomSchema, CreateToolAtomSchema, CreateNegativeAtomSchema, OutcomeSignalsSchema } from '../validation/index.js';
|
|
3
|
+
|
|
4
|
+
describe('CreateAtomSchema', () => {
|
|
5
|
+
const valid = {
|
|
6
|
+
type: 'context', observation: 'test', context: '{}',
|
|
7
|
+
confidence: 0.5, fitness_score: 0.5, trust_tier: 'local',
|
|
8
|
+
source_agent_hash: 'hash', decay_rate: 0.99,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
it('accepts valid data', () => {
|
|
12
|
+
expect(() => CreateAtomSchema.parse(valid)).not.toThrow();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('rejects confidence > 1', () => {
|
|
16
|
+
expect(() => CreateAtomSchema.parse({ ...valid, confidence: 1.1 })).toThrow();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('rejects confidence < 0', () => {
|
|
20
|
+
expect(() => CreateAtomSchema.parse({ ...valid, confidence: -0.1 })).toThrow();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('rejects empty observation', () => {
|
|
24
|
+
expect(() => CreateAtomSchema.parse({ ...valid, observation: '' })).toThrow();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('rejects invalid type', () => {
|
|
28
|
+
expect(() => CreateAtomSchema.parse({ ...valid, type: 'invalid' })).toThrow();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('rejects invalid trust tier', () => {
|
|
32
|
+
expect(() => CreateAtomSchema.parse({ ...valid, trust_tier: 'bogus' })).toThrow();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('CreateToolAtomSchema', () => {
|
|
37
|
+
const valid = {
|
|
38
|
+
type: 'tool', observation: 'test', context: '{}',
|
|
39
|
+
confidence: 0.5, fitness_score: 0.5, trust_tier: 'local',
|
|
40
|
+
source_agent_hash: 'hash', decay_rate: 0.99,
|
|
41
|
+
tool_name: 'browser.click', params_hash: 'abc',
|
|
42
|
+
outcome: 'success', error_signature: null,
|
|
43
|
+
latency_ms: null, reliability_score: 1.0,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
it('accepts valid tool atom', () => {
|
|
47
|
+
expect(() => CreateToolAtomSchema.parse(valid)).not.toThrow();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('rejects missing tool_name', () => {
|
|
51
|
+
const { tool_name, ...rest } = valid;
|
|
52
|
+
expect(() => CreateToolAtomSchema.parse(rest)).toThrow();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('rejects invalid outcome', () => {
|
|
56
|
+
expect(() => CreateToolAtomSchema.parse({ ...valid, outcome: 'maybe' })).toThrow();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('CreateNegativeAtomSchema', () => {
|
|
61
|
+
const valid = {
|
|
62
|
+
type: 'negative', observation: 'test', context: '{}',
|
|
63
|
+
confidence: 0.5, fitness_score: 0.5, trust_tier: 'local',
|
|
64
|
+
source_agent_hash: 'hash', decay_rate: 0.99,
|
|
65
|
+
anti_pattern: 'bad thing', failure_cluster_size: 3,
|
|
66
|
+
error_type: 'crash', severity: 'high',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
it('accepts valid negative atom', () => {
|
|
70
|
+
expect(() => CreateNegativeAtomSchema.parse(valid)).not.toThrow();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('rejects invalid severity', () => {
|
|
74
|
+
expect(() => CreateNegativeAtomSchema.parse({ ...valid, severity: 'extreme' })).toThrow();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('OutcomeSignalsSchema', () => {
|
|
79
|
+
it('accepts valid signals', () => {
|
|
80
|
+
expect(() => OutcomeSignalsSchema.parse({
|
|
81
|
+
completed_without_error: true,
|
|
82
|
+
revisited_within_1hr: false,
|
|
83
|
+
human_accepted: null,
|
|
84
|
+
convergence_steps: 5,
|
|
85
|
+
error_free: true,
|
|
86
|
+
})).not.toThrow();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('rejects missing fields', () => {
|
|
90
|
+
expect(() => OutcomeSignalsSchema.parse({})).toThrow();
|
|
91
|
+
});
|
|
92
|
+
});
|