@soleri/core 2.0.2 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/dist/brain/brain.d.ts +14 -50
  2. package/dist/brain/brain.d.ts.map +1 -1
  3. package/dist/brain/brain.js +207 -16
  4. package/dist/brain/brain.js.map +1 -1
  5. package/dist/brain/intelligence.d.ts +86 -0
  6. package/dist/brain/intelligence.d.ts.map +1 -0
  7. package/dist/brain/intelligence.js +771 -0
  8. package/dist/brain/intelligence.js.map +1 -0
  9. package/dist/brain/types.d.ts +197 -0
  10. package/dist/brain/types.d.ts.map +1 -0
  11. package/dist/brain/types.js +2 -0
  12. package/dist/brain/types.js.map +1 -0
  13. package/dist/cognee/client.d.ts +35 -0
  14. package/dist/cognee/client.d.ts.map +1 -0
  15. package/dist/cognee/client.js +291 -0
  16. package/dist/cognee/client.js.map +1 -0
  17. package/dist/cognee/types.d.ts +46 -0
  18. package/dist/cognee/types.d.ts.map +1 -0
  19. package/dist/cognee/types.js +3 -0
  20. package/dist/cognee/types.js.map +1 -0
  21. package/dist/control/identity-manager.d.ts +22 -0
  22. package/dist/control/identity-manager.d.ts.map +1 -0
  23. package/dist/control/identity-manager.js +233 -0
  24. package/dist/control/identity-manager.js.map +1 -0
  25. package/dist/control/intent-router.d.ts +32 -0
  26. package/dist/control/intent-router.d.ts.map +1 -0
  27. package/dist/control/intent-router.js +242 -0
  28. package/dist/control/intent-router.js.map +1 -0
  29. package/dist/control/types.d.ts +68 -0
  30. package/dist/control/types.d.ts.map +1 -0
  31. package/dist/control/types.js +9 -0
  32. package/dist/control/types.js.map +1 -0
  33. package/dist/curator/curator.d.ts +29 -0
  34. package/dist/curator/curator.d.ts.map +1 -1
  35. package/dist/curator/curator.js +142 -5
  36. package/dist/curator/curator.js.map +1 -1
  37. package/dist/facades/types.d.ts +1 -1
  38. package/dist/governance/governance.d.ts +42 -0
  39. package/dist/governance/governance.d.ts.map +1 -0
  40. package/dist/governance/governance.js +488 -0
  41. package/dist/governance/governance.js.map +1 -0
  42. package/dist/governance/index.d.ts +3 -0
  43. package/dist/governance/index.d.ts.map +1 -0
  44. package/dist/governance/index.js +2 -0
  45. package/dist/governance/index.js.map +1 -0
  46. package/dist/governance/types.d.ts +102 -0
  47. package/dist/governance/types.d.ts.map +1 -0
  48. package/dist/governance/types.js +3 -0
  49. package/dist/governance/types.js.map +1 -0
  50. package/dist/index.d.ts +35 -3
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +32 -1
  53. package/dist/index.js.map +1 -1
  54. package/dist/llm/llm-client.d.ts.map +1 -1
  55. package/dist/llm/llm-client.js +9 -2
  56. package/dist/llm/llm-client.js.map +1 -1
  57. package/dist/logging/logger.d.ts +37 -0
  58. package/dist/logging/logger.d.ts.map +1 -0
  59. package/dist/logging/logger.js +145 -0
  60. package/dist/logging/logger.js.map +1 -0
  61. package/dist/logging/types.d.ts +19 -0
  62. package/dist/logging/types.d.ts.map +1 -0
  63. package/dist/logging/types.js +2 -0
  64. package/dist/logging/types.js.map +1 -0
  65. package/dist/loop/loop-manager.d.ts +49 -0
  66. package/dist/loop/loop-manager.d.ts.map +1 -0
  67. package/dist/loop/loop-manager.js +105 -0
  68. package/dist/loop/loop-manager.js.map +1 -0
  69. package/dist/loop/types.d.ts +35 -0
  70. package/dist/loop/types.d.ts.map +1 -0
  71. package/dist/loop/types.js +8 -0
  72. package/dist/loop/types.js.map +1 -0
  73. package/dist/planning/gap-analysis.d.ts +29 -0
  74. package/dist/planning/gap-analysis.d.ts.map +1 -0
  75. package/dist/planning/gap-analysis.js +265 -0
  76. package/dist/planning/gap-analysis.js.map +1 -0
  77. package/dist/planning/gap-types.d.ts +29 -0
  78. package/dist/planning/gap-types.d.ts.map +1 -0
  79. package/dist/planning/gap-types.js +28 -0
  80. package/dist/planning/gap-types.js.map +1 -0
  81. package/dist/planning/planner.d.ts +150 -1
  82. package/dist/planning/planner.d.ts.map +1 -1
  83. package/dist/planning/planner.js +365 -2
  84. package/dist/planning/planner.js.map +1 -1
  85. package/dist/project/project-registry.d.ts +79 -0
  86. package/dist/project/project-registry.d.ts.map +1 -0
  87. package/dist/project/project-registry.js +276 -0
  88. package/dist/project/project-registry.js.map +1 -0
  89. package/dist/project/types.d.ts +28 -0
  90. package/dist/project/types.d.ts.map +1 -0
  91. package/dist/project/types.js +5 -0
  92. package/dist/project/types.js.map +1 -0
  93. package/dist/runtime/admin-extra-ops.d.ts +13 -0
  94. package/dist/runtime/admin-extra-ops.d.ts.map +1 -0
  95. package/dist/runtime/admin-extra-ops.js +284 -0
  96. package/dist/runtime/admin-extra-ops.js.map +1 -0
  97. package/dist/runtime/admin-ops.d.ts +15 -0
  98. package/dist/runtime/admin-ops.d.ts.map +1 -0
  99. package/dist/runtime/admin-ops.js +322 -0
  100. package/dist/runtime/admin-ops.js.map +1 -0
  101. package/dist/runtime/capture-ops.d.ts +15 -0
  102. package/dist/runtime/capture-ops.d.ts.map +1 -0
  103. package/dist/runtime/capture-ops.js +345 -0
  104. package/dist/runtime/capture-ops.js.map +1 -0
  105. package/dist/runtime/core-ops.d.ts +7 -3
  106. package/dist/runtime/core-ops.d.ts.map +1 -1
  107. package/dist/runtime/core-ops.js +646 -15
  108. package/dist/runtime/core-ops.js.map +1 -1
  109. package/dist/runtime/curator-extra-ops.d.ts +9 -0
  110. package/dist/runtime/curator-extra-ops.d.ts.map +1 -0
  111. package/dist/runtime/curator-extra-ops.js +59 -0
  112. package/dist/runtime/curator-extra-ops.js.map +1 -0
  113. package/dist/runtime/domain-ops.d.ts.map +1 -1
  114. package/dist/runtime/domain-ops.js +59 -13
  115. package/dist/runtime/domain-ops.js.map +1 -1
  116. package/dist/runtime/grading-ops.d.ts +14 -0
  117. package/dist/runtime/grading-ops.d.ts.map +1 -0
  118. package/dist/runtime/grading-ops.js +105 -0
  119. package/dist/runtime/grading-ops.js.map +1 -0
  120. package/dist/runtime/loop-ops.d.ts +13 -0
  121. package/dist/runtime/loop-ops.d.ts.map +1 -0
  122. package/dist/runtime/loop-ops.js +179 -0
  123. package/dist/runtime/loop-ops.js.map +1 -0
  124. package/dist/runtime/memory-cross-project-ops.d.ts +12 -0
  125. package/dist/runtime/memory-cross-project-ops.d.ts.map +1 -0
  126. package/dist/runtime/memory-cross-project-ops.js +165 -0
  127. package/dist/runtime/memory-cross-project-ops.js.map +1 -0
  128. package/dist/runtime/memory-extra-ops.d.ts +13 -0
  129. package/dist/runtime/memory-extra-ops.d.ts.map +1 -0
  130. package/dist/runtime/memory-extra-ops.js +173 -0
  131. package/dist/runtime/memory-extra-ops.js.map +1 -0
  132. package/dist/runtime/orchestrate-ops.d.ts +17 -0
  133. package/dist/runtime/orchestrate-ops.d.ts.map +1 -0
  134. package/dist/runtime/orchestrate-ops.js +240 -0
  135. package/dist/runtime/orchestrate-ops.js.map +1 -0
  136. package/dist/runtime/planning-extra-ops.d.ts +17 -0
  137. package/dist/runtime/planning-extra-ops.d.ts.map +1 -0
  138. package/dist/runtime/planning-extra-ops.js +300 -0
  139. package/dist/runtime/planning-extra-ops.js.map +1 -0
  140. package/dist/runtime/project-ops.d.ts +15 -0
  141. package/dist/runtime/project-ops.d.ts.map +1 -0
  142. package/dist/runtime/project-ops.js +181 -0
  143. package/dist/runtime/project-ops.js.map +1 -0
  144. package/dist/runtime/runtime.d.ts.map +1 -1
  145. package/dist/runtime/runtime.js +48 -1
  146. package/dist/runtime/runtime.js.map +1 -1
  147. package/dist/runtime/types.d.ts +23 -0
  148. package/dist/runtime/types.d.ts.map +1 -1
  149. package/dist/runtime/vault-extra-ops.d.ts +9 -0
  150. package/dist/runtime/vault-extra-ops.d.ts.map +1 -0
  151. package/dist/runtime/vault-extra-ops.js +195 -0
  152. package/dist/runtime/vault-extra-ops.js.map +1 -0
  153. package/dist/telemetry/telemetry.d.ts +48 -0
  154. package/dist/telemetry/telemetry.d.ts.map +1 -0
  155. package/dist/telemetry/telemetry.js +87 -0
  156. package/dist/telemetry/telemetry.js.map +1 -0
  157. package/dist/vault/vault.d.ts +94 -0
  158. package/dist/vault/vault.d.ts.map +1 -1
  159. package/dist/vault/vault.js +340 -1
  160. package/dist/vault/vault.js.map +1 -1
  161. package/package.json +1 -1
  162. package/src/__tests__/admin-extra-ops.test.ts +420 -0
  163. package/src/__tests__/admin-ops.test.ts +271 -0
  164. package/src/__tests__/brain-intelligence.test.ts +828 -0
  165. package/src/__tests__/brain.test.ts +396 -27
  166. package/src/__tests__/capture-ops.test.ts +509 -0
  167. package/src/__tests__/cognee-client.test.ts +524 -0
  168. package/src/__tests__/core-ops.test.ts +341 -49
  169. package/src/__tests__/curator-extra-ops.test.ts +359 -0
  170. package/src/__tests__/curator.test.ts +126 -31
  171. package/src/__tests__/domain-ops.test.ts +111 -9
  172. package/src/__tests__/governance.test.ts +522 -0
  173. package/src/__tests__/grading-ops.test.ts +340 -0
  174. package/src/__tests__/identity-manager.test.ts +243 -0
  175. package/src/__tests__/intent-router.test.ts +222 -0
  176. package/src/__tests__/logger.test.ts +200 -0
  177. package/src/__tests__/loop-ops.test.ts +398 -0
  178. package/src/__tests__/memory-cross-project-ops.test.ts +246 -0
  179. package/src/__tests__/memory-extra-ops.test.ts +352 -0
  180. package/src/__tests__/orchestrate-ops.test.ts +284 -0
  181. package/src/__tests__/planner.test.ts +331 -0
  182. package/src/__tests__/planning-extra-ops.test.ts +548 -0
  183. package/src/__tests__/project-ops.test.ts +367 -0
  184. package/src/__tests__/runtime.test.ts +13 -11
  185. package/src/__tests__/vault-extra-ops.test.ts +407 -0
  186. package/src/brain/brain.ts +308 -72
  187. package/src/brain/intelligence.ts +1230 -0
  188. package/src/brain/types.ts +214 -0
  189. package/src/cognee/client.ts +352 -0
  190. package/src/cognee/types.ts +62 -0
  191. package/src/control/identity-manager.ts +354 -0
  192. package/src/control/intent-router.ts +326 -0
  193. package/src/control/types.ts +102 -0
  194. package/src/curator/curator.ts +265 -15
  195. package/src/governance/governance.ts +698 -0
  196. package/src/governance/index.ts +18 -0
  197. package/src/governance/types.ts +111 -0
  198. package/src/index.ts +128 -3
  199. package/src/llm/llm-client.ts +18 -24
  200. package/src/logging/logger.ts +154 -0
  201. package/src/logging/types.ts +21 -0
  202. package/src/loop/loop-manager.ts +130 -0
  203. package/src/loop/types.ts +44 -0
  204. package/src/planning/gap-analysis.ts +506 -0
  205. package/src/planning/gap-types.ts +58 -0
  206. package/src/planning/planner.ts +478 -2
  207. package/src/project/project-registry.ts +358 -0
  208. package/src/project/types.ts +31 -0
  209. package/src/runtime/admin-extra-ops.ts +307 -0
  210. package/src/runtime/admin-ops.ts +329 -0
  211. package/src/runtime/capture-ops.ts +385 -0
  212. package/src/runtime/core-ops.ts +747 -26
  213. package/src/runtime/curator-extra-ops.ts +71 -0
  214. package/src/runtime/domain-ops.ts +65 -13
  215. package/src/runtime/grading-ops.ts +121 -0
  216. package/src/runtime/loop-ops.ts +194 -0
  217. package/src/runtime/memory-cross-project-ops.ts +192 -0
  218. package/src/runtime/memory-extra-ops.ts +186 -0
  219. package/src/runtime/orchestrate-ops.ts +272 -0
  220. package/src/runtime/planning-extra-ops.ts +327 -0
  221. package/src/runtime/project-ops.ts +196 -0
  222. package/src/runtime/runtime.ts +54 -1
  223. package/src/runtime/types.ts +23 -0
  224. package/src/runtime/vault-extra-ops.ts +225 -0
  225. package/src/telemetry/telemetry.ts +118 -0
  226. package/src/vault/vault.ts +412 -1
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Types for Identity Management and Intent Routing.
3
+ *
4
+ * Two separate concerns:
5
+ * - Identity: agent persona CRUD with versioning and guidelines
6
+ * - Intent Routing: keyword-based intent classification and operational modes
7
+ */
8
+
9
+ // ─── Identity Types ──────────────────────────────────────────────────
10
+
11
+ export type GuidelineCategory = 'behavior' | 'preference' | 'restriction' | 'style';
12
+
13
+ export interface Guideline {
14
+ id: string;
15
+ category: GuidelineCategory;
16
+ text: string;
17
+ priority: number;
18
+ createdAt: string;
19
+ updatedAt: string;
20
+ }
21
+
22
+ export interface AgentIdentity {
23
+ agentId: string;
24
+ name: string;
25
+ role: string;
26
+ description: string;
27
+ personality: string[];
28
+ guidelines: Guideline[];
29
+ version: number;
30
+ updatedAt: string;
31
+ }
32
+
33
+ export interface IdentityVersion {
34
+ version: number;
35
+ snapshot: string;
36
+ changedBy: string;
37
+ changeReason: string;
38
+ createdAt: string;
39
+ }
40
+
41
+ export interface IdentityUpdateInput {
42
+ name?: string;
43
+ role?: string;
44
+ description?: string;
45
+ personality?: string[];
46
+ changedBy?: string;
47
+ changeReason?: string;
48
+ }
49
+
50
+ export interface GuidelineInput {
51
+ category: GuidelineCategory;
52
+ text: string;
53
+ priority?: number;
54
+ }
55
+
56
+ // ─── Intent Routing Types ────────────────────────────────────────────
57
+
58
+ export type IntentType =
59
+ | 'build'
60
+ | 'fix'
61
+ | 'validate'
62
+ | 'design'
63
+ | 'improve'
64
+ | 'deliver'
65
+ | 'explore'
66
+ | 'plan'
67
+ | 'review'
68
+ | 'general';
69
+
70
+ export type OperationalMode =
71
+ | 'BUILD-MODE'
72
+ | 'FIX-MODE'
73
+ | 'VALIDATE-MODE'
74
+ | 'DESIGN-MODE'
75
+ | 'IMPROVE-MODE'
76
+ | 'DELIVER-MODE'
77
+ | 'EXPLORE-MODE'
78
+ | 'PLAN-MODE'
79
+ | 'REVIEW-MODE'
80
+ | 'GENERAL-MODE';
81
+
82
+ export interface IntentClassification {
83
+ intent: IntentType;
84
+ mode: OperationalMode;
85
+ confidence: number;
86
+ method: 'keyword';
87
+ matchedKeywords: string[];
88
+ }
89
+
90
+ export interface ModeConfig {
91
+ mode: OperationalMode;
92
+ intent: IntentType;
93
+ description: string;
94
+ behaviorRules: string[];
95
+ keywords: string[];
96
+ }
97
+
98
+ export interface MorphResult {
99
+ previousMode: OperationalMode;
100
+ currentMode: OperationalMode;
101
+ behaviorRules: string[];
102
+ }
@@ -93,6 +93,15 @@ export class Curator {
93
93
  created_at INTEGER NOT NULL DEFAULT (unixepoch())
94
94
  );
95
95
 
96
+ CREATE TABLE IF NOT EXISTS curator_entry_history (
97
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
98
+ entry_id TEXT NOT NULL,
99
+ snapshot TEXT NOT NULL,
100
+ changed_by TEXT DEFAULT 'system',
101
+ change_reason TEXT,
102
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
103
+ );
104
+
96
105
  CREATE TABLE IF NOT EXISTS curator_contradictions (
97
106
  id INTEGER PRIMARY KEY AUTOINCREMENT,
98
107
  pattern_id TEXT NOT NULL,
@@ -188,7 +197,13 @@ export class Curator {
188
197
  JSON.stringify(dedupedTags),
189
198
  entryId,
190
199
  );
191
- this.logChange('normalize_tags', entryId, JSON.stringify(entry.tags), JSON.stringify(dedupedTags), 'Tag normalization');
200
+ this.logChange(
201
+ 'normalize_tags',
202
+ entryId,
203
+ JSON.stringify(entry.tags),
204
+ JSON.stringify(dedupedTags),
205
+ 'Tag normalization',
206
+ );
192
207
  }
193
208
 
194
209
  return results;
@@ -245,13 +260,13 @@ export class Curator {
245
260
  // Build vectors for all entries
246
261
  const vectors = new Map<string, SparseVector>();
247
262
  for (const entry of entries) {
248
- const text = [entry.title, entry.description, entry.context ?? '', entry.tags.join(' ')].join(' ');
263
+ const text = [entry.title, entry.description, entry.context ?? '', entry.tags.join(' ')].join(
264
+ ' ',
265
+ );
249
266
  vectors.set(entry.id, calculateTfIdf(tokenize(text), vocabulary));
250
267
  }
251
268
 
252
- const targetEntries = entryId
253
- ? entries.filter((e) => e.id === entryId)
254
- : entries;
269
+ const targetEntries = entryId ? entries.filter((e) => e.id === entryId) : entries;
255
270
 
256
271
  const results: DuplicateDetectionResult[] = [];
257
272
 
@@ -428,7 +443,8 @@ export class Curator {
428
443
  const dryRun = options?.dryRun ?? true;
429
444
  const staleDaysThreshold = options?.staleDaysThreshold ?? DEFAULT_STALE_DAYS;
430
445
  const duplicateThreshold = options?.duplicateThreshold ?? DEFAULT_DUPLICATE_THRESHOLD;
431
- const contradictionThreshold = options?.contradictionThreshold ?? DEFAULT_CONTRADICTION_THRESHOLD;
446
+ const contradictionThreshold =
447
+ options?.contradictionThreshold ?? DEFAULT_CONTRADICTION_THRESHOLD;
432
448
 
433
449
  // Detect duplicates
434
450
  const duplicates = this.detectDuplicates(undefined, duplicateThreshold);
@@ -455,7 +471,13 @@ export class Curator {
455
471
  VALUES (?, 'archived', unixepoch())
456
472
  ON CONFLICT(entry_id) DO UPDATE SET status = 'archived', last_groomed_at = unixepoch()`,
457
473
  ).run(entryId);
458
- this.logChange('archive', entryId, 'active', 'archived', 'Stale entry archived during consolidation');
474
+ this.logChange(
475
+ 'archive',
476
+ entryId,
477
+ 'active',
478
+ 'archived',
479
+ 'Stale entry archived during consolidation',
480
+ );
459
481
  mutations++;
460
482
  }
461
483
 
@@ -526,9 +548,21 @@ export class Curator {
526
548
  const hasAntiPatterns = typeCount['anti-pattern'] > 0;
527
549
  const hasRules = typeCount.rule > 0;
528
550
  let coverageScore = 1;
529
- if (!hasPatterns) { score -= 10; coverageScore -= 0.33; recommendations.push('No patterns found — add patterns to improve coverage.'); }
530
- if (!hasAntiPatterns) { score -= 5; coverageScore -= 0.17; recommendations.push('No anti-patterns found — add anti-patterns to detect contradictions.'); }
531
- if (!hasRules) { score -= 5; coverageScore -= 0.17; recommendations.push('No rules found — add rules for completeness.'); }
551
+ if (!hasPatterns) {
552
+ score -= 10;
553
+ coverageScore -= 0.33;
554
+ recommendations.push('No patterns found — add patterns to improve coverage.');
555
+ }
556
+ if (!hasAntiPatterns) {
557
+ score -= 5;
558
+ coverageScore -= 0.17;
559
+ recommendations.push('No anti-patterns found — add anti-patterns to detect contradictions.');
560
+ }
561
+ if (!hasRules) {
562
+ score -= 5;
563
+ coverageScore -= 0.17;
564
+ recommendations.push('No rules found — add rules for completeness.');
565
+ }
532
566
  coverageScore = Math.max(0, coverageScore);
533
567
 
534
568
  // Freshness: penalize stale entries
@@ -536,14 +570,18 @@ export class Curator {
536
570
  const now = Math.floor(Date.now() / 1000);
537
571
  const staleThreshold = now - DEFAULT_STALE_DAYS * 86400;
538
572
  const staleCount = (
539
- db.prepare('SELECT COUNT(*) as count FROM entries WHERE updated_at < ?').get(staleThreshold) as { count: number }
573
+ db
574
+ .prepare('SELECT COUNT(*) as count FROM entries WHERE updated_at < ?')
575
+ .get(staleThreshold) as { count: number }
540
576
  ).count;
541
577
  const staleRatio = staleCount / entries.length;
542
578
  const freshnessScore = 1 - staleRatio;
543
579
  if (staleRatio > 0.3) {
544
580
  const penalty = Math.min(20, Math.round(staleRatio * 30));
545
581
  score -= penalty;
546
- recommendations.push(`${staleCount} stale entries (${Math.round(staleRatio * 100)}%) — run grooming to update.`);
582
+ recommendations.push(
583
+ `${staleCount} stale entries (${Math.round(staleRatio * 100)}%) — run grooming to update.`,
584
+ );
547
585
  }
548
586
 
549
587
  // Quality: penalize duplicates and contradictions
@@ -571,12 +609,18 @@ export class Curator {
571
609
  if (lowTagRatio > 0.3) {
572
610
  const penalty = Math.min(10, Math.round(lowTagRatio * 15));
573
611
  score -= penalty;
574
- recommendations.push(`${lowTagEntries.length} entries have fewer than 2 tags — improve tagging.`);
612
+ recommendations.push(
613
+ `${lowTagEntries.length} entries have fewer than 2 tags — improve tagging.`,
614
+ );
575
615
  }
576
616
 
577
617
  // Penalize ungroomed entries
578
618
  const groomedCount = (
579
- db.prepare('SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL').get() as { count: number }
619
+ db
620
+ .prepare(
621
+ 'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
622
+ )
623
+ .get() as { count: number }
580
624
  ).count;
581
625
  if (groomedCount < entries.length) {
582
626
  const ungroomed = entries.length - groomedCount;
@@ -603,13 +647,219 @@ export class Curator {
603
647
  };
604
648
  }
605
649
 
650
+ // ─── Entry History (Version Snapshots) ─────────────────────────
651
+
652
+ recordSnapshot(
653
+ entryId: string,
654
+ changedBy?: string,
655
+ changeReason?: string,
656
+ ): { recorded: boolean; historyId: number } {
657
+ const entry = this.vault.get(entryId);
658
+ if (!entry) return { recorded: false, historyId: -1 };
659
+
660
+ const db = this.vault.getDb();
661
+ const result = db
662
+ .prepare(
663
+ 'INSERT INTO curator_entry_history (entry_id, snapshot, changed_by, change_reason, created_at) VALUES (?, ?, ?, ?, unixepoch())',
664
+ )
665
+ .run(entryId, JSON.stringify(entry), changedBy ?? 'system', changeReason ?? null);
666
+
667
+ return { recorded: true, historyId: Number(result.lastInsertRowid) };
668
+ }
669
+
670
+ getVersionHistory(
671
+ entryId: string,
672
+ ): Array<{
673
+ historyId: number;
674
+ entryId: string;
675
+ snapshot: IntelligenceEntry;
676
+ changedBy: string;
677
+ changeReason: string | null;
678
+ createdAt: number;
679
+ }> {
680
+ const db = this.vault.getDb();
681
+ const rows = db
682
+ .prepare(
683
+ 'SELECT * FROM curator_entry_history WHERE entry_id = ? ORDER BY created_at ASC, id ASC',
684
+ )
685
+ .all(entryId) as Array<Record<string, unknown>>;
686
+
687
+ return rows.map((row) => ({
688
+ historyId: row.id as number,
689
+ entryId: row.entry_id as string,
690
+ snapshot: JSON.parse(row.snapshot as string) as IntelligenceEntry,
691
+ changedBy: row.changed_by as string,
692
+ changeReason: (row.change_reason as string) ?? null,
693
+ createdAt: row.created_at as number,
694
+ }));
695
+ }
696
+
697
+ // ─── Queue Stats ──────────────────────────────────────────────
698
+
699
+ getQueueStats(): {
700
+ totalEntries: number;
701
+ groomedEntries: number;
702
+ ungroomedEntries: number;
703
+ staleEntries: number;
704
+ freshEntries: number;
705
+ avgDaysSinceGroom: number;
706
+ } {
707
+ const db = this.vault.getDb();
708
+ const totalEntries = (
709
+ db.prepare('SELECT COUNT(*) as count FROM entries').get() as { count: number }
710
+ ).count;
711
+
712
+ const groomedEntries = (
713
+ db
714
+ .prepare(
715
+ 'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
716
+ )
717
+ .get() as { count: number }
718
+ ).count;
719
+
720
+ const ungroomedEntries = totalEntries - groomedEntries;
721
+
722
+ const now = Math.floor(Date.now() / 1000);
723
+ const staleThreshold = now - 30 * 86400;
724
+ const freshThreshold = now - 7 * 86400;
725
+
726
+ const staleEntries = (
727
+ db
728
+ .prepare(
729
+ 'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL AND last_groomed_at < ?',
730
+ )
731
+ .get(staleThreshold) as { count: number }
732
+ ).count;
733
+
734
+ const freshEntries = (
735
+ db
736
+ .prepare(
737
+ 'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL AND last_groomed_at >= ?',
738
+ )
739
+ .get(freshThreshold) as { count: number }
740
+ ).count;
741
+
742
+ let avgDaysSinceGroom = 0;
743
+ if (groomedEntries > 0) {
744
+ const sumRow = db
745
+ .prepare(
746
+ 'SELECT SUM(? - last_groomed_at) as total FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
747
+ )
748
+ .get(now) as { total: number | null };
749
+ const totalSeconds = sumRow.total ?? 0;
750
+ avgDaysSinceGroom = Math.round((totalSeconds / groomedEntries / 86400) * 100) / 100;
751
+ }
752
+
753
+ return {
754
+ totalEntries,
755
+ groomedEntries,
756
+ ungroomedEntries,
757
+ staleEntries,
758
+ freshEntries,
759
+ avgDaysSinceGroom,
760
+ };
761
+ }
762
+
763
+ // ─── Metadata Enrichment ──────────────────────────────────────
764
+
765
+ enrichMetadata(
766
+ entryId: string,
767
+ ): { enriched: boolean; changes: Array<{ field: string; before: string; after: string }> } {
768
+ const entry = this.vault.get(entryId);
769
+ if (!entry) return { enriched: false, changes: [] };
770
+
771
+ const changes: Array<{ field: string; before: string; after: string }> = [];
772
+ const updates: Partial<
773
+ Pick<IntelligenceEntry, 'title' | 'description' | 'tags' | 'severity' | 'type'>
774
+ > = {};
775
+
776
+ // Auto-capitalize title
777
+ if (entry.title.length > 0 && entry.title[0] !== entry.title[0].toUpperCase()) {
778
+ const capitalized = entry.title[0].toUpperCase() + entry.title.slice(1);
779
+ changes.push({ field: 'title', before: entry.title, after: capitalized });
780
+ updates.title = capitalized;
781
+ }
782
+
783
+ // Normalize tags: lowercase, trim, dedup
784
+ const normalizedTags = [...new Set(entry.tags.map((t) => t.toLowerCase().trim()))];
785
+ const tagsChanged =
786
+ normalizedTags.length !== entry.tags.length ||
787
+ normalizedTags.some((t, i) => t !== entry.tags[i]);
788
+ if (tagsChanged) {
789
+ changes.push({
790
+ field: 'tags',
791
+ before: JSON.stringify(entry.tags),
792
+ after: JSON.stringify(normalizedTags),
793
+ });
794
+ updates.tags = normalizedTags;
795
+ }
796
+
797
+ // Infer severity from keywords if currently 'suggestion'
798
+ if (entry.severity === 'suggestion') {
799
+ const text = (entry.title + ' ' + entry.description).toLowerCase();
800
+ const criticalKeywords = ['never', 'must not', 'critical', 'security', 'vulnerability'];
801
+ const warningKeywords = ['avoid', 'should not', 'deprecated', 'careful', 'warning'];
802
+ if (criticalKeywords.some((k) => text.includes(k))) {
803
+ changes.push({ field: 'severity', before: entry.severity, after: 'critical' });
804
+ updates.severity = 'critical';
805
+ } else if (warningKeywords.some((k) => text.includes(k))) {
806
+ changes.push({ field: 'severity', before: entry.severity, after: 'warning' });
807
+ updates.severity = 'warning';
808
+ }
809
+ }
810
+
811
+ // Infer type from title patterns
812
+ if (entry.type === 'pattern') {
813
+ const titleLower = entry.title.toLowerCase();
814
+ if (
815
+ titleLower.startsWith('avoid') ||
816
+ titleLower.startsWith('never') ||
817
+ titleLower.startsWith("don't") ||
818
+ titleLower.startsWith('do not')
819
+ ) {
820
+ changes.push({ field: 'type', before: entry.type, after: 'anti-pattern' });
821
+ updates.type = 'anti-pattern';
822
+ }
823
+ }
824
+
825
+ // Trim whitespace from description
826
+ const trimmed = entry.description.trim();
827
+ if (trimmed !== entry.description) {
828
+ changes.push({ field: 'description', before: entry.description, after: trimmed });
829
+ updates.description = trimmed;
830
+ }
831
+
832
+ if (changes.length === 0) {
833
+ return { enriched: false, changes: [] };
834
+ }
835
+
836
+ // Apply updates
837
+ this.vault.update(entryId, updates);
838
+
839
+ // Record snapshot
840
+ this.recordSnapshot(entryId, 'curator', 'Metadata enrichment');
841
+
842
+ // Log change
843
+ this.logChange(
844
+ 'enrich_metadata',
845
+ entryId,
846
+ JSON.stringify(changes.map((c) => c.field)),
847
+ JSON.stringify(changes.map((c) => c.after)),
848
+ 'Rule-based metadata enrichment',
849
+ );
850
+
851
+ return { enriched: true, changes };
852
+ }
853
+
606
854
  // ─── Private Helpers ────────────────────────────────────────────
607
855
 
608
856
  private buildVocabulary(entries: IntelligenceEntry[]): Map<string, number> {
609
857
  const docCount = entries.length;
610
858
  const termDocFreq = new Map<string, number>();
611
859
  for (const entry of entries) {
612
- const text = [entry.title, entry.description, entry.context ?? '', entry.tags.join(' ')].join(' ');
860
+ const text = [entry.title, entry.description, entry.context ?? '', entry.tags.join(' ')].join(
861
+ ' ',
862
+ );
613
863
  const tokens = new Set(tokenize(text));
614
864
  for (const token of tokens) {
615
865
  termDocFreq.set(token, (termDocFreq.get(token) ?? 0) + 1);