driftdetect-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/dist/analyzers/ast-analyzer.d.ts +251 -0
  2. package/dist/analyzers/ast-analyzer.d.ts.map +1 -0
  3. package/dist/analyzers/ast-analyzer.js +548 -0
  4. package/dist/analyzers/ast-analyzer.js.map +1 -0
  5. package/dist/analyzers/flow-analyzer.d.ts +241 -0
  6. package/dist/analyzers/flow-analyzer.d.ts.map +1 -0
  7. package/dist/analyzers/flow-analyzer.js +1219 -0
  8. package/dist/analyzers/flow-analyzer.js.map +1 -0
  9. package/dist/analyzers/index.d.ts +18 -0
  10. package/dist/analyzers/index.d.ts.map +1 -0
  11. package/dist/analyzers/index.js +19 -0
  12. package/dist/analyzers/index.js.map +1 -0
  13. package/dist/analyzers/semantic-analyzer.d.ts +252 -0
  14. package/dist/analyzers/semantic-analyzer.d.ts.map +1 -0
  15. package/dist/analyzers/semantic-analyzer.js +1182 -0
  16. package/dist/analyzers/semantic-analyzer.js.map +1 -0
  17. package/dist/analyzers/type-analyzer.d.ts +289 -0
  18. package/dist/analyzers/type-analyzer.d.ts.map +1 -0
  19. package/dist/analyzers/type-analyzer.js +1269 -0
  20. package/dist/analyzers/type-analyzer.js.map +1 -0
  21. package/dist/analyzers/types.d.ts +537 -0
  22. package/dist/analyzers/types.d.ts.map +1 -0
  23. package/dist/analyzers/types.js +11 -0
  24. package/dist/analyzers/types.js.map +1 -0
  25. package/dist/config/config-loader.d.ts +166 -0
  26. package/dist/config/config-loader.d.ts.map +1 -0
  27. package/dist/config/config-loader.js +429 -0
  28. package/dist/config/config-loader.js.map +1 -0
  29. package/dist/config/config-validator.d.ts +204 -0
  30. package/dist/config/config-validator.d.ts.map +1 -0
  31. package/dist/config/config-validator.js +632 -0
  32. package/dist/config/config-validator.js.map +1 -0
  33. package/dist/config/defaults.d.ts +8 -0
  34. package/dist/config/defaults.d.ts.map +1 -0
  35. package/dist/config/defaults.js +26 -0
  36. package/dist/config/defaults.js.map +1 -0
  37. package/dist/config/index.d.ts +10 -0
  38. package/dist/config/index.d.ts.map +1 -0
  39. package/dist/config/index.js +10 -0
  40. package/dist/config/index.js.map +1 -0
  41. package/dist/config/types.d.ts +47 -0
  42. package/dist/config/types.d.ts.map +1 -0
  43. package/dist/config/types.js +7 -0
  44. package/dist/config/types.js.map +1 -0
  45. package/dist/index.d.ts +37 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +39 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/manifest/exporter.d.ts +21 -0
  50. package/dist/manifest/exporter.d.ts.map +1 -0
  51. package/dist/manifest/exporter.js +339 -0
  52. package/dist/manifest/exporter.js.map +1 -0
  53. package/dist/manifest/index.d.ts +14 -0
  54. package/dist/manifest/index.d.ts.map +1 -0
  55. package/dist/manifest/index.js +15 -0
  56. package/dist/manifest/index.js.map +1 -0
  57. package/dist/manifest/manifest-store.d.ts +111 -0
  58. package/dist/manifest/manifest-store.d.ts.map +1 -0
  59. package/dist/manifest/manifest-store.js +418 -0
  60. package/dist/manifest/manifest-store.js.map +1 -0
  61. package/dist/manifest/types.d.ts +238 -0
  62. package/dist/manifest/types.d.ts.map +1 -0
  63. package/dist/manifest/types.js +11 -0
  64. package/dist/manifest/types.js.map +1 -0
  65. package/dist/matcher/confidence-scorer.d.ts +188 -0
  66. package/dist/matcher/confidence-scorer.d.ts.map +1 -0
  67. package/dist/matcher/confidence-scorer.js +302 -0
  68. package/dist/matcher/confidence-scorer.js.map +1 -0
  69. package/dist/matcher/index.d.ts +24 -0
  70. package/dist/matcher/index.d.ts.map +1 -0
  71. package/dist/matcher/index.js +26 -0
  72. package/dist/matcher/index.js.map +1 -0
  73. package/dist/matcher/outlier-detector.d.ts +252 -0
  74. package/dist/matcher/outlier-detector.d.ts.map +1 -0
  75. package/dist/matcher/outlier-detector.js +544 -0
  76. package/dist/matcher/outlier-detector.js.map +1 -0
  77. package/dist/matcher/pattern-matcher.d.ts +169 -0
  78. package/dist/matcher/pattern-matcher.d.ts.map +1 -0
  79. package/dist/matcher/pattern-matcher.js +692 -0
  80. package/dist/matcher/pattern-matcher.js.map +1 -0
  81. package/dist/matcher/types.d.ts +476 -0
  82. package/dist/matcher/types.d.ts.map +1 -0
  83. package/dist/matcher/types.js +36 -0
  84. package/dist/matcher/types.js.map +1 -0
  85. package/dist/parsers/base-parser.d.ts +282 -0
  86. package/dist/parsers/base-parser.d.ts.map +1 -0
  87. package/dist/parsers/base-parser.js +421 -0
  88. package/dist/parsers/base-parser.js.map +1 -0
  89. package/dist/parsers/css-parser.d.ts +225 -0
  90. package/dist/parsers/css-parser.d.ts.map +1 -0
  91. package/dist/parsers/css-parser.js +477 -0
  92. package/dist/parsers/css-parser.js.map +1 -0
  93. package/dist/parsers/index.d.ts +15 -0
  94. package/dist/parsers/index.d.ts.map +1 -0
  95. package/dist/parsers/index.js +15 -0
  96. package/dist/parsers/index.js.map +1 -0
  97. package/dist/parsers/json-parser.d.ts +219 -0
  98. package/dist/parsers/json-parser.d.ts.map +1 -0
  99. package/dist/parsers/json-parser.js +602 -0
  100. package/dist/parsers/json-parser.js.map +1 -0
  101. package/dist/parsers/markdown-parser.d.ts +276 -0
  102. package/dist/parsers/markdown-parser.d.ts.map +1 -0
  103. package/dist/parsers/markdown-parser.js +731 -0
  104. package/dist/parsers/markdown-parser.js.map +1 -0
  105. package/dist/parsers/parser-manager.d.ts +294 -0
  106. package/dist/parsers/parser-manager.d.ts.map +1 -0
  107. package/dist/parsers/parser-manager.js +738 -0
  108. package/dist/parsers/parser-manager.js.map +1 -0
  109. package/dist/parsers/python-parser.d.ts +204 -0
  110. package/dist/parsers/python-parser.d.ts.map +1 -0
  111. package/dist/parsers/python-parser.js +517 -0
  112. package/dist/parsers/python-parser.js.map +1 -0
  113. package/dist/parsers/types.d.ts +43 -0
  114. package/dist/parsers/types.d.ts.map +1 -0
  115. package/dist/parsers/types.js +7 -0
  116. package/dist/parsers/types.js.map +1 -0
  117. package/dist/parsers/typescript-parser.d.ts +264 -0
  118. package/dist/parsers/typescript-parser.d.ts.map +1 -0
  119. package/dist/parsers/typescript-parser.js +658 -0
  120. package/dist/parsers/typescript-parser.js.map +1 -0
  121. package/dist/rules/evaluator.d.ts +305 -0
  122. package/dist/rules/evaluator.d.ts.map +1 -0
  123. package/dist/rules/evaluator.js +579 -0
  124. package/dist/rules/evaluator.js.map +1 -0
  125. package/dist/rules/index.d.ts +13 -0
  126. package/dist/rules/index.d.ts.map +1 -0
  127. package/dist/rules/index.js +13 -0
  128. package/dist/rules/index.js.map +1 -0
  129. package/dist/rules/quick-fix-generator.d.ts +334 -0
  130. package/dist/rules/quick-fix-generator.d.ts.map +1 -0
  131. package/dist/rules/quick-fix-generator.js +1075 -0
  132. package/dist/rules/quick-fix-generator.js.map +1 -0
  133. package/dist/rules/rule-engine.d.ts +241 -0
  134. package/dist/rules/rule-engine.d.ts.map +1 -0
  135. package/dist/rules/rule-engine.js +585 -0
  136. package/dist/rules/rule-engine.js.map +1 -0
  137. package/dist/rules/severity-manager.d.ts +394 -0
  138. package/dist/rules/severity-manager.d.ts.map +1 -0
  139. package/dist/rules/severity-manager.js +619 -0
  140. package/dist/rules/severity-manager.js.map +1 -0
  141. package/dist/rules/types.d.ts +370 -0
  142. package/dist/rules/types.d.ts.map +1 -0
  143. package/dist/rules/types.js +133 -0
  144. package/dist/rules/types.js.map +1 -0
  145. package/dist/rules/variant-manager.d.ts +388 -0
  146. package/dist/rules/variant-manager.d.ts.map +1 -0
  147. package/dist/rules/variant-manager.js +777 -0
  148. package/dist/rules/variant-manager.js.map +1 -0
  149. package/dist/scanner/change-detector.d.ts +164 -0
  150. package/dist/scanner/change-detector.d.ts.map +1 -0
  151. package/dist/scanner/change-detector.js +263 -0
  152. package/dist/scanner/change-detector.js.map +1 -0
  153. package/dist/scanner/dependency-graph.d.ts +270 -0
  154. package/dist/scanner/dependency-graph.d.ts.map +1 -0
  155. package/dist/scanner/dependency-graph.js +436 -0
  156. package/dist/scanner/dependency-graph.js.map +1 -0
  157. package/dist/scanner/file-walker.d.ts +127 -0
  158. package/dist/scanner/file-walker.d.ts.map +1 -0
  159. package/dist/scanner/file-walker.js +526 -0
  160. package/dist/scanner/file-walker.js.map +1 -0
  161. package/dist/scanner/index.d.ts +12 -0
  162. package/dist/scanner/index.d.ts.map +1 -0
  163. package/dist/scanner/index.js +12 -0
  164. package/dist/scanner/index.js.map +1 -0
  165. package/dist/scanner/types.d.ts +218 -0
  166. package/dist/scanner/types.d.ts.map +1 -0
  167. package/dist/scanner/types.js +10 -0
  168. package/dist/scanner/types.js.map +1 -0
  169. package/dist/scanner/worker-pool.d.ts +317 -0
  170. package/dist/scanner/worker-pool.d.ts.map +1 -0
  171. package/dist/scanner/worker-pool.js +571 -0
  172. package/dist/scanner/worker-pool.js.map +1 -0
  173. package/dist/store/cache-manager.d.ts +179 -0
  174. package/dist/store/cache-manager.d.ts.map +1 -0
  175. package/dist/store/cache-manager.js +391 -0
  176. package/dist/store/cache-manager.js.map +1 -0
  177. package/dist/store/history-store.d.ts +314 -0
  178. package/dist/store/history-store.d.ts.map +1 -0
  179. package/dist/store/history-store.js +707 -0
  180. package/dist/store/history-store.js.map +1 -0
  181. package/dist/store/index.d.ts +20 -0
  182. package/dist/store/index.d.ts.map +1 -0
  183. package/dist/store/index.js +26 -0
  184. package/dist/store/index.js.map +1 -0
  185. package/dist/store/lock-file-manager.d.ts +202 -0
  186. package/dist/store/lock-file-manager.d.ts.map +1 -0
  187. package/dist/store/lock-file-manager.js +475 -0
  188. package/dist/store/lock-file-manager.js.map +1 -0
  189. package/dist/store/pattern-store.d.ts +289 -0
  190. package/dist/store/pattern-store.d.ts.map +1 -0
  191. package/dist/store/pattern-store.js +936 -0
  192. package/dist/store/pattern-store.js.map +1 -0
  193. package/dist/store/schema-validator.d.ts +159 -0
  194. package/dist/store/schema-validator.d.ts.map +1 -0
  195. package/dist/store/schema-validator.js +1096 -0
  196. package/dist/store/schema-validator.js.map +1 -0
  197. package/dist/store/types.d.ts +585 -0
  198. package/dist/store/types.d.ts.map +1 -0
  199. package/dist/store/types.js +82 -0
  200. package/dist/store/types.js.map +1 -0
  201. package/dist/types/analysis.d.ts +19 -0
  202. package/dist/types/analysis.d.ts.map +1 -0
  203. package/dist/types/analysis.js +5 -0
  204. package/dist/types/analysis.js.map +1 -0
  205. package/dist/types/common.d.ts +7 -0
  206. package/dist/types/common.d.ts.map +1 -0
  207. package/dist/types/common.js +5 -0
  208. package/dist/types/common.js.map +1 -0
  209. package/dist/types/index.d.ts +12 -0
  210. package/dist/types/index.d.ts.map +1 -0
  211. package/dist/types/index.js +10 -0
  212. package/dist/types/index.js.map +1 -0
  213. package/dist/types/patterns.d.ts +40 -0
  214. package/dist/types/patterns.d.ts.map +1 -0
  215. package/dist/types/patterns.js +7 -0
  216. package/dist/types/patterns.js.map +1 -0
  217. package/dist/types/violations.d.ts +7 -0
  218. package/dist/types/violations.d.ts.map +1 -0
  219. package/dist/types/violations.js +7 -0
  220. package/dist/types/violations.js.map +1 -0
  221. package/package.json +46 -0
@@ -0,0 +1,707 @@
1
+ /**
2
+ * History Store - Pattern change tracking and approval history
3
+ *
4
+ * Tracks pattern changes over time and stores approval history.
5
+ * History entries are stored in .drift/history/ directory.
6
+ *
7
+ * @requirements 4.4 - THE Pattern_Store SHALL maintain history of pattern changes in .drift/history/
8
+ */
9
+ import * as fs from 'node:fs/promises';
10
+ import * as path from 'node:path';
11
+ import { EventEmitter } from 'node:events';
12
+ import { HISTORY_FILE_VERSION } from './types.js';
13
+ // ============================================================================
14
+ // Constants
15
+ // ============================================================================
16
+ /** Directory name for drift configuration */
17
+ const DRIFT_DIR = '.drift';
18
+ /** Directory name for history */
19
+ const HISTORY_DIR = 'history';
20
+ /** Main history file name */
21
+ const HISTORY_FILE = 'patterns.json';
22
+ /** Default maximum history entries per pattern */
23
+ const DEFAULT_MAX_ENTRIES_PER_PATTERN = 100;
24
+ /** Default maximum age for history entries in days */
25
+ const DEFAULT_MAX_AGE_DAYS = 365;
26
+ // ============================================================================
27
+ // Error Classes
28
+ // ============================================================================
29
+ /**
30
+ * Error thrown when a history operation fails
31
+ */
32
+ export class HistoryStoreError extends Error {
33
+ errorCause;
34
+ constructor(message, errorCause) {
35
+ super(message);
36
+ this.name = 'HistoryStoreError';
37
+ this.errorCause = errorCause;
38
+ }
39
+ }
40
+ /**
41
+ * Error thrown when a pattern history is not found
42
+ */
43
+ export class PatternHistoryNotFoundError extends Error {
44
+ patternId;
45
+ constructor(patternId) {
46
+ super(`Pattern history not found: ${patternId}`);
47
+ this.patternId = patternId;
48
+ this.name = 'PatternHistoryNotFoundError';
49
+ }
50
+ }
51
+ /**
52
+ * Default history store configuration
53
+ */
54
+ export const DEFAULT_HISTORY_STORE_CONFIG = {
55
+ rootDir: '.',
56
+ maxEntriesPerPattern: DEFAULT_MAX_ENTRIES_PER_PATTERN,
57
+ maxAgeDays: DEFAULT_MAX_AGE_DAYS,
58
+ autoSave: false,
59
+ autoSaveDebounce: 1000,
60
+ };
61
+ // ============================================================================
62
+ // Helper Functions
63
+ // ============================================================================
64
+ /**
65
+ * Check if a file exists
66
+ */
67
+ async function fileExists(filePath) {
68
+ try {
69
+ await fs.access(filePath);
70
+ return true;
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ }
76
+ /**
77
+ * Ensure a directory exists
78
+ */
79
+ async function ensureDir(dirPath) {
80
+ await fs.mkdir(dirPath, { recursive: true });
81
+ }
82
+ // ============================================================================
83
+ // History Store Class
84
+ // ============================================================================
85
+ /**
86
+ * History Store - Manages pattern change history
87
+ *
88
+ * Tracks all pattern changes including:
89
+ * - Pattern created
90
+ * - Pattern updated
91
+ * - Pattern approved
92
+ * - Pattern ignored
93
+ * - Pattern deleted
94
+ * - Confidence changed
95
+ * - Locations changed
96
+ * - Severity changed
97
+ *
98
+ * History is stored in .drift/history/patterns.json
99
+ *
100
+ * @requirements 4.4 - Pattern history tracked in .drift/history/
101
+ */
102
+ export class HistoryStore extends EventEmitter {
103
+ config;
104
+ historyDir;
105
+ historyFilePath;
106
+ histories = new Map();
107
+ loaded = false;
108
+ dirty = false;
109
+ saveTimeout = null;
110
+ constructor(config = {}) {
111
+ super();
112
+ this.config = { ...DEFAULT_HISTORY_STORE_CONFIG, ...config };
113
+ this.historyDir = path.join(this.config.rootDir, DRIFT_DIR, HISTORY_DIR);
114
+ this.historyFilePath = path.join(this.historyDir, HISTORY_FILE);
115
+ }
116
+ // ==========================================================================
117
+ // Initialization
118
+ // ==========================================================================
119
+ /**
120
+ * Initialize the history store
121
+ *
122
+ * Creates necessary directories and loads existing history.
123
+ */
124
+ async initialize() {
125
+ // Create directory structure
126
+ await ensureDir(this.historyDir);
127
+ // Load existing history
128
+ await this.load();
129
+ this.loaded = true;
130
+ }
131
+ // ==========================================================================
132
+ // Loading
133
+ // ==========================================================================
134
+ /**
135
+ * Load history from disk
136
+ *
137
+ * @requirements 4.4 - Load history from .drift/history/
138
+ */
139
+ async load() {
140
+ this.histories.clear();
141
+ if (!(await fileExists(this.historyFilePath))) {
142
+ this.emitEvent('file:loaded', undefined, { count: 0 });
143
+ return;
144
+ }
145
+ try {
146
+ const content = await fs.readFile(this.historyFilePath, 'utf-8');
147
+ const data = JSON.parse(content);
148
+ // Load pattern histories into map
149
+ for (const history of data.patterns) {
150
+ this.histories.set(history.patternId, history);
151
+ }
152
+ this.emitEvent('file:loaded', undefined, { count: this.histories.size });
153
+ }
154
+ catch (error) {
155
+ if (error.code === 'ENOENT') {
156
+ return; // File doesn't exist, skip
157
+ }
158
+ throw new HistoryStoreError(`Failed to load history file: ${this.historyFilePath}`, error);
159
+ }
160
+ }
161
+ // ==========================================================================
162
+ // Saving
163
+ // ==========================================================================
164
+ /**
165
+ * Save history to disk
166
+ *
167
+ * @requirements 4.4 - Persist history in .drift/history/
168
+ */
169
+ async save() {
170
+ const historyFile = {
171
+ version: HISTORY_FILE_VERSION,
172
+ patterns: Array.from(this.histories.values()),
173
+ lastUpdated: new Date().toISOString(),
174
+ };
175
+ // Ensure directory exists
176
+ await ensureDir(this.historyDir);
177
+ // Write file
178
+ await fs.writeFile(this.historyFilePath, JSON.stringify(historyFile, null, 2));
179
+ this.dirty = false;
180
+ this.emitEvent('file:saved', undefined, { count: this.histories.size });
181
+ }
182
+ /**
183
+ * Schedule an auto-save if enabled
184
+ */
185
+ scheduleAutoSave() {
186
+ if (!this.config.autoSave) {
187
+ return;
188
+ }
189
+ if (this.saveTimeout) {
190
+ clearTimeout(this.saveTimeout);
191
+ }
192
+ this.saveTimeout = setTimeout(async () => {
193
+ if (this.dirty) {
194
+ await this.save();
195
+ }
196
+ }, this.config.autoSaveDebounce);
197
+ }
198
+ // ==========================================================================
199
+ // Recording Events
200
+ // ==========================================================================
201
+ /**
202
+ * Record a history event for a pattern
203
+ *
204
+ * @param patternId - Pattern ID
205
+ * @param category - Pattern category
206
+ * @param eventType - Type of event
207
+ * @param options - Additional event options
208
+ */
209
+ recordEvent(patternId, category, eventType, options = {}) {
210
+ const now = new Date().toISOString();
211
+ // Create the event
212
+ const event = {
213
+ timestamp: now,
214
+ type: eventType,
215
+ patternId,
216
+ ...(options.user && { user: options.user }),
217
+ ...(options.previousValue !== undefined && { previousValue: options.previousValue }),
218
+ ...(options.newValue !== undefined && { newValue: options.newValue }),
219
+ ...(options.details && { details: options.details }),
220
+ };
221
+ // Get or create pattern history
222
+ let history = this.histories.get(patternId);
223
+ if (!history) {
224
+ history = {
225
+ patternId,
226
+ category,
227
+ events: [],
228
+ createdAt: now,
229
+ lastModified: now,
230
+ };
231
+ this.histories.set(patternId, history);
232
+ }
233
+ // Add event to history
234
+ history.events.push(event);
235
+ history.lastModified = now;
236
+ // Prune if needed
237
+ this.prunePatternHistory(patternId);
238
+ this.dirty = true;
239
+ this.emitEvent('event:recorded', patternId, { eventType });
240
+ this.scheduleAutoSave();
241
+ return event;
242
+ }
243
+ /**
244
+ * Record a pattern creation event
245
+ */
246
+ recordCreated(pattern, user) {
247
+ const options = {
248
+ newValue: {
249
+ name: pattern.name,
250
+ description: pattern.description,
251
+ confidence: pattern.confidence.score,
252
+ severity: pattern.severity,
253
+ },
254
+ };
255
+ if (user !== undefined) {
256
+ options.user = user;
257
+ }
258
+ return this.recordEvent(pattern.id, pattern.category, 'created', options);
259
+ }
260
+ /**
261
+ * Record a pattern update event
262
+ */
263
+ recordUpdated(pattern, previousPattern, user) {
264
+ const options = {
265
+ previousValue: {
266
+ name: previousPattern.name,
267
+ description: previousPattern.description,
268
+ },
269
+ newValue: {
270
+ name: pattern.name,
271
+ description: pattern.description,
272
+ },
273
+ };
274
+ if (user !== undefined) {
275
+ options.user = user;
276
+ }
277
+ return this.recordEvent(pattern.id, pattern.category, 'updated', options);
278
+ }
279
+ /**
280
+ * Record a pattern approval event
281
+ */
282
+ recordApproved(pattern, user) {
283
+ const options = {
284
+ details: {
285
+ confidence: pattern.confidence.score,
286
+ severity: pattern.severity,
287
+ },
288
+ };
289
+ if (user !== undefined) {
290
+ options.user = user;
291
+ }
292
+ return this.recordEvent(pattern.id, pattern.category, 'approved', options);
293
+ }
294
+ /**
295
+ * Record a pattern ignore event
296
+ */
297
+ recordIgnored(pattern, user) {
298
+ const options = {};
299
+ if (user !== undefined) {
300
+ options.user = user;
301
+ }
302
+ return this.recordEvent(pattern.id, pattern.category, 'ignored', options);
303
+ }
304
+ /**
305
+ * Record a pattern deletion event
306
+ */
307
+ recordDeleted(pattern, user) {
308
+ const options = {
309
+ previousValue: {
310
+ name: pattern.name,
311
+ status: pattern.status,
312
+ },
313
+ };
314
+ if (user !== undefined) {
315
+ options.user = user;
316
+ }
317
+ return this.recordEvent(pattern.id, pattern.category, 'deleted', options);
318
+ }
319
+ /**
320
+ * Record a confidence change event
321
+ */
322
+ recordConfidenceChanged(pattern, previousScore, user) {
323
+ const options = {
324
+ previousValue: previousScore,
325
+ newValue: pattern.confidence.score,
326
+ };
327
+ if (user !== undefined) {
328
+ options.user = user;
329
+ }
330
+ return this.recordEvent(pattern.id, pattern.category, 'confidence_changed', options);
331
+ }
332
+ /**
333
+ * Record a locations change event
334
+ */
335
+ recordLocationsChanged(pattern, previousCount, user) {
336
+ const options = {
337
+ previousValue: previousCount,
338
+ newValue: pattern.locations.length,
339
+ details: {
340
+ added: Math.max(0, pattern.locations.length - previousCount),
341
+ removed: Math.max(0, previousCount - pattern.locations.length),
342
+ },
343
+ };
344
+ if (user !== undefined) {
345
+ options.user = user;
346
+ }
347
+ return this.recordEvent(pattern.id, pattern.category, 'locations_changed', options);
348
+ }
349
+ /**
350
+ * Record a severity change event
351
+ */
352
+ recordSeverityChanged(pattern, previousSeverity, user) {
353
+ const options = {
354
+ previousValue: previousSeverity,
355
+ newValue: pattern.severity,
356
+ };
357
+ if (user !== undefined) {
358
+ options.user = user;
359
+ }
360
+ return this.recordEvent(pattern.id, pattern.category, 'severity_changed', options);
361
+ }
362
+ // ==========================================================================
363
+ // Querying
364
+ // ==========================================================================
365
+ /**
366
+ * Get history for a specific pattern
367
+ *
368
+ * @param patternId - Pattern ID
369
+ * @returns Pattern history or undefined if not found
370
+ */
371
+ getPatternHistory(patternId) {
372
+ return this.histories.get(patternId);
373
+ }
374
+ /**
375
+ * Get history for a specific pattern, throwing if not found
376
+ *
377
+ * @param patternId - Pattern ID
378
+ * @returns Pattern history
379
+ * @throws PatternHistoryNotFoundError if not found
380
+ */
381
+ getPatternHistoryOrThrow(patternId) {
382
+ const history = this.histories.get(patternId);
383
+ if (!history) {
384
+ throw new PatternHistoryNotFoundError(patternId);
385
+ }
386
+ return history;
387
+ }
388
+ /**
389
+ * Check if history exists for a pattern
390
+ *
391
+ * @param patternId - Pattern ID
392
+ * @returns True if history exists
393
+ */
394
+ hasPatternHistory(patternId) {
395
+ return this.histories.has(patternId);
396
+ }
397
+ /**
398
+ * Query history events with filtering and pagination
399
+ *
400
+ * @param query - Query options
401
+ * @returns Query result with matching events
402
+ */
403
+ query(query = {}) {
404
+ const startTime = Date.now();
405
+ // Collect all events from relevant histories
406
+ let events = [];
407
+ // Filter by pattern ID(s)
408
+ if (query.patternId) {
409
+ const history = this.histories.get(query.patternId);
410
+ if (history) {
411
+ events = [...history.events];
412
+ }
413
+ }
414
+ else if (query.patternIds && query.patternIds.length > 0) {
415
+ for (const patternId of query.patternIds) {
416
+ const history = this.histories.get(patternId);
417
+ if (history) {
418
+ events.push(...history.events);
419
+ }
420
+ }
421
+ }
422
+ else {
423
+ // Get all events
424
+ for (const history of this.histories.values()) {
425
+ events.push(...history.events);
426
+ }
427
+ }
428
+ // Apply filters
429
+ events = this.applyFilters(events, query);
430
+ // Sort by timestamp descending (most recent first)
431
+ events.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
432
+ // Get total before pagination
433
+ const total = events.length;
434
+ // Apply pagination
435
+ const offset = query.offset ?? 0;
436
+ const limit = query.limit ?? events.length;
437
+ const hasMore = offset + limit < total;
438
+ events = events.slice(offset, offset + limit);
439
+ return {
440
+ events,
441
+ total,
442
+ hasMore,
443
+ executionTime: Date.now() - startTime,
444
+ };
445
+ }
446
+ /**
447
+ * Apply filters to events
448
+ */
449
+ applyFilters(events, query) {
450
+ return events.filter((event) => {
451
+ // Filter by event type
452
+ if (query.eventType) {
453
+ const types = Array.isArray(query.eventType)
454
+ ? query.eventType
455
+ : [query.eventType];
456
+ if (!types.includes(event.type)) {
457
+ return false;
458
+ }
459
+ }
460
+ // Filter by category
461
+ if (query.category) {
462
+ const categories = Array.isArray(query.category)
463
+ ? query.category
464
+ : [query.category];
465
+ const history = this.histories.get(event.patternId);
466
+ if (!history || !categories.includes(history.category)) {
467
+ return false;
468
+ }
469
+ }
470
+ // Filter by user
471
+ if (query.user && event.user !== query.user) {
472
+ return false;
473
+ }
474
+ // Filter by date range
475
+ if (query.after) {
476
+ const eventTime = new Date(event.timestamp).getTime();
477
+ const afterTime = new Date(query.after).getTime();
478
+ if (eventTime < afterTime) {
479
+ return false;
480
+ }
481
+ }
482
+ if (query.before) {
483
+ const eventTime = new Date(event.timestamp).getTime();
484
+ const beforeTime = new Date(query.before).getTime();
485
+ if (eventTime > beforeTime) {
486
+ return false;
487
+ }
488
+ }
489
+ return true;
490
+ });
491
+ }
492
+ // ==========================================================================
493
+ // Convenience Query Methods
494
+ // ==========================================================================
495
+ /**
496
+ * Get all history events
497
+ */
498
+ getAllEvents() {
499
+ return this.query().events;
500
+ }
501
+ /**
502
+ * Get events by type
503
+ */
504
+ getEventsByType(eventType) {
505
+ return this.query({ eventType }).events;
506
+ }
507
+ /**
508
+ * Get events by category
509
+ */
510
+ getEventsByCategory(category) {
511
+ return this.query({ category }).events;
512
+ }
513
+ /**
514
+ * Get events in date range
515
+ */
516
+ getEventsInDateRange(after, before) {
517
+ return this.query({ after, before }).events;
518
+ }
519
+ /**
520
+ * Get recent events
521
+ *
522
+ * @param limit - Maximum number of events to return
523
+ */
524
+ getRecentEvents(limit = 50) {
525
+ return this.query({ limit }).events;
526
+ }
527
+ /**
528
+ * Get approval history
529
+ */
530
+ getApprovalHistory() {
531
+ return this.getEventsByType('approved');
532
+ }
533
+ /**
534
+ * Get events by user
535
+ */
536
+ getEventsByUser(user) {
537
+ return this.query({ user }).events;
538
+ }
539
+ // ==========================================================================
540
+ // Pruning
541
+ // ==========================================================================
542
+ /**
543
+ * Prune history for a specific pattern
544
+ *
545
+ * Removes old entries based on maxEntriesPerPattern and maxAgeDays config.
546
+ *
547
+ * @param patternId - Pattern ID to prune
548
+ */
549
+ prunePatternHistory(patternId) {
550
+ const history = this.histories.get(patternId);
551
+ if (!history) {
552
+ return;
553
+ }
554
+ const now = Date.now();
555
+ const maxAgeMs = this.config.maxAgeDays * 24 * 60 * 60 * 1000;
556
+ // Filter out old events
557
+ history.events = history.events.filter((event) => {
558
+ const eventTime = new Date(event.timestamp).getTime();
559
+ return now - eventTime < maxAgeMs;
560
+ });
561
+ // Limit number of entries
562
+ if (history.events.length > this.config.maxEntriesPerPattern) {
563
+ // Keep most recent events
564
+ history.events = history.events
565
+ .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
566
+ .slice(0, this.config.maxEntriesPerPattern);
567
+ }
568
+ }
569
+ /**
570
+ * Prune all history
571
+ *
572
+ * Removes old entries based on maxEntriesPerPattern and maxAgeDays config.
573
+ */
574
+ prune() {
575
+ for (const patternId of this.histories.keys()) {
576
+ this.prunePatternHistory(patternId);
577
+ }
578
+ // Remove empty histories
579
+ for (const [patternId, history] of this.histories.entries()) {
580
+ if (history.events.length === 0) {
581
+ this.histories.delete(patternId);
582
+ }
583
+ }
584
+ this.dirty = true;
585
+ this.emitEvent('history:pruned', undefined, { count: this.histories.size });
586
+ this.scheduleAutoSave();
587
+ }
588
+ /**
589
+ * Delete history for a specific pattern
590
+ *
591
+ * @param patternId - Pattern ID
592
+ * @returns True if history was deleted
593
+ */
594
+ deletePatternHistory(patternId) {
595
+ const deleted = this.histories.delete(patternId);
596
+ if (deleted) {
597
+ this.dirty = true;
598
+ this.scheduleAutoSave();
599
+ }
600
+ return deleted;
601
+ }
602
+ // ==========================================================================
603
+ // Statistics
604
+ // ==========================================================================
605
+ /**
606
+ * Get statistics about the history store
607
+ */
608
+ getStats() {
609
+ const eventsByType = {
610
+ created: 0,
611
+ approved: 0,
612
+ ignored: 0,
613
+ updated: 0,
614
+ deleted: 0,
615
+ confidence_changed: 0,
616
+ locations_changed: 0,
617
+ severity_changed: 0,
618
+ };
619
+ let totalEvents = 0;
620
+ let oldestEvent = null;
621
+ let newestEvent = null;
622
+ for (const history of this.histories.values()) {
623
+ for (const event of history.events) {
624
+ totalEvents++;
625
+ eventsByType[event.type]++;
626
+ if (!oldestEvent || event.timestamp < oldestEvent) {
627
+ oldestEvent = event.timestamp;
628
+ }
629
+ if (!newestEvent || event.timestamp > newestEvent) {
630
+ newestEvent = event.timestamp;
631
+ }
632
+ }
633
+ }
634
+ return {
635
+ totalPatterns: this.histories.size,
636
+ totalEvents,
637
+ eventsByType,
638
+ oldestEvent,
639
+ newestEvent,
640
+ };
641
+ }
642
+ // ==========================================================================
643
+ // Event Handling
644
+ // ==========================================================================
645
+ /**
646
+ * Emit a history store event
647
+ */
648
+ emitEvent(type, patternId, data) {
649
+ const event = {
650
+ type,
651
+ timestamp: new Date().toISOString(),
652
+ };
653
+ if (patternId !== undefined) {
654
+ event.patternId = patternId;
655
+ }
656
+ if (data !== undefined) {
657
+ event.data = data;
658
+ }
659
+ this.emit(type, event);
660
+ this.emit('*', event); // Wildcard for all events
661
+ }
662
+ // ==========================================================================
663
+ // Utility Methods
664
+ // ==========================================================================
665
+ /**
666
+ * Get the number of patterns with history
667
+ */
668
+ get size() {
669
+ return this.histories.size;
670
+ }
671
+ /**
672
+ * Check if the store has been loaded
673
+ */
674
+ get isLoaded() {
675
+ return this.loaded;
676
+ }
677
+ /**
678
+ * Check if there are unsaved changes
679
+ */
680
+ get isDirty() {
681
+ return this.dirty;
682
+ }
683
+ /**
684
+ * Get the history directory path
685
+ */
686
+ get path() {
687
+ return this.historyDir;
688
+ }
689
+ /**
690
+ * Clear all history from memory (does not affect disk)
691
+ */
692
+ clear() {
693
+ this.histories.clear();
694
+ this.dirty = true;
695
+ }
696
+ /**
697
+ * Dispose of the history store
698
+ */
699
+ dispose() {
700
+ if (this.saveTimeout) {
701
+ clearTimeout(this.saveTimeout);
702
+ this.saveTimeout = null;
703
+ }
704
+ this.removeAllListeners();
705
+ }
706
+ }
707
+ //# sourceMappingURL=history-store.js.map