@soleri/core 9.3.0 → 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.
Files changed (177) hide show
  1. package/dist/brain/intelligence.d.ts +5 -0
  2. package/dist/brain/intelligence.d.ts.map +1 -1
  3. package/dist/brain/intelligence.js +115 -26
  4. package/dist/brain/intelligence.js.map +1 -1
  5. package/dist/brain/learning-radar.d.ts +3 -3
  6. package/dist/brain/learning-radar.d.ts.map +1 -1
  7. package/dist/brain/learning-radar.js +8 -4
  8. package/dist/brain/learning-radar.js.map +1 -1
  9. package/dist/control/intent-router.d.ts +2 -2
  10. package/dist/control/intent-router.d.ts.map +1 -1
  11. package/dist/control/intent-router.js +35 -1
  12. package/dist/control/intent-router.js.map +1 -1
  13. package/dist/control/types.d.ts +10 -2
  14. package/dist/control/types.d.ts.map +1 -1
  15. package/dist/curator/curator.d.ts +4 -0
  16. package/dist/curator/curator.d.ts.map +1 -1
  17. package/dist/curator/curator.js +23 -1
  18. package/dist/curator/curator.js.map +1 -1
  19. package/dist/curator/schema.d.ts +1 -1
  20. package/dist/curator/schema.d.ts.map +1 -1
  21. package/dist/curator/schema.js +8 -0
  22. package/dist/curator/schema.js.map +1 -1
  23. package/dist/domain-packs/types.d.ts +6 -0
  24. package/dist/domain-packs/types.d.ts.map +1 -1
  25. package/dist/domain-packs/types.js +1 -0
  26. package/dist/domain-packs/types.js.map +1 -1
  27. package/dist/engine/module-manifest.d.ts +2 -0
  28. package/dist/engine/module-manifest.d.ts.map +1 -1
  29. package/dist/engine/module-manifest.js +117 -2
  30. package/dist/engine/module-manifest.js.map +1 -1
  31. package/dist/engine/register-engine.d.ts +9 -0
  32. package/dist/engine/register-engine.d.ts.map +1 -1
  33. package/dist/engine/register-engine.js +59 -1
  34. package/dist/engine/register-engine.js.map +1 -1
  35. package/dist/facades/types.d.ts +5 -1
  36. package/dist/facades/types.d.ts.map +1 -1
  37. package/dist/facades/types.js.map +1 -1
  38. package/dist/index.d.ts +6 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +5 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/operator/operator-context-store.d.ts +54 -0
  43. package/dist/operator/operator-context-store.d.ts.map +1 -0
  44. package/dist/operator/operator-context-store.js +434 -0
  45. package/dist/operator/operator-context-store.js.map +1 -0
  46. package/dist/operator/operator-context-types.d.ts +101 -0
  47. package/dist/operator/operator-context-types.d.ts.map +1 -0
  48. package/dist/operator/operator-context-types.js +27 -0
  49. package/dist/operator/operator-context-types.js.map +1 -0
  50. package/dist/packs/index.d.ts +2 -2
  51. package/dist/packs/index.d.ts.map +1 -1
  52. package/dist/packs/index.js +1 -1
  53. package/dist/packs/index.js.map +1 -1
  54. package/dist/packs/lockfile.d.ts +3 -0
  55. package/dist/packs/lockfile.d.ts.map +1 -1
  56. package/dist/packs/lockfile.js.map +1 -1
  57. package/dist/packs/types.d.ts +8 -2
  58. package/dist/packs/types.d.ts.map +1 -1
  59. package/dist/packs/types.js +6 -0
  60. package/dist/packs/types.js.map +1 -1
  61. package/dist/planning/plan-lifecycle.d.ts +12 -1
  62. package/dist/planning/plan-lifecycle.d.ts.map +1 -1
  63. package/dist/planning/plan-lifecycle.js +52 -19
  64. package/dist/planning/plan-lifecycle.js.map +1 -1
  65. package/dist/planning/planner-types.d.ts +6 -0
  66. package/dist/planning/planner-types.d.ts.map +1 -1
  67. package/dist/planning/planner.d.ts +21 -1
  68. package/dist/planning/planner.d.ts.map +1 -1
  69. package/dist/planning/planner.js +62 -3
  70. package/dist/planning/planner.js.map +1 -1
  71. package/dist/planning/task-complexity-assessor.d.ts +42 -0
  72. package/dist/planning/task-complexity-assessor.d.ts.map +1 -0
  73. package/dist/planning/task-complexity-assessor.js +132 -0
  74. package/dist/planning/task-complexity-assessor.js.map +1 -0
  75. package/dist/plugins/types.d.ts +18 -18
  76. package/dist/runtime/admin-ops.d.ts +1 -1
  77. package/dist/runtime/admin-ops.d.ts.map +1 -1
  78. package/dist/runtime/admin-ops.js +118 -3
  79. package/dist/runtime/admin-ops.js.map +1 -1
  80. package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
  81. package/dist/runtime/admin-setup-ops.js +19 -9
  82. package/dist/runtime/admin-setup-ops.js.map +1 -1
  83. package/dist/runtime/capture-ops.d.ts.map +1 -1
  84. package/dist/runtime/capture-ops.js +35 -7
  85. package/dist/runtime/capture-ops.js.map +1 -1
  86. package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
  87. package/dist/runtime/facades/brain-facade.js +4 -2
  88. package/dist/runtime/facades/brain-facade.js.map +1 -1
  89. package/dist/runtime/facades/control-facade.d.ts.map +1 -1
  90. package/dist/runtime/facades/control-facade.js +8 -2
  91. package/dist/runtime/facades/control-facade.js.map +1 -1
  92. package/dist/runtime/facades/curator-facade.d.ts.map +1 -1
  93. package/dist/runtime/facades/curator-facade.js +13 -0
  94. package/dist/runtime/facades/curator-facade.js.map +1 -1
  95. package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
  96. package/dist/runtime/facades/memory-facade.js +10 -12
  97. package/dist/runtime/facades/memory-facade.js.map +1 -1
  98. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
  99. package/dist/runtime/facades/orchestrate-facade.js +36 -1
  100. package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
  101. package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
  102. package/dist/runtime/facades/plan-facade.js +20 -4
  103. package/dist/runtime/facades/plan-facade.js.map +1 -1
  104. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  105. package/dist/runtime/orchestrate-ops.js +109 -31
  106. package/dist/runtime/orchestrate-ops.js.map +1 -1
  107. package/dist/runtime/plan-feedback-helper.d.ts +21 -0
  108. package/dist/runtime/plan-feedback-helper.d.ts.map +1 -0
  109. package/dist/runtime/plan-feedback-helper.js +52 -0
  110. package/dist/runtime/plan-feedback-helper.js.map +1 -0
  111. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  112. package/dist/runtime/planning-extra-ops.js +73 -34
  113. package/dist/runtime/planning-extra-ops.js.map +1 -1
  114. package/dist/runtime/session-briefing.d.ts.map +1 -1
  115. package/dist/runtime/session-briefing.js +9 -1
  116. package/dist/runtime/session-briefing.js.map +1 -1
  117. package/dist/runtime/types.d.ts +3 -0
  118. package/dist/runtime/types.d.ts.map +1 -1
  119. package/dist/skills/sync-skills.d.ts.map +1 -1
  120. package/dist/skills/sync-skills.js +13 -7
  121. package/dist/skills/sync-skills.js.map +1 -1
  122. package/package.json +1 -1
  123. package/src/brain/brain-intelligence.test.ts +30 -0
  124. package/src/brain/extraction-quality.test.ts +323 -0
  125. package/src/brain/intelligence.ts +133 -30
  126. package/src/brain/learning-radar.ts +8 -5
  127. package/src/brain/second-brain-features.test.ts +1 -1
  128. package/src/control/intent-router.test.ts +73 -3
  129. package/src/control/intent-router.ts +38 -1
  130. package/src/control/types.ts +13 -2
  131. package/src/curator/curator.test.ts +92 -0
  132. package/src/curator/curator.ts +29 -1
  133. package/src/curator/schema.ts +8 -0
  134. package/src/domain-packs/types.ts +8 -0
  135. package/src/engine/module-manifest.test.ts +51 -2
  136. package/src/engine/module-manifest.ts +119 -2
  137. package/src/engine/register-engine.test.ts +73 -1
  138. package/src/engine/register-engine.ts +61 -1
  139. package/src/facades/types.ts +5 -0
  140. package/src/index.ts +30 -0
  141. package/src/operator/operator-context-store.test.ts +698 -0
  142. package/src/operator/operator-context-store.ts +569 -0
  143. package/src/operator/operator-context-types.ts +139 -0
  144. package/src/packs/index.ts +3 -1
  145. package/src/packs/lockfile.ts +3 -0
  146. package/src/packs/types.ts +9 -0
  147. package/src/planning/plan-lifecycle.ts +80 -22
  148. package/src/planning/planner-types.ts +6 -0
  149. package/src/planning/planner.ts +74 -4
  150. package/src/planning/task-complexity-assessor.test.ts +302 -0
  151. package/src/planning/task-complexity-assessor.ts +180 -0
  152. package/src/runtime/admin-ops.test.ts +159 -3
  153. package/src/runtime/admin-ops.ts +123 -3
  154. package/src/runtime/admin-setup-ops.ts +30 -10
  155. package/src/runtime/capture-ops.test.ts +84 -0
  156. package/src/runtime/capture-ops.ts +35 -7
  157. package/src/runtime/facades/admin-facade.test.ts +1 -1
  158. package/src/runtime/facades/brain-facade.ts +6 -3
  159. package/src/runtime/facades/control-facade.ts +10 -2
  160. package/src/runtime/facades/curator-facade.ts +18 -0
  161. package/src/runtime/facades/memory-facade.test.ts +14 -12
  162. package/src/runtime/facades/memory-facade.ts +10 -12
  163. package/src/runtime/facades/orchestrate-facade.ts +33 -1
  164. package/src/runtime/facades/plan-facade.test.ts +213 -0
  165. package/src/runtime/facades/plan-facade.ts +23 -4
  166. package/src/runtime/orchestrate-ops.test.ts +404 -0
  167. package/src/runtime/orchestrate-ops.ts +129 -37
  168. package/src/runtime/plan-feedback-helper.test.ts +173 -0
  169. package/src/runtime/plan-feedback-helper.ts +63 -0
  170. package/src/runtime/planning-extra-ops.test.ts +43 -1
  171. package/src/runtime/planning-extra-ops.ts +96 -33
  172. package/src/runtime/session-briefing.test.ts +1 -0
  173. package/src/runtime/session-briefing.ts +10 -1
  174. package/src/runtime/types.ts +3 -0
  175. package/src/skills/sync-skills.ts +14 -7
  176. package/src/vault/vault-scaling.test.ts +5 -5
  177. package/vitest.config.ts +1 -0
@@ -0,0 +1,569 @@
1
+ /**
2
+ * OperatorContextStore — persistence and compounding for the operator context
3
+ * signal taxonomy.
4
+ *
5
+ * Manages a single SQLite table (`operator_context`) and implements the
6
+ * compounding algorithms that blend per-session signals into a stable profile.
7
+ */
8
+
9
+ import { randomUUID } from 'node:crypto';
10
+ import type { PersistenceProvider } from '../persistence/types.js';
11
+ import {
12
+ DECLINED_CATEGORIES,
13
+ type ContextItemType,
14
+ type CorrectionItem,
15
+ type CorrectionSignal,
16
+ type ExpertiseItem,
17
+ type ExpertiseLevel,
18
+ type ExpertiseSignal,
19
+ type InterestItem,
20
+ type InterestSignal,
21
+ type OperatorContext,
22
+ type OperatorSignals,
23
+ type WorkPatternItem,
24
+ type WorkPatternSignal,
25
+ } from './operator-context-types.js';
26
+
27
+ // =============================================================================
28
+ // CONSTANTS
29
+ // =============================================================================
30
+
31
+ const DEFAULT_CONFIDENCE = 0.5;
32
+
33
+ /** Patterns that indicate the user is prohibiting / stopping something. */
34
+ const DONT_PATTERNS = /^(don't|do not|stop|never|no |avoid |quit )/i;
35
+
36
+ /** Patterns that indicate the user is reversing a previous prohibition. */
37
+ const DO_PATTERNS = /^(actually,? ?|you can |feel free |go ahead |it's fine |is fine |start )/i;
38
+
39
+ /** Regex to detect declined-category content in signal text fields. */
40
+ const DECLINED_RE = new RegExp(`\\b(${DECLINED_CATEGORIES.join('|')})\\b`, 'i');
41
+
42
+ // =============================================================================
43
+ // STORE
44
+ // =============================================================================
45
+
46
+ export class OperatorContextStore {
47
+ private provider: PersistenceProvider;
48
+ private lastRendered: string | null = null;
49
+
50
+ constructor(provider: PersistenceProvider) {
51
+ this.provider = provider;
52
+ this.init();
53
+ }
54
+
55
+ // ─── Initialization ─────────────────────────────────────────────────
56
+
57
+ init(): void {
58
+ this.provider.execSql(`
59
+ CREATE TABLE IF NOT EXISTS operator_context (
60
+ id TEXT PRIMARY KEY,
61
+ type TEXT NOT NULL,
62
+ key TEXT NOT NULL,
63
+ value TEXT NOT NULL,
64
+ confidence REAL DEFAULT 0.5,
65
+ scope TEXT DEFAULT 'global',
66
+ session_count INTEGER DEFAULT 1,
67
+ last_observed INTEGER NOT NULL,
68
+ created_at INTEGER NOT NULL,
69
+ active INTEGER DEFAULT 1
70
+ );
71
+
72
+ CREATE INDEX IF NOT EXISTS idx_operator_context_type
73
+ ON operator_context(type);
74
+
75
+ CREATE INDEX IF NOT EXISTS idx_operator_context_key
76
+ ON operator_context(type, key);
77
+ `);
78
+ }
79
+
80
+ // ─── Read ──────────────────────────────────────────────────────────
81
+
82
+ getContext(): OperatorContext {
83
+ const rows = this.provider.all<ContextRow>(
84
+ 'SELECT * FROM operator_context WHERE active = 1 ORDER BY type, last_observed DESC',
85
+ );
86
+
87
+ const expertise: ExpertiseItem[] = [];
88
+ const corrections: CorrectionItem[] = [];
89
+ const interests: InterestItem[] = [];
90
+ const patterns: WorkPatternItem[] = [];
91
+
92
+ for (const row of rows) {
93
+ const parsed = JSON.parse(row.value);
94
+ switch (row.type as ContextItemType) {
95
+ case 'expertise':
96
+ expertise.push(parsed as ExpertiseItem);
97
+ break;
98
+ case 'correction':
99
+ corrections.push(parsed as CorrectionItem);
100
+ break;
101
+ case 'interest':
102
+ interests.push(parsed as InterestItem);
103
+ break;
104
+ case 'pattern':
105
+ patterns.push(parsed as WorkPatternItem);
106
+ break;
107
+ }
108
+ }
109
+
110
+ const sessionCountRow = this.provider.get<{ cnt: number }>(
111
+ 'SELECT MAX(session_count) as cnt FROM operator_context',
112
+ );
113
+ const lastRow = this.provider.get<{ ts: number }>(
114
+ 'SELECT MAX(last_observed) as ts FROM operator_context',
115
+ );
116
+
117
+ return {
118
+ expertise,
119
+ corrections,
120
+ interests,
121
+ patterns,
122
+ sessionCount: sessionCountRow?.cnt ?? 0,
123
+ lastUpdated: lastRow?.ts ?? 0,
124
+ };
125
+ }
126
+
127
+ // ─── Compound ─────────────────────────────────────────────────────
128
+
129
+ compoundSignals(signals: OperatorSignals, sessionId: string): void {
130
+ this.provider.transaction(() => {
131
+ for (const sig of signals.expertise) {
132
+ if (this.isDeclined(sig.topic, sig.evidence)) continue;
133
+ this.compoundExpertise(sig);
134
+ }
135
+ for (const sig of signals.corrections) {
136
+ if (this.isDeclined(sig.rule, sig.quote)) continue;
137
+ // If this correction undoes an existing one, deactivate it and skip storage
138
+ if (this.tryUndoCorrection(sig.rule)) continue;
139
+ this.compoundCorrection(sig, sessionId);
140
+ }
141
+ for (const sig of signals.interests) {
142
+ if (this.isDeclined(sig.tag, sig.context)) continue;
143
+ this.compoundInterest(sig);
144
+ }
145
+ for (const sig of signals.patterns) {
146
+ if (this.isDeclined(sig.pattern)) continue;
147
+ this.compoundPattern(sig);
148
+ }
149
+
150
+ // Decay interests not mentioned in this session
151
+ this.decayInterests(signals.interests.map((i) => i.tag));
152
+ });
153
+ }
154
+
155
+ // ─── Drift ────────────────────────────────────────────────────────
156
+
157
+ hasDrifted(): boolean {
158
+ const current = this.renderContextFile();
159
+ if (this.lastRendered === null) {
160
+ this.lastRendered = current;
161
+ return true; // first render is always drift
162
+ }
163
+ const drifted = current !== this.lastRendered;
164
+ if (drifted) {
165
+ this.lastRendered = current;
166
+ }
167
+ return drifted;
168
+ }
169
+
170
+ // ─── Render ───────────────────────────────────────────────────────
171
+
172
+ renderContextFile(): string {
173
+ const ctx = this.getContext();
174
+ const lines: string[] = ['# Operator Context', ''];
175
+
176
+ // Expertise
177
+ if (ctx.expertise.length > 0) {
178
+ const items = ctx.expertise
179
+ .sort((a, b) => b.confidence - a.confidence)
180
+ .map(
181
+ (e) =>
182
+ `${e.topic} (${e.level}, ${e.sessionCount} sessions, confidence ${e.confidence.toFixed(2)})`,
183
+ );
184
+ lines.push(`**Expertise:** ${items.join(', ')}.`);
185
+ lines.push('');
186
+ }
187
+
188
+ // Corrections
189
+ const active = ctx.corrections.filter((c) => c.active);
190
+ if (active.length > 0) {
191
+ lines.push('**Corrections:**');
192
+ for (const c of active) {
193
+ const scopeTag = c.scope === 'project' ? ` [project]` : '';
194
+ const quoteTag = c.quote ? ` — "${c.quote}"` : '';
195
+ lines.push(`- ${c.rule}${scopeTag}${quoteTag}`);
196
+ }
197
+ lines.push('');
198
+ }
199
+
200
+ // Interests
201
+ const significantInterests = ctx.interests
202
+ .filter((i) => i.confidence >= 0.3)
203
+ .sort((a, b) => b.confidence - a.confidence);
204
+ if (significantInterests.length > 0) {
205
+ const items = significantInterests.map(
206
+ (i) => `${i.tag} (${i.mentionCount} mentions, confidence ${i.confidence.toFixed(2)})`,
207
+ );
208
+ lines.push(`**Interests:** ${items.join(', ')}.`);
209
+ lines.push('');
210
+ }
211
+
212
+ // Patterns
213
+ if (ctx.patterns.length > 0) {
214
+ const items = ctx.patterns
215
+ .sort((a, b) => b.confidence - a.confidence)
216
+ .map((p) => `${p.pattern} (${p.frequency}, ${p.observedCount} observations)`);
217
+ lines.push(`**Work patterns:** ${items.join(', ')}.`);
218
+ lines.push('');
219
+ }
220
+
221
+ return lines.join('\n').trimEnd();
222
+ }
223
+
224
+ // ─── Inspect / Delete ─────────────────────────────────────────────
225
+
226
+ inspect(): OperatorContext {
227
+ return this.getContext();
228
+ }
229
+
230
+ deleteItem(type: ContextItemType, id: string): boolean {
231
+ const result = this.provider.run('DELETE FROM operator_context WHERE type = ? AND id = ?', [
232
+ type,
233
+ id,
234
+ ]);
235
+ return result.changes > 0;
236
+ }
237
+
238
+ // ─── Private: Compounding Algorithms ──────────────────────────────
239
+
240
+ private compoundExpertise(sig: ExpertiseSignal): void {
241
+ const now = Date.now();
242
+ const signalConf = sig.confidence ?? DEFAULT_CONFIDENCE;
243
+ const key = sig.topic.toLowerCase();
244
+
245
+ const existing = this.getRow('expertise', key);
246
+ if (existing) {
247
+ const item = JSON.parse(existing.value) as ExpertiseItem;
248
+
249
+ // Exponential moving average
250
+ const newConfidence = item.confidence * 0.7 + signalConf * 0.3;
251
+
252
+ // Level upgrade: only up, never down automatically
253
+ let level = item.level;
254
+ if (this.levelRank(sig.level) > this.levelRank(item.level) && newConfidence > 0.8) {
255
+ level = sig.level;
256
+ }
257
+
258
+ const updated: ExpertiseItem = {
259
+ ...item,
260
+ level,
261
+ confidence: Math.min(1.0, newConfidence),
262
+ sessionCount: item.sessionCount + 1,
263
+ lastObserved: now,
264
+ };
265
+
266
+ this.updateRow(existing.id, 'expertise', key, updated, updated.confidence);
267
+ } else {
268
+ const item: ExpertiseItem = {
269
+ topic: sig.topic,
270
+ level: sig.level,
271
+ confidence: signalConf,
272
+ sessionCount: 1,
273
+ lastObserved: now,
274
+ };
275
+ this.insertRow('expertise', key, item, signalConf);
276
+ }
277
+ }
278
+
279
+ private compoundCorrection(sig: CorrectionSignal, sessionId: string): void {
280
+ const now = Date.now();
281
+ const key = sig.rule.toLowerCase();
282
+
283
+ // Corrections: latest wins on conflict
284
+ const existing = this.getRow('correction', key);
285
+ if (existing) {
286
+ const item: CorrectionItem = {
287
+ ...(JSON.parse(existing.value) as CorrectionItem),
288
+ rule: sig.rule,
289
+ quote: sig.quote,
290
+ scope: sig.scope,
291
+ active: true,
292
+ sessionId,
293
+ };
294
+ this.updateRow(existing.id, 'correction', key, item, 1.0, sig.scope);
295
+ } else {
296
+ const correctionId = randomUUID();
297
+ const item: CorrectionItem = {
298
+ id: correctionId,
299
+ rule: sig.rule,
300
+ quote: sig.quote,
301
+ scope: sig.scope,
302
+ active: true,
303
+ createdAt: now,
304
+ sessionId,
305
+ };
306
+ this.insertRow('correction', key, item, 1.0, sig.scope, correctionId);
307
+ }
308
+ }
309
+
310
+ private compoundInterest(sig: InterestSignal): void {
311
+ const now = Date.now();
312
+ const key = sig.tag.toLowerCase();
313
+
314
+ const existing = this.getRow('interest', key);
315
+ if (existing) {
316
+ const item = JSON.parse(existing.value) as InterestItem;
317
+ const newMentions = item.mentionCount + 1;
318
+ const newConfidence = Math.min(1.0, item.confidence + 0.1);
319
+
320
+ const updated: InterestItem = {
321
+ ...item,
322
+ confidence: newConfidence,
323
+ mentionCount: newMentions,
324
+ lastMentioned: now,
325
+ };
326
+ this.updateRow(existing.id, 'interest', key, updated, updated.confidence);
327
+ } else {
328
+ const item: InterestItem = {
329
+ tag: sig.tag,
330
+ confidence: DEFAULT_CONFIDENCE,
331
+ mentionCount: 1,
332
+ lastMentioned: now,
333
+ };
334
+ this.insertRow('interest', key, item, item.confidence);
335
+ }
336
+ }
337
+
338
+ private compoundPattern(sig: WorkPatternSignal): void {
339
+ const now = Date.now();
340
+ const signalConf = DEFAULT_CONFIDENCE;
341
+ const key = sig.pattern.toLowerCase();
342
+
343
+ const existing = this.getRow('pattern', key);
344
+ if (existing) {
345
+ const item = JSON.parse(existing.value) as WorkPatternItem;
346
+
347
+ // Exponential moving average
348
+ const newConfidence = item.confidence * 0.8 + signalConf * 0.2;
349
+ const newCount = item.observedCount + 1;
350
+
351
+ // Frequency upgrades
352
+ let frequency = item.frequency;
353
+ if (newCount >= 8) frequency = 'frequent';
354
+ else if (newCount >= 3) frequency = 'occasional';
355
+
356
+ const updated: WorkPatternItem = {
357
+ ...item,
358
+ frequency,
359
+ confidence: Math.min(1.0, newConfidence),
360
+ observedCount: newCount,
361
+ lastObserved: now,
362
+ };
363
+ this.updateRow(existing.id, 'pattern', key, updated, updated.confidence);
364
+ } else {
365
+ const item: WorkPatternItem = {
366
+ pattern: sig.pattern,
367
+ frequency: sig.frequency ?? 'once',
368
+ confidence: signalConf,
369
+ observedCount: 1,
370
+ lastObserved: now,
371
+ };
372
+ this.insertRow('pattern', key, item, item.confidence);
373
+ }
374
+ }
375
+
376
+ private decayInterests(mentionedTags: string[]): void {
377
+ const mentioned = new Set(mentionedTags.map((t) => t.toLowerCase()));
378
+ const allInterests = this.provider.all<ContextRow>(
379
+ "SELECT * FROM operator_context WHERE type = 'interest' AND active = 1",
380
+ );
381
+
382
+ for (const row of allInterests) {
383
+ if (mentioned.has(row.key)) continue;
384
+ const item = JSON.parse(row.value) as InterestItem;
385
+ const decayed = Math.max(0.1, item.confidence - 0.01);
386
+ if (decayed !== item.confidence) {
387
+ const updated = { ...item, confidence: decayed };
388
+ this.updateRow(row.id, 'interest', row.key, updated, decayed);
389
+ }
390
+ }
391
+ }
392
+
393
+ // ─── Private: DB Helpers ──────────────────────────────────────────
394
+
395
+ private getRow(type: ContextItemType, key: string): ContextRow | undefined {
396
+ return this.provider.get<ContextRow>(
397
+ 'SELECT * FROM operator_context WHERE type = ? AND key = ? AND active = 1',
398
+ [type, key],
399
+ );
400
+ }
401
+
402
+ private insertRow(
403
+ type: ContextItemType,
404
+ key: string,
405
+ value: unknown,
406
+ confidence: number,
407
+ scope: string = 'global',
408
+ rowId?: string,
409
+ ): void {
410
+ const now = Date.now();
411
+ this.provider.run(
412
+ `INSERT INTO operator_context (id, type, key, value, confidence, scope, session_count, last_observed, created_at, active)
413
+ VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, 1)`,
414
+ [rowId ?? randomUUID(), type, key, JSON.stringify(value), confidence, scope, now, now],
415
+ );
416
+ }
417
+
418
+ private updateRow(
419
+ id: string,
420
+ type: ContextItemType,
421
+ key: string,
422
+ value: unknown,
423
+ confidence: number,
424
+ scope?: string,
425
+ ): void {
426
+ const now = Date.now();
427
+ const scopeClause = scope !== undefined ? ', scope = ?' : '';
428
+ const params: unknown[] = [
429
+ JSON.stringify(value),
430
+ confidence,
431
+ now,
432
+ ...(scope !== undefined ? [scope] : []),
433
+ id,
434
+ ];
435
+ this.provider.run(
436
+ `UPDATE operator_context SET value = ?, confidence = ?, last_observed = ?${scopeClause} WHERE id = ?`,
437
+ params,
438
+ );
439
+ // Suppress unused warnings
440
+ void type;
441
+ void key;
442
+ }
443
+
444
+ // ─── Private: Utilities ───────────────────────────────────────────
445
+
446
+ private levelRank(level: ExpertiseLevel): number {
447
+ switch (level) {
448
+ case 'learning':
449
+ return 0;
450
+ case 'intermediate':
451
+ return 1;
452
+ case 'expert':
453
+ return 2;
454
+ }
455
+ }
456
+
457
+ private isDeclined(...fields: (string | undefined | null)[]): boolean {
458
+ for (const field of fields) {
459
+ if (field && DECLINED_RE.test(field)) return true;
460
+ }
461
+ return false;
462
+ }
463
+
464
+ /**
465
+ * Check all active corrections for an undo match. If found, deactivate the
466
+ * existing correction and return `true` (meaning: the new signal is a
467
+ * reversal, not a new rule to store).
468
+ */
469
+ private tryUndoCorrection(rule: string): boolean {
470
+ const newNorm = normalizeCorrection(rule);
471
+ const activeCorrections = this.provider.all<ContextRow>(
472
+ "SELECT * FROM operator_context WHERE type = 'correction' AND active = 1",
473
+ );
474
+
475
+ for (const row of activeCorrections) {
476
+ const item = JSON.parse(row.value) as CorrectionItem;
477
+ const existingNorm = normalizeCorrection(item.rule);
478
+
479
+ if (isUndoCorrection(newNorm, existingNorm)) {
480
+ // Deactivate the old correction
481
+ const deactivated = { ...item, active: false };
482
+ this.provider.run(
483
+ 'UPDATE operator_context SET value = ?, active = 0, last_observed = ? WHERE id = ?',
484
+ [JSON.stringify(deactivated), Date.now(), row.id],
485
+ );
486
+ return true;
487
+ }
488
+ }
489
+ return false;
490
+ }
491
+ }
492
+
493
+ // =============================================================================
494
+ // UNDO DETECTION HELPERS (exported for testing)
495
+ // =============================================================================
496
+
497
+ export interface NormalizedCorrection {
498
+ topic: string;
499
+ direction: 'do' | 'dont';
500
+ }
501
+
502
+ /**
503
+ * Extract the core topic and direction from a correction rule.
504
+ *
505
+ * "don't summarize" → { topic: "summarize", direction: "dont" }
506
+ * "actually, summaries are fine" → { topic: "summaries are fine", direction: "do" }
507
+ */
508
+ export function normalizeCorrection(rule: string): NormalizedCorrection {
509
+ let direction: 'do' | 'dont' = 'dont';
510
+ let topic = rule.toLowerCase().trim();
511
+
512
+ if (DONT_PATTERNS.test(topic)) {
513
+ direction = 'dont';
514
+ topic = topic.replace(DONT_PATTERNS, '').trim();
515
+ } else if (DO_PATTERNS.test(topic)) {
516
+ direction = 'do';
517
+ topic = topic.replace(DO_PATTERNS, '').trim();
518
+ }
519
+
520
+ // Strip trailing punctuation and common filler for comparison
521
+ topic = topic.replace(/[.!?]+$/, '').trim();
522
+
523
+ return { topic, direction };
524
+ }
525
+
526
+ /**
527
+ * Two corrections are an undo pair when they share a topic (fuzzy substring
528
+ * match) but have opposite directions.
529
+ */
530
+ export function isUndoCorrection(a: NormalizedCorrection, b: NormalizedCorrection): boolean {
531
+ if (a.direction === b.direction) return false;
532
+
533
+ // Exact match
534
+ if (a.topic === b.topic) return true;
535
+
536
+ // Fuzzy: one topic is a substring of the other (covers "summarize" vs
537
+ // "summaries are fine" — we check if either contains the first significant
538
+ // word of the other).
539
+ const aWords = significantWords(a.topic);
540
+ const bWords = significantWords(b.topic);
541
+
542
+ // At least one significant word must overlap
543
+ return aWords.some((w) => bWords.includes(w));
544
+ }
545
+
546
+ /** Extract meaningful words (>= 4 chars) for fuzzy topic matching. */
547
+ function significantWords(topic: string): string[] {
548
+ return topic
549
+ .split(/\s+/)
550
+ .filter((w) => w.length >= 4)
551
+ .map((w) => w.replace(/(?:ing|ies|ise|ize|es|s)$/, '')); // crude stemming
552
+ }
553
+
554
+ // =============================================================================
555
+ // INTERNAL ROW TYPE
556
+ // =============================================================================
557
+
558
+ interface ContextRow {
559
+ id: string;
560
+ type: string;
561
+ key: string;
562
+ value: string;
563
+ confidence: number;
564
+ scope: string;
565
+ session_count: number;
566
+ last_observed: number;
567
+ created_at: number;
568
+ active: number;
569
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Operator Context Signal Taxonomy — types for Soleri's adaptive persona feature.
3
+ *
4
+ * 4 signal types the agent reports through `orchestrate_complete`:
5
+ * - expertise: what the operator knows
6
+ * - corrections: what the operator wants done differently
7
+ * - interests: what the operator cares about beyond work
8
+ * - patterns: how the operator works
9
+ *
10
+ * Signals are compounded over sessions into a stable OperatorContext profile.
11
+ */
12
+
13
+ // =============================================================================
14
+ // INPUT SIGNALS (reported per session)
15
+ // =============================================================================
16
+
17
+ /** Bag of signals the agent reports after a session. */
18
+ export interface OperatorSignals {
19
+ expertise: ExpertiseSignal[];
20
+ corrections: CorrectionSignal[];
21
+ interests: InterestSignal[];
22
+ patterns: WorkPatternSignal[];
23
+ }
24
+
25
+ /** Observed expertise in a topic. */
26
+ export interface ExpertiseSignal {
27
+ /** e.g. "typescript", "react", "postgresql" */
28
+ topic: string;
29
+ level: ExpertiseLevel;
30
+ /** Brief quote or observation. */
31
+ evidence?: string;
32
+ /** 0.0–1.0, engine assigns default if missing. */
33
+ confidence?: number;
34
+ }
35
+
36
+ /** Operator correction — "do this" / "don't do that". */
37
+ export interface CorrectionSignal {
38
+ /** What to do or not do. */
39
+ rule: string;
40
+ /** User's exact words. */
41
+ quote?: string;
42
+ scope: SignalScope;
43
+ }
44
+
45
+ /** Something the operator cares about outside of work. */
46
+ export interface InterestSignal {
47
+ /** e.g. "metal music", "coffee", "climbing" */
48
+ tag: string;
49
+ /** How it came up. */
50
+ context?: string;
51
+ }
52
+
53
+ /** How the operator works. */
54
+ export interface WorkPatternSignal {
55
+ /** e.g. "batches work locally", "prefers small PRs" */
56
+ pattern: string;
57
+ frequency?: PatternFrequency;
58
+ }
59
+
60
+ // =============================================================================
61
+ // COMPOUNDED PROFILE (stored in SQLite)
62
+ // =============================================================================
63
+
64
+ /** Full compounded operator context, assembled from accumulated signals. */
65
+ export interface OperatorContext {
66
+ expertise: ExpertiseItem[];
67
+ corrections: CorrectionItem[];
68
+ interests: InterestItem[];
69
+ patterns: WorkPatternItem[];
70
+ sessionCount: number;
71
+ lastUpdated: number;
72
+ }
73
+
74
+ /** Compounded expertise entry. */
75
+ export interface ExpertiseItem {
76
+ topic: string;
77
+ level: ExpertiseLevel;
78
+ confidence: number;
79
+ /** How many sessions observed this topic. */
80
+ sessionCount: number;
81
+ lastObserved: number;
82
+ }
83
+
84
+ /** Stored correction. */
85
+ export interface CorrectionItem {
86
+ id: string;
87
+ rule: string;
88
+ quote?: string;
89
+ scope: SignalScope;
90
+ projectPath?: string;
91
+ active: boolean;
92
+ createdAt: number;
93
+ sessionId?: string;
94
+ }
95
+
96
+ /** Compounded interest entry. */
97
+ export interface InterestItem {
98
+ tag: string;
99
+ confidence: number;
100
+ mentionCount: number;
101
+ lastMentioned: number;
102
+ }
103
+
104
+ /** Compounded work pattern entry. */
105
+ export interface WorkPatternItem {
106
+ pattern: string;
107
+ frequency: PatternFrequency;
108
+ confidence: number;
109
+ observedCount: number;
110
+ lastObserved: number;
111
+ }
112
+
113
+ // =============================================================================
114
+ // SHARED ENUMS / LITERALS
115
+ // =============================================================================
116
+
117
+ export type ExpertiseLevel = 'learning' | 'intermediate' | 'expert';
118
+ export type SignalScope = 'global' | 'project';
119
+ export type PatternFrequency = 'once' | 'occasional' | 'frequent';
120
+ export type ContextItemType = 'expertise' | 'correction' | 'interest' | 'pattern';
121
+
122
+ // =============================================================================
123
+ // MUST-NOT-LEARN CATEGORIES
124
+ // =============================================================================
125
+
126
+ /** Categories the agent must never store, regardless of what is observed. */
127
+ export const DECLINED_CATEGORIES = [
128
+ 'health',
129
+ 'medical',
130
+ 'political',
131
+ 'religious',
132
+ 'sexual',
133
+ 'financial',
134
+ 'legal',
135
+ 'family',
136
+ 'relationship',
137
+ ] as const;
138
+
139
+ export type DeclinedCategory = (typeof DECLINED_CATEGORIES)[number];