@soleri/core 9.2.0 → 9.3.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 (298) hide show
  1. package/data/flows/build.flow.yaml +8 -9
  2. package/data/flows/deliver.flow.yaml +9 -10
  3. package/data/flows/design.flow.yaml +3 -4
  4. package/data/flows/enhance.flow.yaml +5 -6
  5. package/data/flows/explore.flow.yaml +3 -4
  6. package/data/flows/fix.flow.yaml +5 -6
  7. package/data/flows/plan.flow.yaml +4 -5
  8. package/data/flows/review.flow.yaml +3 -4
  9. package/dist/curator/curator.d.ts.map +1 -1
  10. package/dist/curator/curator.js +98 -22
  11. package/dist/curator/curator.js.map +1 -1
  12. package/dist/engine/bin/soleri-engine.js.map +1 -1
  13. package/dist/engine/module-manifest.d.ts.map +1 -1
  14. package/dist/engine/module-manifest.js +21 -1
  15. package/dist/engine/module-manifest.js.map +1 -1
  16. package/dist/engine/register-engine.d.ts.map +1 -1
  17. package/dist/engine/register-engine.js +25 -1
  18. package/dist/engine/register-engine.js.map +1 -1
  19. package/dist/flows/gate-evaluator.js.map +1 -1
  20. package/dist/operator/operator-profile.d.ts.map +1 -1
  21. package/dist/operator/operator-profile.js +11 -5
  22. package/dist/operator/operator-profile.js.map +1 -1
  23. package/dist/operator/operator-signals.d.ts.map +1 -1
  24. package/dist/operator/operator-signals.js.map +1 -1
  25. package/dist/planning/evidence-collector.js.map +1 -1
  26. package/dist/planning/gap-passes.d.ts.map +1 -1
  27. package/dist/planning/gap-passes.js +23 -6
  28. package/dist/planning/gap-passes.js.map +1 -1
  29. package/dist/planning/gap-patterns.d.ts.map +1 -1
  30. package/dist/planning/gap-patterns.js +57 -11
  31. package/dist/planning/gap-patterns.js.map +1 -1
  32. package/dist/planning/github-projection.d.ts.map +1 -1
  33. package/dist/planning/github-projection.js +39 -20
  34. package/dist/planning/github-projection.js.map +1 -1
  35. package/dist/planning/impact-analyzer.d.ts.map +1 -1
  36. package/dist/planning/impact-analyzer.js +20 -18
  37. package/dist/planning/impact-analyzer.js.map +1 -1
  38. package/dist/planning/plan-lifecycle.d.ts.map +1 -1
  39. package/dist/planning/plan-lifecycle.js +22 -9
  40. package/dist/planning/plan-lifecycle.js.map +1 -1
  41. package/dist/planning/planner.d.ts.map +1 -1
  42. package/dist/planning/planner.js +60 -17
  43. package/dist/planning/planner.js.map +1 -1
  44. package/dist/planning/rationalization-detector.d.ts.map +1 -1
  45. package/dist/planning/rationalization-detector.js.map +1 -1
  46. package/dist/planning/reconciliation-engine.d.ts.map +1 -1
  47. package/dist/planning/reconciliation-engine.js.map +1 -1
  48. package/dist/planning/task-verifier.d.ts.map +1 -1
  49. package/dist/planning/task-verifier.js +14 -6
  50. package/dist/planning/task-verifier.js.map +1 -1
  51. package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
  52. package/dist/runtime/admin-setup-ops.js +2 -1
  53. package/dist/runtime/admin-setup-ops.js.map +1 -1
  54. package/dist/runtime/branching-ops.d.ts +12 -0
  55. package/dist/runtime/branching-ops.d.ts.map +1 -0
  56. package/dist/runtime/branching-ops.js +100 -0
  57. package/dist/runtime/branching-ops.js.map +1 -0
  58. package/dist/runtime/context-health.d.ts.map +1 -1
  59. package/dist/runtime/context-health.js.map +1 -1
  60. package/dist/runtime/facades/branching-facade.d.ts +7 -0
  61. package/dist/runtime/facades/branching-facade.d.ts.map +1 -0
  62. package/dist/runtime/facades/branching-facade.js +8 -0
  63. package/dist/runtime/facades/branching-facade.js.map +1 -0
  64. package/dist/runtime/facades/chat-service-ops.d.ts.map +1 -1
  65. package/dist/runtime/facades/chat-service-ops.js +3 -1
  66. package/dist/runtime/facades/chat-service-ops.js.map +1 -1
  67. package/dist/runtime/facades/chat-transport-ops.d.ts.map +1 -1
  68. package/dist/runtime/facades/chat-transport-ops.js.map +1 -1
  69. package/dist/runtime/facades/index.d.ts.map +1 -1
  70. package/dist/runtime/facades/index.js +42 -0
  71. package/dist/runtime/facades/index.js.map +1 -1
  72. package/dist/runtime/facades/intake-facade.d.ts +9 -0
  73. package/dist/runtime/facades/intake-facade.d.ts.map +1 -0
  74. package/dist/runtime/facades/intake-facade.js +11 -0
  75. package/dist/runtime/facades/intake-facade.js.map +1 -0
  76. package/dist/runtime/facades/links-facade.d.ts +9 -0
  77. package/dist/runtime/facades/links-facade.d.ts.map +1 -0
  78. package/dist/runtime/facades/links-facade.js +10 -0
  79. package/dist/runtime/facades/links-facade.js.map +1 -0
  80. package/dist/runtime/facades/operator-facade.d.ts.map +1 -1
  81. package/dist/runtime/facades/operator-facade.js.map +1 -1
  82. package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
  83. package/dist/runtime/facades/plan-facade.js +4 -1
  84. package/dist/runtime/facades/plan-facade.js.map +1 -1
  85. package/dist/runtime/facades/tier-facade.d.ts +7 -0
  86. package/dist/runtime/facades/tier-facade.d.ts.map +1 -0
  87. package/dist/runtime/facades/tier-facade.js +8 -0
  88. package/dist/runtime/facades/tier-facade.js.map +1 -0
  89. package/dist/runtime/facades/vault-facade.d.ts +9 -1
  90. package/dist/runtime/facades/vault-facade.d.ts.map +1 -1
  91. package/dist/runtime/facades/vault-facade.js +44 -187
  92. package/dist/runtime/facades/vault-facade.js.map +1 -1
  93. package/dist/runtime/github-integration.d.ts.map +1 -1
  94. package/dist/runtime/github-integration.js +11 -4
  95. package/dist/runtime/github-integration.js.map +1 -1
  96. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  97. package/dist/runtime/orchestrate-ops.js +32 -10
  98. package/dist/runtime/orchestrate-ops.js.map +1 -1
  99. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  100. package/dist/runtime/planning-extra-ops.js.map +1 -1
  101. package/dist/runtime/runtime.d.ts.map +1 -1
  102. package/dist/runtime/runtime.js +3 -1
  103. package/dist/runtime/runtime.js.map +1 -1
  104. package/dist/runtime/session-briefing.d.ts.map +1 -1
  105. package/dist/runtime/session-briefing.js +5 -1
  106. package/dist/runtime/session-briefing.js.map +1 -1
  107. package/dist/runtime/tier-ops.d.ts +13 -0
  108. package/dist/runtime/tier-ops.d.ts.map +1 -0
  109. package/dist/runtime/tier-ops.js +110 -0
  110. package/dist/runtime/tier-ops.js.map +1 -0
  111. package/dist/skills/sync-skills.d.ts.map +1 -1
  112. package/dist/skills/sync-skills.js +1 -1
  113. package/dist/skills/sync-skills.js.map +1 -1
  114. package/dist/vault/linking.d.ts.map +1 -1
  115. package/dist/vault/linking.js +41 -5
  116. package/dist/vault/linking.js.map +1 -1
  117. package/dist/vault/vault-entries.d.ts.map +1 -1
  118. package/dist/vault/vault-entries.js +68 -26
  119. package/dist/vault/vault-entries.js.map +1 -1
  120. package/dist/vault/vault-maintenance.d.ts.map +1 -1
  121. package/dist/vault/vault-maintenance.js +6 -2
  122. package/dist/vault/vault-maintenance.js.map +1 -1
  123. package/dist/vault/vault-markdown-sync.d.ts.map +1 -1
  124. package/dist/vault/vault-markdown-sync.js.map +1 -1
  125. package/dist/vault/vault-memories.d.ts.map +1 -1
  126. package/dist/vault/vault-memories.js +3 -1
  127. package/dist/vault/vault-memories.js.map +1 -1
  128. package/dist/vault/vault-schema.js +36 -10
  129. package/dist/vault/vault-schema.js.map +1 -1
  130. package/dist/vault/vault.d.ts.map +1 -1
  131. package/dist/vault/vault.js +5 -1
  132. package/dist/vault/vault.js.map +1 -1
  133. package/package.json +7 -7
  134. package/src/agency/agency-manager.test.ts +60 -40
  135. package/src/agency/default-rules.test.ts +17 -9
  136. package/src/capabilities/registry.test.ts +2 -12
  137. package/src/chat/agent-loop.test.ts +33 -43
  138. package/src/chat/mcp-bridge.test.ts +7 -2
  139. package/src/claudemd/inject.test.ts +2 -12
  140. package/src/context/context-engine.test.ts +96 -51
  141. package/src/control/intent-router.test.ts +3 -3
  142. package/src/curator/classifier.test.ts +14 -8
  143. package/src/curator/contradiction-detector.test.ts +30 -5
  144. package/src/curator/curator.ts +278 -56
  145. package/src/curator/duplicate-detector.test.ts +77 -15
  146. package/src/curator/quality-gate.test.ts +71 -31
  147. package/src/curator/tag-manager.test.ts +12 -4
  148. package/src/domain-packs/knowledge-installer.test.ts +2 -10
  149. package/src/domain-packs/token-resolver.test.ts +1 -3
  150. package/src/domain-packs/types.test.ts +16 -2
  151. package/src/enforcement/registry.test.ts +2 -8
  152. package/src/engine/bin/soleri-engine.ts +3 -1
  153. package/src/engine/module-manifest.test.ts +5 -4
  154. package/src/engine/module-manifest.ts +21 -1
  155. package/src/engine/register-engine.test.ts +6 -1
  156. package/src/engine/register-engine.ts +26 -3
  157. package/src/errors/classify.test.ts +6 -2
  158. package/src/errors/retry.test.ts +1 -4
  159. package/src/facades/facade-factory.test.ts +110 -64
  160. package/src/flows/epilogue.test.ts +16 -10
  161. package/src/flows/gate-evaluator.test.ts +12 -6
  162. package/src/flows/gate-evaluator.ts +1 -3
  163. package/src/governance/governance.test.ts +137 -21
  164. package/src/health/health-registry.test.ts +8 -1
  165. package/src/intake/content-classifier.test.ts +121 -51
  166. package/src/intake/dedup-gate.test.ts +38 -22
  167. package/src/intake/intake-pipeline.test.ts +5 -3
  168. package/src/intake/text-ingester.test.ts +26 -20
  169. package/src/llm/key-pool.test.ts +1 -3
  170. package/src/llm/llm-client.test.ts +1 -4
  171. package/src/llm/oauth-discovery.test.ts +16 -16
  172. package/src/llm/utils.test.ts +62 -18
  173. package/src/logging/logger.test.ts +4 -1
  174. package/src/loop/loop-manager.test.ts +2 -6
  175. package/src/migrations/migration-runner.edge-cases.test.ts +2 -7
  176. package/src/operator/operator-profile-extended.test.ts +15 -5
  177. package/src/operator/operator-profile.test.ts +26 -8
  178. package/src/operator/operator-profile.ts +38 -22
  179. package/src/operator/operator-signals-extended.test.ts +35 -23
  180. package/src/operator/operator-signals.test.ts +6 -10
  181. package/src/operator/operator-signals.ts +2 -1
  182. package/src/operator/prompts/hook-precompact-operator-dispatch.md +10 -6
  183. package/src/operator/prompts/subagent-soft-signal-extractor.md +5 -0
  184. package/src/operator/prompts/subagent-synthesis-cognition.md +19 -10
  185. package/src/operator/prompts/subagent-synthesis-communication.md +13 -7
  186. package/src/operator/prompts/subagent-synthesis-technical.md +19 -9
  187. package/src/operator/prompts/subagent-synthesis-trust.md +27 -21
  188. package/src/persona/defaults.test.ts +1 -5
  189. package/src/planning/evidence-collector.test.ts +147 -38
  190. package/src/planning/evidence-collector.ts +1 -4
  191. package/src/planning/gap-analysis-alternatives.test.ts +41 -11
  192. package/src/planning/gap-passes.test.ts +215 -33
  193. package/src/planning/gap-passes.ts +115 -46
  194. package/src/planning/gap-patterns.test.ts +87 -13
  195. package/src/planning/gap-patterns.ts +114 -31
  196. package/src/planning/github-projection.test.ts +6 -1
  197. package/src/planning/github-projection.ts +41 -20
  198. package/src/planning/impact-analyzer.test.ts +10 -23
  199. package/src/planning/impact-analyzer.ts +33 -46
  200. package/src/planning/plan-lifecycle.test.ts +103 -36
  201. package/src/planning/plan-lifecycle.ts +49 -18
  202. package/src/planning/planner.test.ts +12 -2
  203. package/src/planning/planner.ts +198 -58
  204. package/src/planning/rationalization-detector.test.ts +5 -20
  205. package/src/planning/rationalization-detector.ts +14 -16
  206. package/src/planning/reconciliation-engine.test.ts +20 -3
  207. package/src/planning/reconciliation-engine.ts +1 -2
  208. package/src/planning/task-verifier.test.ts +59 -27
  209. package/src/planning/task-verifier.ts +15 -9
  210. package/src/playbooks/playbook-executor.test.ts +1 -3
  211. package/src/plugins/plugin-loader.test.ts +19 -14
  212. package/src/plugins/plugin-registry.test.ts +45 -33
  213. package/src/project/project-registry.test.ts +23 -12
  214. package/src/prompts/template-manager.test.ts +4 -1
  215. package/src/queue/job-queue.test.ts +10 -14
  216. package/src/runtime/admin-extra-ops.test.ts +5 -19
  217. package/src/runtime/admin-ops.test.ts +1 -3
  218. package/src/runtime/admin-setup-ops.test.ts +3 -4
  219. package/src/runtime/admin-setup-ops.ts +9 -2
  220. package/src/runtime/archive-ops.test.ts +4 -1
  221. package/src/runtime/branching-ops.test.ts +144 -0
  222. package/src/runtime/branching-ops.ts +107 -0
  223. package/src/runtime/capture-ops.test.ts +7 -21
  224. package/src/runtime/chain-ops.test.ts +16 -6
  225. package/src/runtime/claude-md-helpers.test.ts +1 -3
  226. package/src/runtime/context-health.test.ts +1 -3
  227. package/src/runtime/context-health.ts +1 -3
  228. package/src/runtime/curator-extra-ops.test.ts +3 -1
  229. package/src/runtime/domain-ops.test.ts +46 -36
  230. package/src/runtime/facades/admin-facade.test.ts +1 -4
  231. package/src/runtime/facades/archive-facade.test.ts +21 -7
  232. package/src/runtime/facades/brain-facade.test.ts +176 -72
  233. package/src/runtime/facades/branching-facade.test.ts +43 -0
  234. package/src/runtime/facades/branching-facade.ts +11 -0
  235. package/src/runtime/facades/chat-facade.test.ts +81 -28
  236. package/src/runtime/facades/chat-service-ops.test.ts +178 -73
  237. package/src/runtime/facades/chat-service-ops.ts +3 -1
  238. package/src/runtime/facades/chat-session-ops.test.ts +25 -10
  239. package/src/runtime/facades/chat-transport-ops.test.ts +101 -34
  240. package/src/runtime/facades/chat-transport-ops.ts +0 -1
  241. package/src/runtime/facades/context-facade.test.ts +19 -4
  242. package/src/runtime/facades/control-facade.test.ts +3 -3
  243. package/src/runtime/facades/index.ts +42 -0
  244. package/src/runtime/facades/intake-facade.test.ts +215 -0
  245. package/src/runtime/facades/intake-facade.ts +14 -0
  246. package/src/runtime/facades/links-facade.test.ts +203 -0
  247. package/src/runtime/facades/links-facade.ts +13 -0
  248. package/src/runtime/facades/loop-facade.test.ts +22 -5
  249. package/src/runtime/facades/memory-facade.test.ts +19 -5
  250. package/src/runtime/facades/operator-facade.test.ts +17 -4
  251. package/src/runtime/facades/operator-facade.ts +11 -3
  252. package/src/runtime/facades/orchestrate-facade.test.ts +7 -1
  253. package/src/runtime/facades/plan-facade.test.ts +29 -12
  254. package/src/runtime/facades/plan-facade.ts +7 -2
  255. package/src/runtime/facades/tier-facade.test.ts +47 -0
  256. package/src/runtime/facades/tier-facade.ts +11 -0
  257. package/src/runtime/facades/vault-facade.test.ts +174 -242
  258. package/src/runtime/facades/vault-facade.ts +55 -199
  259. package/src/runtime/github-integration.ts +11 -8
  260. package/src/runtime/grading-ops.test.ts +39 -8
  261. package/src/runtime/intake-ops.test.ts +69 -16
  262. package/src/runtime/loop-ops.test.ts +16 -6
  263. package/src/runtime/memory-cross-project-ops.test.ts +25 -14
  264. package/src/runtime/orchestrate-ops.ts +54 -27
  265. package/src/runtime/pack-ops.test.ts +23 -6
  266. package/src/runtime/planning-extra-ops.test.ts +17 -7
  267. package/src/runtime/planning-extra-ops.ts +3 -1
  268. package/src/runtime/playbook-ops.test.ts +26 -3
  269. package/src/runtime/plugin-ops.test.ts +83 -25
  270. package/src/runtime/project-ops.test.ts +26 -6
  271. package/src/runtime/runtime.ts +3 -1
  272. package/src/runtime/session-briefing.test.ts +183 -54
  273. package/src/runtime/session-briefing.ts +8 -2
  274. package/src/runtime/sync-ops.test.ts +3 -12
  275. package/src/runtime/telemetry-ops.test.ts +31 -6
  276. package/src/runtime/tier-ops.test.ts +159 -0
  277. package/src/runtime/tier-ops.ts +119 -0
  278. package/src/runtime/vault-extra-ops.test.ts +32 -8
  279. package/src/runtime/vault-sharing-ops.test.ts +1 -4
  280. package/src/skills/sync-skills.ts +2 -12
  281. package/src/transport/ws-server.test.ts +7 -4
  282. package/src/vault/__tests__/vault-characterization.test.ts +492 -81
  283. package/src/vault/linking.test.ts +50 -17
  284. package/src/vault/linking.ts +48 -7
  285. package/src/vault/obsidian-sync.test.ts +6 -3
  286. package/src/vault/scope-detector.test.ts +1 -3
  287. package/src/vault/vault-branching.test.ts +9 -7
  288. package/src/vault/vault-entries.ts +209 -65
  289. package/src/vault/vault-maintenance.ts +7 -12
  290. package/src/vault/vault-manager.test.ts +10 -10
  291. package/src/vault/vault-markdown-sync.ts +4 -1
  292. package/src/vault/vault-memories.ts +7 -7
  293. package/src/vault/vault-schema.ts +72 -15
  294. package/src/vault/vault.ts +55 -9
  295. package/src/brain/strength-scorer.ts +0 -404
  296. package/src/engine/index.ts +0 -21
  297. package/src/persona/index.ts +0 -9
  298. package/src/vault/vault-interfaces.ts +0 -56
@@ -16,8 +16,15 @@ import type {
16
16
  HealthAuditResult,
17
17
  } from './types.js';
18
18
 
19
- import { detectDuplicates as detectDuplicatesPure, DEFAULT_DUPLICATE_THRESHOLD } from './duplicate-detector.js';
20
- import { findContradictions, DEFAULT_CONTRADICTION_THRESHOLD, type ContradictionCandidate } from './contradiction-detector.js';
19
+ import {
20
+ detectDuplicates as detectDuplicatesPure,
21
+ DEFAULT_DUPLICATE_THRESHOLD,
22
+ } from './duplicate-detector.js';
23
+ import {
24
+ findContradictions,
25
+ DEFAULT_CONTRADICTION_THRESHOLD,
26
+ type ContradictionCandidate,
27
+ } from './contradiction-detector.js';
21
28
  import {
22
29
  normalizeTag as normalizeTagPure,
23
30
  normalizeAndDedup,
@@ -53,13 +60,20 @@ export class Curator {
53
60
  const p = this.provider;
54
61
  return {
55
62
  getAlias(lower: string) {
56
- return p.get<{ canonical: string }>('SELECT canonical FROM curator_tag_alias WHERE alias = ?', [lower])?.canonical ?? null;
63
+ return (
64
+ p.get<{ canonical: string }>('SELECT canonical FROM curator_tag_alias WHERE alias = ?', [
65
+ lower,
66
+ ])?.canonical ?? null
67
+ );
57
68
  },
58
69
  insertCanonical(tag: string) {
59
70
  p.run('INSERT OR IGNORE INTO curator_tag_canonical (tag) VALUES (?)', [tag]);
60
71
  },
61
72
  upsertAlias(alias: string, canonical: string) {
62
- p.run('INSERT OR REPLACE INTO curator_tag_alias (alias, canonical) VALUES (?, ?)', [alias, canonical]);
73
+ p.run('INSERT OR REPLACE INTO curator_tag_alias (alias, canonical) VALUES (?, ?)', [
74
+ alias,
75
+ canonical,
76
+ ]);
63
77
  },
64
78
  getCanonicalRows() {
65
79
  return p.all<{ tag: string; description: string | null; alias_count: number }>(
@@ -67,7 +81,11 @@ export class Curator {
67
81
  );
68
82
  },
69
83
  countTagUsage(tag: string) {
70
- return p.get<{ count: number }>('SELECT COUNT(*) as count FROM entries WHERE tags LIKE ?', [`%"${tag}"%`])?.count ?? 0;
84
+ return (
85
+ p.get<{ count: number }>('SELECT COUNT(*) as count FROM entries WHERE tags LIKE ?', [
86
+ `%"${tag}"%`,
87
+ ])?.count ?? 0
88
+ );
71
89
  },
72
90
  };
73
91
  }
@@ -76,34 +94,59 @@ export class Curator {
76
94
 
77
95
  getStatus(): CuratorStatus {
78
96
  const tableCount = (table: string): number =>
79
- (this.provider.get<{ count: number }>(`SELECT COUNT(*) as count FROM ${table}`) ?? { count: 0 }).count;
97
+ (
98
+ this.provider.get<{ count: number }>(`SELECT COUNT(*) as count FROM ${table}`) ?? {
99
+ count: 0,
100
+ }
101
+ ).count;
80
102
  const lastGroomed = this.provider.get<{ ts: number | null }>(
81
103
  'SELECT MAX(last_groomed_at) as ts FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
82
104
  ) ?? { ts: null };
83
105
  return {
84
106
  initialized: true,
85
- tables: { entry_state: tableCount('curator_entry_state'), tag_canonical: tableCount('curator_tag_canonical'), tag_alias: tableCount('curator_tag_alias'), changelog: tableCount('curator_changelog'), contradictions: tableCount('curator_contradictions') },
107
+ tables: {
108
+ entry_state: tableCount('curator_entry_state'),
109
+ tag_canonical: tableCount('curator_tag_canonical'),
110
+ tag_alias: tableCount('curator_tag_alias'),
111
+ changelog: tableCount('curator_changelog'),
112
+ contradictions: tableCount('curator_contradictions'),
113
+ },
86
114
  lastGroomedAt: lastGroomed.ts,
87
115
  };
88
116
  }
89
117
 
90
118
  // ─── Tags (delegates to tag-manager) ──────────────────────────
91
119
 
92
- normalizeTag(tag: string): TagNormalizationResult { return normalizeTagPure(tag, this.tagStore); }
120
+ normalizeTag(tag: string): TagNormalizationResult {
121
+ return normalizeTagPure(tag, this.tagStore);
122
+ }
93
123
 
94
124
  normalizeTags(entryId: string): TagNormalizationResult[] {
95
125
  const entry = this.vault.get(entryId);
96
126
  if (!entry) return [];
97
127
  const { results, dedupedTags, changed } = normalizeAndDedup(entry.tags, this.tagStore);
98
128
  if (changed) {
99
- this.provider.run('UPDATE entries SET tags = ?, updated_at = unixepoch() WHERE id = ?', [JSON.stringify(dedupedTags), entryId]);
100
- this.logChange('normalize_tags', entryId, JSON.stringify(entry.tags), JSON.stringify(dedupedTags), 'Tag normalization');
129
+ this.provider.run('UPDATE entries SET tags = ?, updated_at = unixepoch() WHERE id = ?', [
130
+ JSON.stringify(dedupedTags),
131
+ entryId,
132
+ ]);
133
+ this.logChange(
134
+ 'normalize_tags',
135
+ entryId,
136
+ JSON.stringify(entry.tags),
137
+ JSON.stringify(dedupedTags),
138
+ 'Tag normalization',
139
+ );
101
140
  }
102
141
  return results;
103
142
  }
104
143
 
105
- addTagAlias(alias: string, canonical: string): void { addTagAliasPure(alias, canonical, this.tagStore); }
106
- getCanonicalTags(): CanonicalTag[] { return getCanonicalTagsPure(this.tagStore); }
144
+ addTagAlias(alias: string, canonical: string): void {
145
+ addTagAliasPure(alias, canonical, this.tagStore);
146
+ }
147
+ getCanonicalTags(): CanonicalTag[] {
148
+ return getCanonicalTagsPure(this.tagStore);
149
+ }
107
150
 
108
151
  // ─── Duplicates (delegates to duplicate-detector) ─────────────
109
152
 
@@ -114,24 +157,45 @@ export class Curator {
114
157
  // ─── Contradictions (delegates to contradiction-detector) ─────
115
158
 
116
159
  detectContradictions(threshold?: number): Contradiction[] {
117
- const searchFn = (title: string) => this.vault.search(title, { type: 'pattern', limit: 20 }).map((r) => r.entry);
118
- return this.persistContradictions(findContradictions(this.vault.list({ limit: 100000 }), threshold, searchFn));
160
+ const searchFn = (title: string) =>
161
+ this.vault.search(title, { type: 'pattern', limit: 20 }).map((r) => r.entry);
162
+ return this.persistContradictions(
163
+ findContradictions(this.vault.list({ limit: 100000 }), threshold, searchFn),
164
+ );
119
165
  }
120
166
 
121
167
  getContradictions(status?: ContradictionStatus): Contradiction[] {
122
- const query = status ? 'SELECT * FROM curator_contradictions WHERE status = ? ORDER BY similarity DESC' : 'SELECT * FROM curator_contradictions ORDER BY similarity DESC';
123
- return this.provider.all<Record<string, unknown>>(query, status ? [status] : undefined).map((r) => this.rowToContradiction(r));
168
+ const query = status
169
+ ? 'SELECT * FROM curator_contradictions WHERE status = ? ORDER BY similarity DESC'
170
+ : 'SELECT * FROM curator_contradictions ORDER BY similarity DESC';
171
+ return this.provider
172
+ .all<Record<string, unknown>>(query, status ? [status] : undefined)
173
+ .map((r) => this.rowToContradiction(r));
124
174
  }
125
175
 
126
176
  resolveContradiction(id: number, resolution: 'resolved' | 'dismissed'): Contradiction | null {
127
- this.provider.run('UPDATE curator_contradictions SET status = ?, resolved_at = unixepoch() WHERE id = ?', [resolution, id]);
128
- const row = this.provider.get<Record<string, unknown>>('SELECT * FROM curator_contradictions WHERE id = ?', [id]);
177
+ this.provider.run(
178
+ 'UPDATE curator_contradictions SET status = ?, resolved_at = unixepoch() WHERE id = ?',
179
+ [resolution, id],
180
+ );
181
+ const row = this.provider.get<Record<string, unknown>>(
182
+ 'SELECT * FROM curator_contradictions WHERE id = ?',
183
+ [id],
184
+ );
129
185
  return row ? this.rowToContradiction(row) : null;
130
186
  }
131
187
 
132
- async detectContradictionsHybrid(threshold?: number): Promise<{ contradictions: Contradiction[]; method: 'tfidf-only' }> {
133
- const searchFn = (title: string) => this.vault.search(title, { type: 'pattern', limit: 20 }).map((r) => r.entry);
134
- return { contradictions: this.persistContradictions(findContradictions(this.vault.list({ limit: 100000 }), threshold, searchFn)), method: 'tfidf-only' };
188
+ async detectContradictionsHybrid(
189
+ threshold?: number,
190
+ ): Promise<{ contradictions: Contradiction[]; method: 'tfidf-only' }> {
191
+ const searchFn = (title: string) =>
192
+ this.vault.search(title, { type: 'pattern', limit: 20 }).map((r) => r.entry);
193
+ return {
194
+ contradictions: this.persistContradictions(
195
+ findContradictions(this.vault.list({ limit: 100000 }), threshold, searchFn),
196
+ ),
197
+ method: 'tfidf-only',
198
+ };
135
199
  }
136
200
 
137
201
  // ─── Grooming ─────────────────────────────────────────────────
@@ -140,11 +204,17 @@ export class Curator {
140
204
  const entry = this.vault.get(entryId);
141
205
  if (!entry) return null;
142
206
  const tagsNormalized = this.normalizeTags(entryId);
143
- const row = this.provider.get<{ updated_at: number }>('SELECT updated_at FROM entries WHERE id = ?', [entryId]);
207
+ const row = this.provider.get<{ updated_at: number }>(
208
+ 'SELECT updated_at FROM entries WHERE id = ?',
209
+ [entryId],
210
+ );
144
211
  const now = Math.floor(Date.now() / 1000);
145
212
  const stale = row ? now - row.updated_at > DEFAULT_STALE_DAYS * 86400 : false;
146
213
  const status = stale ? 'stale' : 'active';
147
- this.provider.run(`INSERT INTO curator_entry_state (entry_id, status, last_groomed_at) VALUES (?, ?, unixepoch()) ON CONFLICT(entry_id) DO UPDATE SET status = excluded.status, last_groomed_at = unixepoch()`, [entryId, status]);
214
+ this.provider.run(
215
+ `INSERT INTO curator_entry_state (entry_id, status, last_groomed_at) VALUES (?, ?, unixepoch()) ON CONFLICT(entry_id) DO UPDATE SET status = excluded.status, last_groomed_at = unixepoch()`,
216
+ [entryId, status],
217
+ );
148
218
  this.logChange('groom', entryId, null, `status=${status}`, 'Routine grooming');
149
219
  return { entryId, tagsNormalized, stale, lastGroomedAt: now };
150
220
  }
@@ -152,12 +222,22 @@ export class Curator {
152
222
  groomAll(): GroomAllResult {
153
223
  const start = Date.now();
154
224
  const entries = this.vault.list({ limit: 100000 });
155
- let tagsNormalized = 0, staleCount = 0;
225
+ let tagsNormalized = 0,
226
+ staleCount = 0;
156
227
  for (const entry of entries) {
157
228
  const result = this.groomEntry(entry.id);
158
- if (result) { tagsNormalized += result.tagsNormalized.filter((t) => t.wasAliased).length; if (result.stale) staleCount++; }
229
+ if (result) {
230
+ tagsNormalized += result.tagsNormalized.filter((t) => t.wasAliased).length;
231
+ if (result.stale) staleCount++;
232
+ }
159
233
  }
160
- return { totalEntries: entries.length, groomedCount: entries.length, tagsNormalized, staleCount, durationMs: Date.now() - start };
234
+ return {
235
+ totalEntries: entries.length,
236
+ groomedCount: entries.length,
237
+ tagsNormalized,
238
+ staleCount,
239
+ durationMs: Date.now() - start,
240
+ };
161
241
  }
162
242
 
163
243
  // ─── Consolidation ───────────────────────────────────────────
@@ -167,17 +247,30 @@ export class Curator {
167
247
  const dryRun = options?.dryRun ?? true;
168
248
  const staleDaysThreshold = options?.staleDaysThreshold ?? DEFAULT_STALE_DAYS;
169
249
  const duplicateThreshold = options?.duplicateThreshold ?? DEFAULT_DUPLICATE_THRESHOLD;
170
- const contradictionThreshold = options?.contradictionThreshold ?? DEFAULT_CONTRADICTION_THRESHOLD;
250
+ const contradictionThreshold =
251
+ options?.contradictionThreshold ?? DEFAULT_CONTRADICTION_THRESHOLD;
171
252
  const duplicates = this.detectDuplicates(undefined, duplicateThreshold);
172
253
  const now = Math.floor(Date.now() / 1000);
173
- const staleRows = this.provider.all<{ id: string }>('SELECT id FROM entries WHERE updated_at < ?', [now - staleDaysThreshold * 86400]);
254
+ const staleRows = this.provider.all<{ id: string }>(
255
+ 'SELECT id FROM entries WHERE updated_at < ?',
256
+ [now - staleDaysThreshold * 86400],
257
+ );
174
258
  const staleEntries = staleRows.map((r) => r.id);
175
259
  const contradictions = this.detectContradictions(contradictionThreshold);
176
260
  let mutations = 0;
177
261
  if (!dryRun) {
178
262
  for (const entryId of staleEntries) {
179
- this.provider.run(`INSERT INTO curator_entry_state (entry_id, status, last_groomed_at) VALUES (?, 'archived', unixepoch()) ON CONFLICT(entry_id) DO UPDATE SET status = 'archived', last_groomed_at = unixepoch()`, [entryId]);
180
- this.logChange('archive', entryId, 'active', 'archived', 'Stale entry archived during consolidation');
263
+ this.provider.run(
264
+ `INSERT INTO curator_entry_state (entry_id, status, last_groomed_at) VALUES (?, 'archived', unixepoch()) ON CONFLICT(entry_id) DO UPDATE SET status = 'archived', last_groomed_at = unixepoch()`,
265
+ [entryId],
266
+ );
267
+ this.logChange(
268
+ 'archive',
269
+ entryId,
270
+ 'active',
271
+ 'archived',
272
+ 'Stale entry archived during consolidation',
273
+ );
181
274
  mutations++;
182
275
  }
183
276
  const removed = new Set<string>();
@@ -185,20 +278,38 @@ export class Curator {
185
278
  for (const match of result.matches) {
186
279
  if (!removed.has(match.entryId) && match.entryId !== result.entryId) {
187
280
  this.vault.remove(match.entryId);
188
- this.logChange('remove_duplicate', match.entryId, null, null, `Duplicate of ${result.entryId} (similarity: ${match.similarity.toFixed(3)})`);
281
+ this.logChange(
282
+ 'remove_duplicate',
283
+ match.entryId,
284
+ null,
285
+ null,
286
+ `Duplicate of ${result.entryId} (similarity: ${match.similarity.toFixed(3)})`,
287
+ );
189
288
  removed.add(match.entryId);
190
289
  mutations++;
191
290
  }
192
291
  }
193
292
  }
194
293
  }
195
- return { dryRun, duplicates, staleEntries, contradictions, mutations, durationMs: Date.now() - start };
294
+ return {
295
+ dryRun,
296
+ duplicates,
297
+ staleEntries,
298
+ contradictions,
299
+ mutations,
300
+ durationMs: Date.now() - start,
301
+ };
196
302
  }
197
303
 
198
304
  // ─── Changelog ────────────────────────────────────────────────
199
305
 
200
306
  getEntryHistory(entryId: string, limit?: number): ChangelogEntry[] {
201
- return this.provider.all<Record<string, unknown>>('SELECT * FROM curator_changelog WHERE entry_id = ? ORDER BY created_at DESC, id DESC LIMIT ?', [entryId, limit ?? 50]).map((r) => this.rowToChangelog(r));
307
+ return this.provider
308
+ .all<Record<string, unknown>>(
309
+ 'SELECT * FROM curator_changelog WHERE entry_id = ? ORDER BY created_at DESC, id DESC LIMIT ?',
310
+ [entryId, limit ?? 50],
311
+ )
312
+ .map((r) => this.rowToChangelog(r));
202
313
  }
203
314
 
204
315
  // ─── Health Audit (delegates to health-audit) ─────────────────
@@ -206,8 +317,19 @@ export class Curator {
206
317
  healthAudit(): HealthAuditResult {
207
318
  const entries = this.vault.list({ limit: 100000 });
208
319
  const dataProvider: HealthDataProvider = {
209
- getStaleCount: (threshold) => (this.provider.get<{ count: number }>('SELECT COUNT(*) as count FROM entries WHERE updated_at < ?', [threshold]) ?? { count: 0 }).count,
210
- getGroomedCount: () => (this.provider.get<{ count: number }>('SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL') ?? { count: 0 }).count,
320
+ getStaleCount: (threshold) =>
321
+ (
322
+ this.provider.get<{ count: number }>(
323
+ 'SELECT COUNT(*) as count FROM entries WHERE updated_at < ?',
324
+ [threshold],
325
+ ) ?? { count: 0 }
326
+ ).count,
327
+ getGroomedCount: () =>
328
+ (
329
+ this.provider.get<{ count: number }>(
330
+ 'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
331
+ ) ?? { count: 0 }
332
+ ).count,
211
333
  getDuplicates: () => this.detectDuplicates(),
212
334
  getOpenContradictions: () => this.getContradictions('open'),
213
335
  };
@@ -216,46 +338,115 @@ export class Curator {
216
338
 
217
339
  // ─── Entry History (Version Snapshots) ────────────────────────
218
340
 
219
- recordSnapshot(entryId: string, changedBy?: string, changeReason?: string): { recorded: boolean; historyId: number } {
341
+ recordSnapshot(
342
+ entryId: string,
343
+ changedBy?: string,
344
+ changeReason?: string,
345
+ ): { recorded: boolean; historyId: number } {
220
346
  const entry = this.vault.get(entryId);
221
347
  if (!entry) return { recorded: false, historyId: -1 };
222
- const result = this.provider.run('INSERT INTO curator_entry_history (entry_id, snapshot, changed_by, change_reason, created_at) VALUES (?, ?, ?, ?, unixepoch())', [entryId, JSON.stringify(entry), changedBy ?? 'system', changeReason ?? null]);
348
+ const result = this.provider.run(
349
+ 'INSERT INTO curator_entry_history (entry_id, snapshot, changed_by, change_reason, created_at) VALUES (?, ?, ?, ?, unixepoch())',
350
+ [entryId, JSON.stringify(entry), changedBy ?? 'system', changeReason ?? null],
351
+ );
223
352
  return { recorded: true, historyId: Number(result.lastInsertRowid) };
224
353
  }
225
354
 
226
- getVersionHistory(entryId: string): Array<{ historyId: number; entryId: string; snapshot: IntelligenceEntry; changedBy: string; changeReason: string | null; createdAt: number }> {
227
- return this.provider.all<Record<string, unknown>>('SELECT * FROM curator_entry_history WHERE entry_id = ? ORDER BY created_at ASC, id ASC', [entryId]).map((row) => ({
228
- historyId: row.id as number, entryId: row.entry_id as string, snapshot: JSON.parse(row.snapshot as string) as IntelligenceEntry, changedBy: row.changed_by as string, changeReason: (row.change_reason as string) ?? null, createdAt: row.created_at as number,
229
- }));
355
+ getVersionHistory(entryId: string): Array<{
356
+ historyId: number;
357
+ entryId: string;
358
+ snapshot: IntelligenceEntry;
359
+ changedBy: string;
360
+ changeReason: string | null;
361
+ createdAt: number;
362
+ }> {
363
+ return this.provider
364
+ .all<Record<string, unknown>>(
365
+ 'SELECT * FROM curator_entry_history WHERE entry_id = ? ORDER BY created_at ASC, id ASC',
366
+ [entryId],
367
+ )
368
+ .map((row) => ({
369
+ historyId: row.id as number,
370
+ entryId: row.entry_id as string,
371
+ snapshot: JSON.parse(row.snapshot as string) as IntelligenceEntry,
372
+ changedBy: row.changed_by as string,
373
+ changeReason: (row.change_reason as string) ?? null,
374
+ createdAt: row.created_at as number,
375
+ }));
230
376
  }
231
377
 
232
378
  // ─── Queue Stats ─────────────────────────────────────────────
233
379
 
234
- getQueueStats(): { totalEntries: number; groomedEntries: number; ungroomedEntries: number; staleEntries: number; freshEntries: number; avgDaysSinceGroom: number } {
380
+ getQueueStats(): {
381
+ totalEntries: number;
382
+ groomedEntries: number;
383
+ ungroomedEntries: number;
384
+ staleEntries: number;
385
+ freshEntries: number;
386
+ avgDaysSinceGroom: number;
387
+ } {
235
388
  const p = this.provider;
236
- const totalEntries = (p.get<{ count: number }>('SELECT COUNT(*) as count FROM entries') ?? { count: 0 }).count;
237
- const groomedEntries = (p.get<{ count: number }>('SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL') ?? { count: 0 }).count;
389
+ const totalEntries = (
390
+ p.get<{ count: number }>('SELECT COUNT(*) as count FROM entries') ?? { count: 0 }
391
+ ).count;
392
+ const groomedEntries = (
393
+ p.get<{ count: number }>(
394
+ 'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
395
+ ) ?? { count: 0 }
396
+ ).count;
238
397
  const now = Math.floor(Date.now() / 1000);
239
- const staleEntries = (p.get<{ count: number }>('SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL AND last_groomed_at < ?', [now - 30 * 86400]) ?? { count: 0 }).count;
240
- const freshEntries = (p.get<{ count: number }>('SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL AND last_groomed_at >= ?', [now - 7 * 86400]) ?? { count: 0 }).count;
398
+ const staleEntries = (
399
+ p.get<{ count: number }>(
400
+ 'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL AND last_groomed_at < ?',
401
+ [now - 30 * 86400],
402
+ ) ?? { count: 0 }
403
+ ).count;
404
+ const freshEntries = (
405
+ p.get<{ count: number }>(
406
+ 'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL AND last_groomed_at >= ?',
407
+ [now - 7 * 86400],
408
+ ) ?? { count: 0 }
409
+ ).count;
241
410
  let avgDaysSinceGroom = 0;
242
411
  if (groomedEntries > 0) {
243
- const totalSeconds = (p.get<{ total: number | null }>('SELECT SUM(? - last_groomed_at) as total FROM curator_entry_state WHERE last_groomed_at IS NOT NULL', [now]) ?? { total: 0 }).total ?? 0;
412
+ const totalSeconds =
413
+ (
414
+ p.get<{ total: number | null }>(
415
+ 'SELECT SUM(? - last_groomed_at) as total FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
416
+ [now],
417
+ ) ?? { total: 0 }
418
+ ).total ?? 0;
244
419
  avgDaysSinceGroom = Math.round((totalSeconds / groomedEntries / 86400) * 100) / 100;
245
420
  }
246
- return { totalEntries, groomedEntries, ungroomedEntries: totalEntries - groomedEntries, staleEntries, freshEntries, avgDaysSinceGroom };
421
+ return {
422
+ totalEntries,
423
+ groomedEntries,
424
+ ungroomedEntries: totalEntries - groomedEntries,
425
+ staleEntries,
426
+ freshEntries,
427
+ avgDaysSinceGroom,
428
+ };
247
429
  }
248
430
 
249
431
  // ─── Metadata Enrichment (delegates to metadata-enricher) ────
250
432
 
251
- enrichMetadata(entryId: string): { enriched: boolean; changes: Array<{ field: string; before: string; after: string }> } {
433
+ enrichMetadata(entryId: string): {
434
+ enriched: boolean;
435
+ changes: Array<{ field: string; before: string; after: string }>;
436
+ } {
252
437
  const entry = this.vault.get(entryId);
253
438
  if (!entry) return { enriched: false, changes: [] };
254
439
  const { changes, updates } = enrichEntryMetadata(entry);
255
440
  if (changes.length === 0) return { enriched: false, changes: [] };
256
441
  this.vault.update(entryId, updates);
257
442
  this.recordSnapshot(entryId, 'curator', 'Metadata enrichment');
258
- this.logChange('enrich_metadata', entryId, JSON.stringify(changes.map((c) => c.field)), JSON.stringify(changes.map((c) => c.after)), 'Rule-based metadata enrichment');
443
+ this.logChange(
444
+ 'enrich_metadata',
445
+ entryId,
446
+ JSON.stringify(changes.map((c) => c.field)),
447
+ JSON.stringify(changes.map((c) => c.after)),
448
+ 'Rule-based metadata enrichment',
449
+ );
259
450
  return { enriched: true, changes };
260
451
  }
261
452
 
@@ -264,24 +455,55 @@ export class Curator {
264
455
  private persistContradictions(candidates: ContradictionCandidate[]): Contradiction[] {
265
456
  const detected: Contradiction[] = [];
266
457
  for (const c of candidates) {
267
- const result = this.provider.run('INSERT OR IGNORE INTO curator_contradictions (pattern_id, antipattern_id, similarity) VALUES (?, ?, ?)', [c.patternId, c.antipatternId, c.similarity]);
458
+ const result = this.provider.run(
459
+ 'INSERT OR IGNORE INTO curator_contradictions (pattern_id, antipattern_id, similarity) VALUES (?, ?, ?)',
460
+ [c.patternId, c.antipatternId, c.similarity],
461
+ );
268
462
  if (result.changes > 0) {
269
- const row = this.provider.get<Record<string, unknown>>('SELECT * FROM curator_contradictions WHERE pattern_id = ? AND antipattern_id = ?', [c.patternId, c.antipatternId]);
463
+ const row = this.provider.get<Record<string, unknown>>(
464
+ 'SELECT * FROM curator_contradictions WHERE pattern_id = ? AND antipattern_id = ?',
465
+ [c.patternId, c.antipatternId],
466
+ );
270
467
  if (row) detected.push(this.rowToContradiction(row));
271
468
  }
272
469
  }
273
470
  return detected;
274
471
  }
275
472
 
276
- private logChange(action: string, entryId: string, beforeValue: string | null, afterValue: string | null, reason: string): void {
277
- this.provider.run('INSERT INTO curator_changelog (action, entry_id, before_value, after_value, reason) VALUES (?, ?, ?, ?, ?)', [action, entryId, beforeValue, afterValue, reason]);
473
+ private logChange(
474
+ action: string,
475
+ entryId: string,
476
+ beforeValue: string | null,
477
+ afterValue: string | null,
478
+ reason: string,
479
+ ): void {
480
+ this.provider.run(
481
+ 'INSERT INTO curator_changelog (action, entry_id, before_value, after_value, reason) VALUES (?, ?, ?, ?, ?)',
482
+ [action, entryId, beforeValue, afterValue, reason],
483
+ );
278
484
  }
279
485
 
280
486
  private rowToContradiction(row: Record<string, unknown>): Contradiction {
281
- return { id: row.id as number, patternId: row.pattern_id as string, antipatternId: row.antipattern_id as string, similarity: row.similarity as number, status: row.status as ContradictionStatus, createdAt: row.created_at as number, resolvedAt: (row.resolved_at as number) ?? null };
487
+ return {
488
+ id: row.id as number,
489
+ patternId: row.pattern_id as string,
490
+ antipatternId: row.antipattern_id as string,
491
+ similarity: row.similarity as number,
492
+ status: row.status as ContradictionStatus,
493
+ createdAt: row.created_at as number,
494
+ resolvedAt: (row.resolved_at as number) ?? null,
495
+ };
282
496
  }
283
497
 
284
498
  private rowToChangelog(row: Record<string, unknown>): ChangelogEntry {
285
- return { id: row.id as number, action: row.action as string, entryId: row.entry_id as string, beforeValue: (row.before_value as string) ?? null, afterValue: (row.after_value as string) ?? null, reason: row.reason as string, createdAt: row.created_at as number };
499
+ return {
500
+ id: row.id as number,
501
+ action: row.action as string,
502
+ entryId: row.entry_id as string,
503
+ beforeValue: (row.before_value as string) ?? null,
504
+ afterValue: (row.after_value as string) ?? null,
505
+ reason: row.reason as string,
506
+ createdAt: row.created_at as number,
507
+ };
286
508
  }
287
509
  }
@@ -97,8 +97,16 @@ describe('duplicate-detector', () => {
97
97
 
98
98
  it('detects duplicates with identical content', () => {
99
99
  const entries = [
100
- makeEntry({ id: 'dup-1', title: 'Validate user input', description: 'Always validate user input before processing.' }),
101
- makeEntry({ id: 'dup-2', title: 'Validate user input', description: 'Always validate user input before processing.' }),
100
+ makeEntry({
101
+ id: 'dup-1',
102
+ title: 'Validate user input',
103
+ description: 'Always validate user input before processing.',
104
+ }),
105
+ makeEntry({
106
+ id: 'dup-2',
107
+ title: 'Validate user input',
108
+ description: 'Always validate user input before processing.',
109
+ }),
102
110
  ];
103
111
  const results = detectDuplicates(entries, undefined, 0.3);
104
112
  expect(results.length).toBeGreaterThan(0);
@@ -107,8 +115,16 @@ describe('duplicate-detector', () => {
107
115
 
108
116
  it('does not flag unrelated entries', () => {
109
117
  const entries = [
110
- makeEntry({ id: 'a', title: 'Database indexing strategies', description: 'Create indices on columns.' }),
111
- makeEntry({ id: 'b', title: 'React component lifecycle', description: 'Use useEffect for side effects.' }),
118
+ makeEntry({
119
+ id: 'a',
120
+ title: 'Database indexing strategies',
121
+ description: 'Create indices on columns.',
122
+ }),
123
+ makeEntry({
124
+ id: 'b',
125
+ title: 'React component lifecycle',
126
+ description: 'Use useEffect for side effects.',
127
+ }),
112
128
  ];
113
129
  const results = detectDuplicates(entries, undefined, 0.8);
114
130
  expect(results.length).toBe(0);
@@ -117,7 +133,11 @@ describe('duplicate-detector', () => {
117
133
  it('filters by entryId when provided', () => {
118
134
  const entries = [
119
135
  makeEntry({ id: 'x', title: 'Authentication with JWT', description: 'Use JWT for auth.' }),
120
- makeEntry({ id: 'y', title: 'JWT authentication pattern', description: 'Implement JWT auth.' }),
136
+ makeEntry({
137
+ id: 'y',
138
+ title: 'JWT authentication pattern',
139
+ description: 'Implement JWT auth.',
140
+ }),
121
141
  makeEntry({ id: 'z', title: 'Database pooling', description: 'Connection pools.' }),
122
142
  ];
123
143
  const results = detectDuplicates(entries, 'x', 0.3);
@@ -129,8 +149,18 @@ describe('duplicate-detector', () => {
129
149
 
130
150
  it('skips cross-domain pairs', () => {
131
151
  const entries = [
132
- makeEntry({ id: '1', domain: 'design', title: 'Use semantic tokens', description: 'Always use semantic tokens.' }),
133
- makeEntry({ id: '2', domain: 'architecture', title: 'Use semantic tokens', description: 'Always use semantic tokens.' }),
152
+ makeEntry({
153
+ id: '1',
154
+ domain: 'design',
155
+ title: 'Use semantic tokens',
156
+ description: 'Always use semantic tokens.',
157
+ }),
158
+ makeEntry({
159
+ id: '2',
160
+ domain: 'architecture',
161
+ title: 'Use semantic tokens',
162
+ description: 'Always use semantic tokens.',
163
+ }),
134
164
  ];
135
165
  const results = detectDuplicates(entries, undefined, 0.3);
136
166
  expect(results.length).toBe(0);
@@ -138,8 +168,18 @@ describe('duplicate-detector', () => {
138
168
 
139
169
  it('flags same-domain similar entries', () => {
140
170
  const entries = [
141
- makeEntry({ id: '1', domain: 'design', title: 'Use semantic tokens for colors', description: 'Always use semantic tokens.' }),
142
- makeEntry({ id: '2', domain: 'design', title: 'Use semantic tokens for color values', description: 'Prefer semantic color tokens.' }),
171
+ makeEntry({
172
+ id: '1',
173
+ domain: 'design',
174
+ title: 'Use semantic tokens for colors',
175
+ description: 'Always use semantic tokens.',
176
+ }),
177
+ makeEntry({
178
+ id: '2',
179
+ domain: 'design',
180
+ title: 'Use semantic tokens for color values',
181
+ description: 'Prefer semantic color tokens.',
182
+ }),
143
183
  ];
144
184
  const results = detectDuplicates(entries, undefined, 0.3);
145
185
  expect(results.length).toBeGreaterThan(0);
@@ -147,8 +187,16 @@ describe('duplicate-detector', () => {
147
187
 
148
188
  it('sets suggestMerge based on MERGE_SUGGESTION_THRESHOLD', () => {
149
189
  const entries = [
150
- makeEntry({ id: 'a', title: 'Exact same title', description: 'Exact same description for merge test.' }),
151
- makeEntry({ id: 'b', title: 'Exact same title', description: 'Exact same description for merge test.' }),
190
+ makeEntry({
191
+ id: 'a',
192
+ title: 'Exact same title',
193
+ description: 'Exact same description for merge test.',
194
+ }),
195
+ makeEntry({
196
+ id: 'b',
197
+ title: 'Exact same title',
198
+ description: 'Exact same description for merge test.',
199
+ }),
152
200
  ];
153
201
  const results = detectDuplicates(entries, undefined, 0.3);
154
202
  expect(results.length).toBeGreaterThan(0);
@@ -158,13 +206,27 @@ describe('duplicate-detector', () => {
158
206
 
159
207
  it('sorts matches by descending similarity', () => {
160
208
  const entries = [
161
- makeEntry({ id: 'base', title: 'Use semantic tokens for colors', description: 'Tokens for styling.' }),
162
- makeEntry({ id: 'close', title: 'Use semantic tokens for color values', description: 'Tokens for styling values.' }),
163
- makeEntry({ id: 'far', title: 'Semantic approach to colors', description: 'Use semantic color approach.' }),
209
+ makeEntry({
210
+ id: 'base',
211
+ title: 'Use semantic tokens for colors',
212
+ description: 'Tokens for styling.',
213
+ }),
214
+ makeEntry({
215
+ id: 'close',
216
+ title: 'Use semantic tokens for color values',
217
+ description: 'Tokens for styling values.',
218
+ }),
219
+ makeEntry({
220
+ id: 'far',
221
+ title: 'Semantic approach to colors',
222
+ description: 'Use semantic color approach.',
223
+ }),
164
224
  ];
165
225
  const results = detectDuplicates(entries, 'base', 0.1);
166
226
  if (results.length > 0 && results[0].matches.length > 1) {
167
- expect(results[0].matches[0].similarity).toBeGreaterThanOrEqual(results[0].matches[1].similarity);
227
+ expect(results[0].matches[0].similarity).toBeGreaterThanOrEqual(
228
+ results[0].matches[1].similarity,
229
+ );
168
230
  }
169
231
  });
170
232