@soleri/core 9.3.1 → 9.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/intelligence.d.ts +5 -0
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +115 -26
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/learning-radar.d.ts +3 -3
- package/dist/brain/learning-radar.d.ts.map +1 -1
- package/dist/brain/learning-radar.js +8 -4
- package/dist/brain/learning-radar.js.map +1 -1
- package/dist/control/intent-router.d.ts +2 -2
- package/dist/control/intent-router.d.ts.map +1 -1
- package/dist/control/intent-router.js +35 -1
- package/dist/control/intent-router.js.map +1 -1
- package/dist/control/types.d.ts +10 -2
- package/dist/control/types.d.ts.map +1 -1
- package/dist/curator/curator.d.ts +4 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +23 -1
- package/dist/curator/curator.js.map +1 -1
- package/dist/curator/schema.d.ts +1 -1
- package/dist/curator/schema.d.ts.map +1 -1
- package/dist/curator/schema.js +8 -0
- package/dist/curator/schema.js.map +1 -1
- package/dist/domain-packs/types.d.ts +6 -0
- package/dist/domain-packs/types.d.ts.map +1 -1
- package/dist/domain-packs/types.js +1 -0
- package/dist/domain-packs/types.js.map +1 -1
- package/dist/engine/module-manifest.js +3 -3
- package/dist/engine/module-manifest.js.map +1 -1
- package/dist/engine/register-engine.d.ts +9 -0
- package/dist/engine/register-engine.d.ts.map +1 -1
- package/dist/engine/register-engine.js +59 -1
- package/dist/engine/register-engine.js.map +1 -1
- package/dist/facades/types.d.ts +5 -1
- package/dist/facades/types.d.ts.map +1 -1
- package/dist/facades/types.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/operator/operator-context-store.d.ts +54 -0
- package/dist/operator/operator-context-store.d.ts.map +1 -0
- package/dist/operator/operator-context-store.js +434 -0
- package/dist/operator/operator-context-store.js.map +1 -0
- package/dist/operator/operator-context-types.d.ts +101 -0
- package/dist/operator/operator-context-types.d.ts.map +1 -0
- package/dist/operator/operator-context-types.js +27 -0
- package/dist/operator/operator-context-types.js.map +1 -0
- package/dist/packs/index.d.ts +2 -2
- package/dist/packs/index.d.ts.map +1 -1
- package/dist/packs/index.js +1 -1
- package/dist/packs/index.js.map +1 -1
- package/dist/packs/lockfile.d.ts +3 -0
- package/dist/packs/lockfile.d.ts.map +1 -1
- package/dist/packs/lockfile.js.map +1 -1
- package/dist/packs/types.d.ts +8 -2
- package/dist/packs/types.d.ts.map +1 -1
- package/dist/packs/types.js +6 -0
- package/dist/packs/types.js.map +1 -1
- package/dist/planning/plan-lifecycle.d.ts +12 -1
- package/dist/planning/plan-lifecycle.d.ts.map +1 -1
- package/dist/planning/plan-lifecycle.js +52 -19
- package/dist/planning/plan-lifecycle.js.map +1 -1
- package/dist/planning/planner-types.d.ts +6 -0
- package/dist/planning/planner-types.d.ts.map +1 -1
- package/dist/planning/planner.d.ts +21 -1
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +62 -3
- package/dist/planning/planner.js.map +1 -1
- package/dist/planning/task-complexity-assessor.d.ts.map +1 -1
- package/dist/planning/task-complexity-assessor.js.map +1 -1
- package/dist/plugins/types.d.ts +18 -18
- package/dist/runtime/admin-ops.d.ts +1 -1
- package/dist/runtime/admin-ops.d.ts.map +1 -1
- package/dist/runtime/admin-ops.js +100 -3
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
- package/dist/runtime/admin-setup-ops.js +19 -9
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/capture-ops.d.ts.map +1 -1
- package/dist/runtime/capture-ops.js +35 -7
- package/dist/runtime/capture-ops.js.map +1 -1
- package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
- package/dist/runtime/facades/brain-facade.js +4 -2
- package/dist/runtime/facades/brain-facade.js.map +1 -1
- package/dist/runtime/facades/control-facade.d.ts.map +1 -1
- package/dist/runtime/facades/control-facade.js +8 -2
- package/dist/runtime/facades/control-facade.js.map +1 -1
- package/dist/runtime/facades/curator-facade.d.ts.map +1 -1
- package/dist/runtime/facades/curator-facade.js +13 -0
- package/dist/runtime/facades/curator-facade.js.map +1 -1
- package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
- package/dist/runtime/facades/memory-facade.js +10 -12
- package/dist/runtime/facades/memory-facade.js.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.js +36 -1
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
- package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
- package/dist/runtime/facades/plan-facade.js +20 -4
- package/dist/runtime/facades/plan-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +71 -4
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/plan-feedback-helper.d.ts +21 -0
- package/dist/runtime/plan-feedback-helper.d.ts.map +1 -0
- package/dist/runtime/plan-feedback-helper.js +52 -0
- package/dist/runtime/plan-feedback-helper.js.map +1 -0
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +73 -34
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/session-briefing.d.ts.map +1 -1
- package/dist/runtime/session-briefing.js +9 -1
- package/dist/runtime/session-briefing.js.map +1 -1
- package/dist/runtime/types.d.ts +3 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/skills/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +13 -7
- package/dist/skills/sync-skills.js.map +1 -1
- package/package.json +1 -1
- package/src/brain/brain-intelligence.test.ts +30 -0
- package/src/brain/extraction-quality.test.ts +323 -0
- package/src/brain/intelligence.ts +133 -30
- package/src/brain/learning-radar.ts +8 -5
- package/src/brain/second-brain-features.test.ts +1 -1
- package/src/control/intent-router.test.ts +73 -3
- package/src/control/intent-router.ts +38 -1
- package/src/control/types.ts +13 -2
- package/src/curator/curator.test.ts +92 -0
- package/src/curator/curator.ts +29 -1
- package/src/curator/schema.ts +8 -0
- package/src/domain-packs/types.ts +8 -0
- package/src/engine/module-manifest.test.ts +8 -2
- package/src/engine/module-manifest.ts +3 -3
- package/src/engine/register-engine.test.ts +73 -1
- package/src/engine/register-engine.ts +61 -1
- package/src/facades/types.ts +5 -0
- package/src/index.ts +22 -0
- package/src/operator/operator-context-store.test.ts +698 -0
- package/src/operator/operator-context-store.ts +569 -0
- package/src/operator/operator-context-types.ts +139 -0
- package/src/packs/index.ts +3 -1
- package/src/packs/lockfile.ts +3 -0
- package/src/packs/types.ts +9 -0
- package/src/planning/plan-lifecycle.ts +80 -22
- package/src/planning/planner-types.ts +6 -0
- package/src/planning/planner.ts +74 -4
- package/src/planning/task-complexity-assessor.test.ts +6 -2
- package/src/planning/task-complexity-assessor.ts +1 -4
- package/src/runtime/admin-ops.test.ts +139 -6
- package/src/runtime/admin-ops.ts +104 -3
- package/src/runtime/admin-setup-ops.ts +30 -10
- package/src/runtime/capture-ops.test.ts +84 -0
- package/src/runtime/capture-ops.ts +35 -7
- package/src/runtime/facades/admin-facade.test.ts +1 -1
- package/src/runtime/facades/brain-facade.ts +6 -3
- package/src/runtime/facades/control-facade.ts +10 -2
- package/src/runtime/facades/curator-facade.ts +18 -0
- package/src/runtime/facades/memory-facade.test.ts +14 -12
- package/src/runtime/facades/memory-facade.ts +10 -12
- package/src/runtime/facades/orchestrate-facade.ts +33 -1
- package/src/runtime/facades/plan-facade.test.ts +213 -0
- package/src/runtime/facades/plan-facade.ts +23 -4
- package/src/runtime/orchestrate-ops.test.ts +202 -2
- package/src/runtime/orchestrate-ops.ts +85 -4
- package/src/runtime/plan-feedback-helper.test.ts +173 -0
- package/src/runtime/plan-feedback-helper.ts +63 -0
- package/src/runtime/planning-extra-ops.test.ts +43 -1
- package/src/runtime/planning-extra-ops.ts +96 -33
- package/src/runtime/session-briefing.test.ts +1 -0
- package/src/runtime/session-briefing.ts +10 -1
- package/src/runtime/types.ts +3 -0
- package/src/skills/sync-skills.ts +14 -7
- package/vitest.config.ts +1 -0
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { Vault } from '../vault/vault.js';
|
|
3
|
+
import { OperatorContextStore, normalizeCorrection } from './operator-context-store.js';
|
|
4
|
+
import { DECLINED_CATEGORIES } from './operator-context-types.js';
|
|
5
|
+
import type {
|
|
6
|
+
OperatorSignals,
|
|
7
|
+
ExpertiseSignal,
|
|
8
|
+
CorrectionSignal,
|
|
9
|
+
InterestSignal,
|
|
10
|
+
WorkPatternSignal,
|
|
11
|
+
} from './operator-context-types.js';
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// HELPERS
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
function emptySignals(): OperatorSignals {
|
|
18
|
+
return { expertise: [], corrections: [], interests: [], patterns: [] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeExpertise(overrides: Partial<ExpertiseSignal> = {}): ExpertiseSignal {
|
|
22
|
+
return { topic: 'typescript', level: 'learning', ...overrides };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeCorrection(overrides: Partial<CorrectionSignal> = {}): CorrectionSignal {
|
|
26
|
+
return { rule: 'always use semicolons', scope: 'global', ...overrides };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeInterest(overrides: Partial<InterestSignal> = {}): InterestSignal {
|
|
30
|
+
return { tag: 'coffee', ...overrides };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makePattern(overrides: Partial<WorkPatternSignal> = {}): WorkPatternSignal {
|
|
34
|
+
return { pattern: 'batches work locally', ...overrides };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// TESTS
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
describe('OperatorContextStore', () => {
|
|
42
|
+
let vault: Vault;
|
|
43
|
+
let store: OperatorContextStore;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
vault = new Vault(':memory:');
|
|
47
|
+
store = new OperatorContextStore(vault.getProvider());
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
vault.close();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ─── Table Creation ─────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
describe('init', () => {
|
|
57
|
+
it('creates table without error on a fresh database', () => {
|
|
58
|
+
expect(store).toBeDefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('is idempotent — second init does not throw', () => {
|
|
62
|
+
const store2 = new OperatorContextStore(vault.getProvider());
|
|
63
|
+
expect(store2).toBeDefined();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ─── Empty State ──────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
describe('getContext (empty)', () => {
|
|
70
|
+
it('returns empty arrays and zero counts', () => {
|
|
71
|
+
const ctx = store.getContext();
|
|
72
|
+
expect(ctx.expertise).toEqual([]);
|
|
73
|
+
expect(ctx.corrections).toEqual([]);
|
|
74
|
+
expect(ctx.interests).toEqual([]);
|
|
75
|
+
expect(ctx.patterns).toEqual([]);
|
|
76
|
+
expect(ctx.sessionCount).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ─── Expertise Compounding ────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
describe('expertise compounding', () => {
|
|
83
|
+
it('inserts a new expertise item', () => {
|
|
84
|
+
store.compoundSignals({ ...emptySignals(), expertise: [makeExpertise()] }, 'session-1');
|
|
85
|
+
const ctx = store.getContext();
|
|
86
|
+
expect(ctx.expertise).toHaveLength(1);
|
|
87
|
+
expect(ctx.expertise[0].topic).toBe('typescript');
|
|
88
|
+
expect(ctx.expertise[0].level).toBe('learning');
|
|
89
|
+
expect(ctx.expertise[0].sessionCount).toBe(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('compounds confidence with exponential moving average', () => {
|
|
93
|
+
// First: default confidence 0.5
|
|
94
|
+
store.compoundSignals(
|
|
95
|
+
{ ...emptySignals(), expertise: [makeExpertise({ confidence: 0.5 })] },
|
|
96
|
+
'session-1',
|
|
97
|
+
);
|
|
98
|
+
// Second: high confidence 0.9
|
|
99
|
+
store.compoundSignals(
|
|
100
|
+
{ ...emptySignals(), expertise: [makeExpertise({ confidence: 0.9 })] },
|
|
101
|
+
'session-2',
|
|
102
|
+
);
|
|
103
|
+
const ctx = store.getContext();
|
|
104
|
+
// EMA: 0.5 * 0.7 + 0.9 * 0.3 = 0.35 + 0.27 = 0.62
|
|
105
|
+
expect(ctx.expertise[0].confidence).toBeCloseTo(0.62, 2);
|
|
106
|
+
expect(ctx.expertise[0].sessionCount).toBe(2);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('upgrades level when confidence exceeds 0.8', () => {
|
|
110
|
+
// Start at learning with high confidence
|
|
111
|
+
store.compoundSignals(
|
|
112
|
+
{ ...emptySignals(), expertise: [makeExpertise({ level: 'learning', confidence: 0.9 })] },
|
|
113
|
+
's1',
|
|
114
|
+
);
|
|
115
|
+
// Push to intermediate with high confidence — should upgrade
|
|
116
|
+
store.compoundSignals(
|
|
117
|
+
{
|
|
118
|
+
...emptySignals(),
|
|
119
|
+
expertise: [makeExpertise({ level: 'intermediate', confidence: 0.95 })],
|
|
120
|
+
},
|
|
121
|
+
's2',
|
|
122
|
+
);
|
|
123
|
+
// Compound a few more times to push EMA above 0.8
|
|
124
|
+
store.compoundSignals(
|
|
125
|
+
{
|
|
126
|
+
...emptySignals(),
|
|
127
|
+
expertise: [makeExpertise({ level: 'intermediate', confidence: 0.95 })],
|
|
128
|
+
},
|
|
129
|
+
's3',
|
|
130
|
+
);
|
|
131
|
+
store.compoundSignals(
|
|
132
|
+
{
|
|
133
|
+
...emptySignals(),
|
|
134
|
+
expertise: [makeExpertise({ level: 'intermediate', confidence: 0.95 })],
|
|
135
|
+
},
|
|
136
|
+
's4',
|
|
137
|
+
);
|
|
138
|
+
const ctx = store.getContext();
|
|
139
|
+
expect(ctx.expertise[0].level).toBe('intermediate');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('never auto-downgrades level', () => {
|
|
143
|
+
// Start at expert
|
|
144
|
+
store.compoundSignals(
|
|
145
|
+
{ ...emptySignals(), expertise: [makeExpertise({ level: 'expert', confidence: 0.9 })] },
|
|
146
|
+
's1',
|
|
147
|
+
);
|
|
148
|
+
// Signal learning — level should stay expert
|
|
149
|
+
store.compoundSignals(
|
|
150
|
+
{ ...emptySignals(), expertise: [makeExpertise({ level: 'learning', confidence: 0.3 })] },
|
|
151
|
+
's2',
|
|
152
|
+
);
|
|
153
|
+
const ctx = store.getContext();
|
|
154
|
+
expect(ctx.expertise[0].level).toBe('expert');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('is case-insensitive on topic key', () => {
|
|
158
|
+
store.compoundSignals(
|
|
159
|
+
{ ...emptySignals(), expertise: [makeExpertise({ topic: 'TypeScript' })] },
|
|
160
|
+
's1',
|
|
161
|
+
);
|
|
162
|
+
store.compoundSignals(
|
|
163
|
+
{ ...emptySignals(), expertise: [makeExpertise({ topic: 'typescript' })] },
|
|
164
|
+
's2',
|
|
165
|
+
);
|
|
166
|
+
const ctx = store.getContext();
|
|
167
|
+
expect(ctx.expertise).toHaveLength(1);
|
|
168
|
+
expect(ctx.expertise[0].sessionCount).toBe(2);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ─── Corrections ──────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
describe('corrections compounding', () => {
|
|
175
|
+
it('inserts a new correction', () => {
|
|
176
|
+
store.compoundSignals({ ...emptySignals(), corrections: [makeCorrection()] }, 'session-1');
|
|
177
|
+
const ctx = store.getContext();
|
|
178
|
+
expect(ctx.corrections).toHaveLength(1);
|
|
179
|
+
expect(ctx.corrections[0].rule).toBe('always use semicolons');
|
|
180
|
+
expect(ctx.corrections[0].active).toBe(true);
|
|
181
|
+
expect(ctx.corrections[0].scope).toBe('global');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('updates existing correction — latest wins', () => {
|
|
185
|
+
store.compoundSignals(
|
|
186
|
+
{ ...emptySignals(), corrections: [makeCorrection({ quote: 'first' })] },
|
|
187
|
+
's1',
|
|
188
|
+
);
|
|
189
|
+
store.compoundSignals(
|
|
190
|
+
{ ...emptySignals(), corrections: [makeCorrection({ quote: 'updated' })] },
|
|
191
|
+
's2',
|
|
192
|
+
);
|
|
193
|
+
const ctx = store.getContext();
|
|
194
|
+
expect(ctx.corrections).toHaveLength(1);
|
|
195
|
+
expect(ctx.corrections[0].quote).toBe('updated');
|
|
196
|
+
expect(ctx.corrections[0].sessionId).toBe('s2');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('respects scope — project and global', () => {
|
|
200
|
+
store.compoundSignals(
|
|
201
|
+
{
|
|
202
|
+
...emptySignals(),
|
|
203
|
+
corrections: [
|
|
204
|
+
makeCorrection({ rule: 'use tabs', scope: 'project' }),
|
|
205
|
+
makeCorrection({ rule: 'no console.log', scope: 'global' }),
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
's1',
|
|
209
|
+
);
|
|
210
|
+
const ctx = store.getContext();
|
|
211
|
+
expect(ctx.corrections).toHaveLength(2);
|
|
212
|
+
const scopes = ctx.corrections.map((c) => c.scope).sort();
|
|
213
|
+
expect(scopes).toEqual(['global', 'project']);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ─── Interests ────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
describe('interests compounding', () => {
|
|
220
|
+
it('inserts a new interest with default confidence', () => {
|
|
221
|
+
store.compoundSignals({ ...emptySignals(), interests: [makeInterest()] }, 's1');
|
|
222
|
+
const ctx = store.getContext();
|
|
223
|
+
expect(ctx.interests).toHaveLength(1);
|
|
224
|
+
expect(ctx.interests[0].tag).toBe('coffee');
|
|
225
|
+
expect(ctx.interests[0].confidence).toBe(0.5);
|
|
226
|
+
expect(ctx.interests[0].mentionCount).toBe(1);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('grows confidence on repeated mentions', () => {
|
|
230
|
+
for (let i = 0; i < 3; i++) {
|
|
231
|
+
store.compoundSignals({ ...emptySignals(), interests: [makeInterest()] }, `s${i}`);
|
|
232
|
+
}
|
|
233
|
+
const ctx = store.getContext();
|
|
234
|
+
// 0.5 → 0.6 → 0.7
|
|
235
|
+
expect(ctx.interests[0].confidence).toBeCloseTo(0.7, 2);
|
|
236
|
+
expect(ctx.interests[0].mentionCount).toBe(3);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('decays confidence when not mentioned', () => {
|
|
240
|
+
store.compoundSignals({ ...emptySignals(), interests: [makeInterest()] }, 's1');
|
|
241
|
+
// Session without mentioning coffee
|
|
242
|
+
store.compoundSignals(emptySignals(), 's2');
|
|
243
|
+
const ctx = store.getContext();
|
|
244
|
+
expect(ctx.interests[0].confidence).toBeCloseTo(0.49, 2);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('decay floors at 0.1', () => {
|
|
248
|
+
store.compoundSignals({ ...emptySignals(), interests: [makeInterest()] }, 's1');
|
|
249
|
+
// Force confidence very low by many empty sessions
|
|
250
|
+
for (let i = 0; i < 100; i++) {
|
|
251
|
+
store.compoundSignals(emptySignals(), `s${i + 2}`);
|
|
252
|
+
}
|
|
253
|
+
const ctx = store.getContext();
|
|
254
|
+
expect(ctx.interests[0].confidence).toBeGreaterThanOrEqual(0.1);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('confidence caps at 1.0', () => {
|
|
258
|
+
for (let i = 0; i < 20; i++) {
|
|
259
|
+
store.compoundSignals({ ...emptySignals(), interests: [makeInterest()] }, `s${i}`);
|
|
260
|
+
}
|
|
261
|
+
const ctx = store.getContext();
|
|
262
|
+
expect(ctx.interests[0].confidence).toBeLessThanOrEqual(1.0);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// ─── Work Patterns ────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
describe('patterns compounding', () => {
|
|
269
|
+
it('inserts a new pattern', () => {
|
|
270
|
+
store.compoundSignals({ ...emptySignals(), patterns: [makePattern()] }, 's1');
|
|
271
|
+
const ctx = store.getContext();
|
|
272
|
+
expect(ctx.patterns).toHaveLength(1);
|
|
273
|
+
expect(ctx.patterns[0].pattern).toBe('batches work locally');
|
|
274
|
+
expect(ctx.patterns[0].frequency).toBe('once');
|
|
275
|
+
expect(ctx.patterns[0].observedCount).toBe(1);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('upgrades frequency: once → occasional at 3 observations', () => {
|
|
279
|
+
for (let i = 0; i < 3; i++) {
|
|
280
|
+
store.compoundSignals({ ...emptySignals(), patterns: [makePattern()] }, `s${i}`);
|
|
281
|
+
}
|
|
282
|
+
const ctx = store.getContext();
|
|
283
|
+
expect(ctx.patterns[0].frequency).toBe('occasional');
|
|
284
|
+
expect(ctx.patterns[0].observedCount).toBe(3);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('upgrades frequency: occasional → frequent at 8 observations', () => {
|
|
288
|
+
for (let i = 0; i < 8; i++) {
|
|
289
|
+
store.compoundSignals({ ...emptySignals(), patterns: [makePattern()] }, `s${i}`);
|
|
290
|
+
}
|
|
291
|
+
const ctx = store.getContext();
|
|
292
|
+
expect(ctx.patterns[0].frequency).toBe('frequent');
|
|
293
|
+
expect(ctx.patterns[0].observedCount).toBe(8);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('compounds confidence with EMA (0.8/0.2 weights)', () => {
|
|
297
|
+
store.compoundSignals({ ...emptySignals(), patterns: [makePattern()] }, 's1');
|
|
298
|
+
store.compoundSignals({ ...emptySignals(), patterns: [makePattern()] }, 's2');
|
|
299
|
+
const ctx = store.getContext();
|
|
300
|
+
// EMA: 0.5 * 0.8 + 0.5 * 0.2 = 0.5 (stays same since signal is same)
|
|
301
|
+
expect(ctx.patterns[0].confidence).toBeCloseTo(0.5, 2);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// ─── Drift Detection ─────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
describe('hasDrifted', () => {
|
|
308
|
+
it('returns true on first call (no previous render)', () => {
|
|
309
|
+
expect(store.hasDrifted()).toBe(true);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('returns false when nothing changed', () => {
|
|
313
|
+
store.hasDrifted(); // prime
|
|
314
|
+
expect(store.hasDrifted()).toBe(false);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('returns true after new correction added', () => {
|
|
318
|
+
store.hasDrifted(); // prime
|
|
319
|
+
store.compoundSignals({ ...emptySignals(), corrections: [makeCorrection()] }, 's1');
|
|
320
|
+
expect(store.hasDrifted()).toBe(true);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('returns true after expertise level change', () => {
|
|
324
|
+
store.compoundSignals(
|
|
325
|
+
{ ...emptySignals(), expertise: [makeExpertise({ confidence: 0.9 })] },
|
|
326
|
+
's1',
|
|
327
|
+
);
|
|
328
|
+
store.hasDrifted(); // prime
|
|
329
|
+
store.compoundSignals(
|
|
330
|
+
{ ...emptySignals(), expertise: [makeExpertise({ confidence: 0.95 })] },
|
|
331
|
+
's2',
|
|
332
|
+
);
|
|
333
|
+
expect(store.hasDrifted()).toBe(true);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ─── Renderer ─────────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
describe('renderContextFile', () => {
|
|
340
|
+
it('returns header only when empty', () => {
|
|
341
|
+
const output = store.renderContextFile();
|
|
342
|
+
expect(output).toBe('# Operator Context');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('renders expertise as facts', () => {
|
|
346
|
+
store.compoundSignals({ ...emptySignals(), expertise: [makeExpertise()] }, 's1');
|
|
347
|
+
const output = store.renderContextFile();
|
|
348
|
+
expect(output).toContain('**Expertise:**');
|
|
349
|
+
expect(output).toContain('typescript');
|
|
350
|
+
expect(output).toContain('learning');
|
|
351
|
+
expect(output).toContain('1 sessions');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('renders corrections as bullet list', () => {
|
|
355
|
+
store.compoundSignals(
|
|
356
|
+
{ ...emptySignals(), corrections: [makeCorrection({ quote: 'do it!' })] },
|
|
357
|
+
's1',
|
|
358
|
+
);
|
|
359
|
+
const output = store.renderContextFile();
|
|
360
|
+
expect(output).toContain('**Corrections:**');
|
|
361
|
+
expect(output).toContain('- always use semicolons');
|
|
362
|
+
expect(output).toContain('"do it!"');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('renders interests above confidence threshold', () => {
|
|
366
|
+
store.compoundSignals({ ...emptySignals(), interests: [makeInterest()] }, 's1');
|
|
367
|
+
const output = store.renderContextFile();
|
|
368
|
+
expect(output).toContain('**Interests:**');
|
|
369
|
+
expect(output).toContain('coffee');
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('renders work patterns', () => {
|
|
373
|
+
store.compoundSignals({ ...emptySignals(), patterns: [makePattern()] }, 's1');
|
|
374
|
+
const output = store.renderContextFile();
|
|
375
|
+
expect(output).toContain('**Work patterns:**');
|
|
376
|
+
expect(output).toContain('batches work locally');
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('produces valid markdown', () => {
|
|
380
|
+
store.compoundSignals(
|
|
381
|
+
{
|
|
382
|
+
expertise: [makeExpertise()],
|
|
383
|
+
corrections: [makeCorrection()],
|
|
384
|
+
interests: [makeInterest()],
|
|
385
|
+
patterns: [makePattern()],
|
|
386
|
+
},
|
|
387
|
+
's1',
|
|
388
|
+
);
|
|
389
|
+
const output = store.renderContextFile();
|
|
390
|
+
// Starts with markdown heading
|
|
391
|
+
expect(output.startsWith('# Operator Context')).toBe(true);
|
|
392
|
+
// No trailing whitespace on lines
|
|
393
|
+
for (const line of output.split('\n')) {
|
|
394
|
+
expect(line).toBe(line.trimEnd());
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// ─── Inspect ──────────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
describe('inspect', () => {
|
|
402
|
+
it('returns the full compounded profile', () => {
|
|
403
|
+
store.compoundSignals(
|
|
404
|
+
{
|
|
405
|
+
expertise: [makeExpertise()],
|
|
406
|
+
corrections: [makeCorrection()],
|
|
407
|
+
interests: [makeInterest()],
|
|
408
|
+
patterns: [makePattern()],
|
|
409
|
+
},
|
|
410
|
+
's1',
|
|
411
|
+
);
|
|
412
|
+
const ctx = store.inspect();
|
|
413
|
+
expect(ctx.expertise).toHaveLength(1);
|
|
414
|
+
expect(ctx.corrections).toHaveLength(1);
|
|
415
|
+
expect(ctx.interests).toHaveLength(1);
|
|
416
|
+
expect(ctx.patterns).toHaveLength(1);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// ─── Delete ───────────────────────────────────────────────────────
|
|
421
|
+
|
|
422
|
+
describe('deleteItem', () => {
|
|
423
|
+
it('removes a specific item', () => {
|
|
424
|
+
store.compoundSignals({ ...emptySignals(), corrections: [makeCorrection()] }, 's1');
|
|
425
|
+
const ctx = store.getContext();
|
|
426
|
+
const id = ctx.corrections[0].id;
|
|
427
|
+
const deleted = store.deleteItem('correction', id);
|
|
428
|
+
expect(deleted).toBe(true);
|
|
429
|
+
expect(store.getContext().corrections).toHaveLength(0);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('returns false for non-existent item', () => {
|
|
433
|
+
const deleted = store.deleteItem('expertise', 'nonexistent-id');
|
|
434
|
+
expect(deleted).toBe(false);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// ─── Must-Not-Learn Categories ────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
describe('declined categories', () => {
|
|
441
|
+
it('rejects signals containing declined category words', () => {
|
|
442
|
+
for (const category of DECLINED_CATEGORIES) {
|
|
443
|
+
const freshVault = new Vault(':memory:');
|
|
444
|
+
const freshStore = new OperatorContextStore(freshVault.getProvider());
|
|
445
|
+
|
|
446
|
+
freshStore.compoundSignals(
|
|
447
|
+
{
|
|
448
|
+
expertise: [makeExpertise({ topic: `${category} stuff` })],
|
|
449
|
+
corrections: [makeCorrection({ rule: `avoid ${category} topics` })],
|
|
450
|
+
interests: [makeInterest({ tag: `${category} news` })],
|
|
451
|
+
patterns: [makePattern({ pattern: `reads ${category} articles` })],
|
|
452
|
+
},
|
|
453
|
+
's1',
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const ctx = freshStore.getContext();
|
|
457
|
+
expect(ctx.expertise).toHaveLength(0);
|
|
458
|
+
expect(ctx.corrections).toHaveLength(0);
|
|
459
|
+
expect(ctx.interests).toHaveLength(0);
|
|
460
|
+
expect(ctx.patterns).toHaveLength(0);
|
|
461
|
+
|
|
462
|
+
freshVault.close();
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('rejects signals where evidence contains declined words', () => {
|
|
467
|
+
store.compoundSignals(
|
|
468
|
+
{
|
|
469
|
+
...emptySignals(),
|
|
470
|
+
expertise: [
|
|
471
|
+
makeExpertise({ topic: 'coding', evidence: 'mentioned their medical condition' }),
|
|
472
|
+
],
|
|
473
|
+
},
|
|
474
|
+
's1',
|
|
475
|
+
);
|
|
476
|
+
expect(store.getContext().expertise).toHaveLength(0);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('allows signals without declined category words', () => {
|
|
480
|
+
store.compoundSignals(
|
|
481
|
+
{
|
|
482
|
+
expertise: [makeExpertise({ topic: 'rust' })],
|
|
483
|
+
corrections: [makeCorrection({ rule: 'prefer const' })],
|
|
484
|
+
interests: [makeInterest({ tag: 'hiking' })],
|
|
485
|
+
patterns: [makePattern({ pattern: 'works in morning' })],
|
|
486
|
+
},
|
|
487
|
+
's1',
|
|
488
|
+
);
|
|
489
|
+
const ctx = store.getContext();
|
|
490
|
+
expect(ctx.expertise).toHaveLength(1);
|
|
491
|
+
expect(ctx.corrections).toHaveLength(1);
|
|
492
|
+
expect(ctx.interests).toHaveLength(1);
|
|
493
|
+
expect(ctx.patterns).toHaveLength(1);
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// ─── Empty Signals ────────────────────────────────────────────────
|
|
498
|
+
|
|
499
|
+
describe('empty signals', () => {
|
|
500
|
+
it('does not crash on empty signal arrays', () => {
|
|
501
|
+
expect(() => store.compoundSignals(emptySignals(), 's1')).not.toThrow();
|
|
502
|
+
expect(store.getContext().expertise).toEqual([]);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('handles multiple empty compounds in sequence', () => {
|
|
506
|
+
for (let i = 0; i < 5; i++) {
|
|
507
|
+
store.compoundSignals(emptySignals(), `s${i}`);
|
|
508
|
+
}
|
|
509
|
+
const ctx = store.getContext();
|
|
510
|
+
expect(ctx.expertise).toEqual([]);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// ─── Mixed Signals ────────────────────────────────────────────────
|
|
515
|
+
|
|
516
|
+
describe('mixed signal batch', () => {
|
|
517
|
+
it('processes all four signal types in one call', () => {
|
|
518
|
+
store.compoundSignals(
|
|
519
|
+
{
|
|
520
|
+
expertise: [makeExpertise({ topic: 'react' }), makeExpertise({ topic: 'node' })],
|
|
521
|
+
corrections: [makeCorrection()],
|
|
522
|
+
interests: [makeInterest(), makeInterest({ tag: 'climbing' })],
|
|
523
|
+
patterns: [makePattern()],
|
|
524
|
+
},
|
|
525
|
+
's1',
|
|
526
|
+
);
|
|
527
|
+
const ctx = store.getContext();
|
|
528
|
+
expect(ctx.expertise).toHaveLength(2);
|
|
529
|
+
expect(ctx.corrections).toHaveLength(1);
|
|
530
|
+
expect(ctx.interests).toHaveLength(2);
|
|
531
|
+
expect(ctx.patterns).toHaveLength(1);
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// ─── Correction Undo Detection ──────────────────────────────────
|
|
536
|
+
|
|
537
|
+
describe('correction undo detection', () => {
|
|
538
|
+
it('deactivates correction when undo detected', () => {
|
|
539
|
+
// First: "don't summarize"
|
|
540
|
+
store.compoundSignals(
|
|
541
|
+
{ ...emptySignals(), corrections: [makeCorrection({ rule: "don't summarize" })] },
|
|
542
|
+
's1',
|
|
543
|
+
);
|
|
544
|
+
expect(store.getContext().corrections).toHaveLength(1);
|
|
545
|
+
expect(store.getContext().corrections[0].active).toBe(true);
|
|
546
|
+
|
|
547
|
+
// Then: "actually, summaries are fine"
|
|
548
|
+
store.compoundSignals(
|
|
549
|
+
{
|
|
550
|
+
...emptySignals(),
|
|
551
|
+
corrections: [makeCorrection({ rule: 'actually, summaries are fine' })],
|
|
552
|
+
},
|
|
553
|
+
's2',
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
// The original correction should be deactivated, and the undo should NOT be stored
|
|
557
|
+
const ctx = store.getContext();
|
|
558
|
+
expect(ctx.corrections).toHaveLength(0);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('undo detection handles various phrasings', () => {
|
|
562
|
+
const pairs: [string, string][] = [
|
|
563
|
+
['no emoji', 'you can use emoji again'],
|
|
564
|
+
['stop using bullet points', 'actually, bullet points are fine'],
|
|
565
|
+
["don't use abbreviations", 'feel free to use abbreviations'],
|
|
566
|
+
['avoid long explanations', 'go ahead with long explanations'],
|
|
567
|
+
['never use slang', "it's fine to use slang"],
|
|
568
|
+
];
|
|
569
|
+
|
|
570
|
+
for (const [original, undo] of pairs) {
|
|
571
|
+
const freshVault = new Vault(':memory:');
|
|
572
|
+
const freshStore = new OperatorContextStore(freshVault.getProvider());
|
|
573
|
+
|
|
574
|
+
freshStore.compoundSignals(
|
|
575
|
+
{ ...emptySignals(), corrections: [makeCorrection({ rule: original })] },
|
|
576
|
+
's1',
|
|
577
|
+
);
|
|
578
|
+
expect(freshStore.getContext().corrections).toHaveLength(1);
|
|
579
|
+
|
|
580
|
+
freshStore.compoundSignals(
|
|
581
|
+
{ ...emptySignals(), corrections: [makeCorrection({ rule: undo })] },
|
|
582
|
+
's2',
|
|
583
|
+
);
|
|
584
|
+
expect(freshStore.getContext().corrections).toHaveLength(0);
|
|
585
|
+
|
|
586
|
+
freshVault.close();
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it('undo only deactivates matching topic — unrelated corrections unaffected', () => {
|
|
591
|
+
store.compoundSignals(
|
|
592
|
+
{
|
|
593
|
+
...emptySignals(),
|
|
594
|
+
corrections: [
|
|
595
|
+
makeCorrection({ rule: "don't summarize" }),
|
|
596
|
+
makeCorrection({ rule: 'always use semicolons' }),
|
|
597
|
+
],
|
|
598
|
+
},
|
|
599
|
+
's1',
|
|
600
|
+
);
|
|
601
|
+
expect(store.getContext().corrections).toHaveLength(2);
|
|
602
|
+
|
|
603
|
+
// Undo only the summarize correction
|
|
604
|
+
store.compoundSignals(
|
|
605
|
+
{
|
|
606
|
+
...emptySignals(),
|
|
607
|
+
corrections: [makeCorrection({ rule: 'actually, summaries are fine' })],
|
|
608
|
+
},
|
|
609
|
+
's2',
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
const ctx = store.getContext();
|
|
613
|
+
expect(ctx.corrections).toHaveLength(1);
|
|
614
|
+
expect(ctx.corrections[0].rule).toBe('always use semicolons');
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it('deactivated corrections do not appear in rendered file', () => {
|
|
618
|
+
store.compoundSignals(
|
|
619
|
+
{
|
|
620
|
+
...emptySignals(),
|
|
621
|
+
corrections: [
|
|
622
|
+
makeCorrection({ rule: "don't summarize" }),
|
|
623
|
+
makeCorrection({ rule: 'no emoji' }),
|
|
624
|
+
],
|
|
625
|
+
},
|
|
626
|
+
's1',
|
|
627
|
+
);
|
|
628
|
+
const beforeRender = store.renderContextFile();
|
|
629
|
+
expect(beforeRender).toContain("don't summarize");
|
|
630
|
+
expect(beforeRender).toContain('no emoji');
|
|
631
|
+
|
|
632
|
+
// Undo "don't summarize"
|
|
633
|
+
store.compoundSignals(
|
|
634
|
+
{
|
|
635
|
+
...emptySignals(),
|
|
636
|
+
corrections: [makeCorrection({ rule: 'actually, summaries are fine' })],
|
|
637
|
+
},
|
|
638
|
+
's2',
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
const afterRender = store.renderContextFile();
|
|
642
|
+
expect(afterRender).not.toContain("don't summarize");
|
|
643
|
+
expect(afterRender).toContain('no emoji');
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// ─── normalizeCorrection unit tests ─────────────────────────────
|
|
648
|
+
|
|
649
|
+
describe('normalizeCorrection', () => {
|
|
650
|
+
it('extracts topic and direction correctly for dont patterns', () => {
|
|
651
|
+
expect(normalizeCorrection("don't summarize")).toEqual({
|
|
652
|
+
topic: 'summarize',
|
|
653
|
+
direction: 'dont',
|
|
654
|
+
});
|
|
655
|
+
expect(normalizeCorrection('stop using emoji')).toEqual({
|
|
656
|
+
topic: 'using emoji',
|
|
657
|
+
direction: 'dont',
|
|
658
|
+
});
|
|
659
|
+
expect(normalizeCorrection('never use slang')).toEqual({
|
|
660
|
+
topic: 'use slang',
|
|
661
|
+
direction: 'dont',
|
|
662
|
+
});
|
|
663
|
+
expect(normalizeCorrection('no abbreviations')).toEqual({
|
|
664
|
+
topic: 'abbreviations',
|
|
665
|
+
direction: 'dont',
|
|
666
|
+
});
|
|
667
|
+
expect(normalizeCorrection('avoid long answers')).toEqual({
|
|
668
|
+
topic: 'long answers',
|
|
669
|
+
direction: 'dont',
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it('extracts topic and direction correctly for do patterns', () => {
|
|
674
|
+
expect(normalizeCorrection('actually, summaries are fine')).toEqual({
|
|
675
|
+
topic: 'summaries are fine',
|
|
676
|
+
direction: 'do',
|
|
677
|
+
});
|
|
678
|
+
expect(normalizeCorrection('you can use emoji again')).toEqual({
|
|
679
|
+
topic: 'use emoji again',
|
|
680
|
+
direction: 'do',
|
|
681
|
+
});
|
|
682
|
+
expect(normalizeCorrection('feel free to abbreviate')).toEqual({
|
|
683
|
+
topic: 'to abbreviate',
|
|
684
|
+
direction: 'do',
|
|
685
|
+
});
|
|
686
|
+
expect(normalizeCorrection('go ahead with bullet points')).toEqual({
|
|
687
|
+
topic: 'with bullet points',
|
|
688
|
+
direction: 'do',
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it('defaults to dont direction for unrecognized prefixes', () => {
|
|
693
|
+
const result = normalizeCorrection('use semicolons');
|
|
694
|
+
expect(result.direction).toBe('dont');
|
|
695
|
+
expect(result.topic).toBe('use semicolons');
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
});
|