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,936 @@
1
+ /**
2
+ * Pattern Store - Pattern persistence and querying
3
+ *
4
+ * Loads and saves patterns to .drift/patterns/ directory.
5
+ * Supports querying by category, confidence, and status.
6
+ * Handles pattern state transitions (discovered → approved/ignored).
7
+ *
8
+ * @requirements 4.1 - THE Pattern_Store SHALL persist patterns as JSON in .drift/patterns/ directory
9
+ * @requirements 4.3 - WHEN a pattern is approved, THE Pattern_Store SHALL move it from discovered/ to approved/
10
+ * @requirements 4.6 - THE Pattern_Store SHALL support querying patterns by category, confidence, and status
11
+ */
12
+ import * as fs from 'node:fs/promises';
13
+ import * as path from 'node:path';
14
+ import * as crypto from 'node:crypto';
15
+ import { EventEmitter } from 'node:events';
16
+ import { PATTERN_CATEGORIES, PATTERN_FILE_VERSION, DEFAULT_PATTERN_STORE_CONFIG, } from './types.js';
17
+ import { validatePatternFile, validateSinglePattern, SchemaValidationError, } from './schema-validator.js';
18
+ // ============================================================================
19
+ // Constants
20
+ // ============================================================================
21
+ /** Directory name for drift configuration */
22
+ const DRIFT_DIR = '.drift';
23
+ /** Directory name for patterns */
24
+ const PATTERNS_DIR = 'patterns';
25
+ /** Status subdirectories */
26
+ const STATUS_DIRS = {
27
+ discovered: 'discovered',
28
+ approved: 'approved',
29
+ ignored: 'ignored',
30
+ };
31
+ /** Valid state transitions for patterns */
32
+ const VALID_TRANSITIONS = {
33
+ discovered: ['approved', 'ignored'],
34
+ approved: ['ignored'],
35
+ ignored: ['approved'],
36
+ };
37
+ // ============================================================================
38
+ // Error Classes
39
+ // ============================================================================
40
+ /**
41
+ * Error thrown when a pattern is not found
42
+ */
43
+ export class PatternNotFoundError extends Error {
44
+ patternId;
45
+ category;
46
+ constructor(patternId, category) {
47
+ super(`Pattern not found: ${patternId}${category ? ` in category ${category}` : ''}`);
48
+ this.patternId = patternId;
49
+ this.category = category;
50
+ this.name = 'PatternNotFoundError';
51
+ }
52
+ }
53
+ /**
54
+ * Error thrown when an invalid state transition is attempted
55
+ */
56
+ export class InvalidStateTransitionError extends Error {
57
+ patternId;
58
+ fromStatus;
59
+ toStatus;
60
+ constructor(patternId, fromStatus, toStatus) {
61
+ super(`Invalid state transition for pattern ${patternId}: ${fromStatus} → ${toStatus}`);
62
+ this.patternId = patternId;
63
+ this.fromStatus = fromStatus;
64
+ this.toStatus = toStatus;
65
+ this.name = 'InvalidStateTransitionError';
66
+ }
67
+ }
68
+ /**
69
+ * Error thrown when a pattern store operation fails
70
+ */
71
+ export class PatternStoreError extends Error {
72
+ errorCause;
73
+ constructor(message, errorCause) {
74
+ super(message);
75
+ this.name = 'PatternStoreError';
76
+ this.errorCause = errorCause;
77
+ }
78
+ }
79
+ // ============================================================================
80
+ // Helper Functions
81
+ // ============================================================================
82
+ /**
83
+ * Convert a Pattern to StoredPattern (removes category and status)
84
+ */
85
+ function patternToStored(pattern) {
86
+ const { category, status, ...stored } = pattern;
87
+ return stored;
88
+ }
89
+ /**
90
+ * Convert a StoredPattern to Pattern (adds category and status)
91
+ */
92
+ function storedToPattern(stored, category, status) {
93
+ return {
94
+ ...stored,
95
+ category,
96
+ status,
97
+ };
98
+ }
99
+ /**
100
+ * Generate a checksum for a pattern file
101
+ */
102
+ function generateChecksum(patterns) {
103
+ const content = JSON.stringify(patterns);
104
+ return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
105
+ }
106
+ /**
107
+ * Check if a file exists
108
+ */
109
+ async function fileExists(filePath) {
110
+ try {
111
+ await fs.access(filePath);
112
+ return true;
113
+ }
114
+ catch {
115
+ return false;
116
+ }
117
+ }
118
+ /**
119
+ * Ensure a directory exists
120
+ */
121
+ async function ensureDir(dirPath) {
122
+ await fs.mkdir(dirPath, { recursive: true });
123
+ }
124
+ // ============================================================================
125
+ // Pattern Store Class
126
+ // ============================================================================
127
+ /**
128
+ * Pattern Store - Manages pattern persistence and querying
129
+ *
130
+ * Patterns are stored in .drift/patterns/ directory organized by status:
131
+ * - .drift/patterns/discovered/ - Patterns found but not yet reviewed
132
+ * - .drift/patterns/approved/ - User-approved patterns (enforced)
133
+ * - .drift/patterns/ignored/ - Patterns explicitly ignored by user
134
+ *
135
+ * Each status directory contains JSON files named by category (e.g., structural.json).
136
+ *
137
+ * @requirements 4.1 - Patterns persisted as JSON in .drift/patterns/
138
+ * @requirements 4.3 - Patterns move between status directories on approval/ignore
139
+ * @requirements 4.6 - Patterns queryable by category, confidence, status
140
+ */
141
+ export class PatternStore extends EventEmitter {
142
+ config;
143
+ patternsDir;
144
+ patterns = new Map();
145
+ loaded = false;
146
+ dirty = false;
147
+ saveTimeout = null;
148
+ constructor(config = {}) {
149
+ super();
150
+ this.config = { ...DEFAULT_PATTERN_STORE_CONFIG, ...config };
151
+ this.patternsDir = path.join(this.config.rootDir, DRIFT_DIR, PATTERNS_DIR);
152
+ }
153
+ // ==========================================================================
154
+ // Initialization
155
+ // ==========================================================================
156
+ /**
157
+ * Initialize the pattern store
158
+ *
159
+ * Creates necessary directories and loads existing patterns.
160
+ */
161
+ async initialize() {
162
+ // Create directory structure
163
+ await this.ensureDirectoryStructure();
164
+ // Load all patterns
165
+ await this.loadAll();
166
+ this.loaded = true;
167
+ }
168
+ /**
169
+ * Ensure the directory structure exists
170
+ */
171
+ async ensureDirectoryStructure() {
172
+ for (const status of Object.values(STATUS_DIRS)) {
173
+ await ensureDir(path.join(this.patternsDir, status));
174
+ }
175
+ }
176
+ // ==========================================================================
177
+ // Loading
178
+ // ==========================================================================
179
+ /**
180
+ * Load all patterns from disk
181
+ *
182
+ * @requirements 4.1 - Load patterns from .drift/patterns/
183
+ */
184
+ async loadAll() {
185
+ this.patterns.clear();
186
+ for (const status of Object.keys(STATUS_DIRS)) {
187
+ await this.loadByStatus(status);
188
+ }
189
+ this.emitEvent('file:loaded', undefined, undefined, { count: this.patterns.size });
190
+ }
191
+ /**
192
+ * Load patterns for a specific status
193
+ */
194
+ async loadByStatus(status) {
195
+ const statusDir = path.join(this.patternsDir, STATUS_DIRS[status]);
196
+ for (const category of PATTERN_CATEGORIES) {
197
+ await this.loadCategoryFile(category, status, statusDir);
198
+ }
199
+ }
200
+ /**
201
+ * Load a single category file
202
+ */
203
+ async loadCategoryFile(category, status, statusDir) {
204
+ const filePath = path.join(statusDir, `${category}.json`);
205
+ if (!(await fileExists(filePath))) {
206
+ return;
207
+ }
208
+ try {
209
+ const content = await fs.readFile(filePath, 'utf-8');
210
+ const data = JSON.parse(content);
211
+ // Validate if enabled
212
+ if (this.config.validateSchema) {
213
+ const result = validatePatternFile(data);
214
+ if (!result.valid) {
215
+ throw new SchemaValidationError(`Invalid pattern file: ${filePath}`, result.errors, 'PatternFile');
216
+ }
217
+ }
218
+ const patternFile = data;
219
+ // Convert stored patterns to full patterns and add to map
220
+ for (const stored of patternFile.patterns) {
221
+ const pattern = storedToPattern(stored, category, status);
222
+ this.patterns.set(pattern.id, pattern);
223
+ }
224
+ }
225
+ catch (error) {
226
+ if (error.code === 'ENOENT') {
227
+ return; // File doesn't exist, skip
228
+ }
229
+ throw new PatternStoreError(`Failed to load pattern file: ${filePath}`, error);
230
+ }
231
+ }
232
+ // ==========================================================================
233
+ // Saving
234
+ // ==========================================================================
235
+ /**
236
+ * Save all patterns to disk
237
+ *
238
+ * @requirements 4.1 - Persist patterns as JSON in .drift/patterns/
239
+ */
240
+ async saveAll() {
241
+ // Group patterns by status and category
242
+ const grouped = this.groupPatternsByStatusAndCategory();
243
+ for (const [status, categories] of Array.from(grouped.entries())) {
244
+ for (const [category, patterns] of Array.from(categories.entries())) {
245
+ await this.saveCategoryFile(category, status, patterns);
246
+ }
247
+ }
248
+ this.dirty = false;
249
+ this.emitEvent('file:saved', undefined, undefined, { count: this.patterns.size });
250
+ }
251
+ /**
252
+ * Group patterns by status and category
253
+ */
254
+ groupPatternsByStatusAndCategory() {
255
+ const grouped = new Map();
256
+ for (const status of Object.keys(STATUS_DIRS)) {
257
+ grouped.set(status, new Map());
258
+ for (const category of PATTERN_CATEGORIES) {
259
+ grouped.get(status).set(category, []);
260
+ }
261
+ }
262
+ for (const pattern of Array.from(this.patterns.values())) {
263
+ grouped.get(pattern.status).get(pattern.category).push(pattern);
264
+ }
265
+ return grouped;
266
+ }
267
+ /**
268
+ * Save a single category file
269
+ */
270
+ async saveCategoryFile(category, status, patterns) {
271
+ const statusDir = path.join(this.patternsDir, STATUS_DIRS[status]);
272
+ const filePath = path.join(statusDir, `${category}.json`);
273
+ // If no patterns, remove the file if it exists
274
+ if (patterns.length === 0) {
275
+ if (await fileExists(filePath)) {
276
+ await fs.unlink(filePath);
277
+ }
278
+ return;
279
+ }
280
+ // Convert to stored patterns
281
+ const storedPatterns = patterns.map(patternToStored);
282
+ // Create pattern file
283
+ const patternFile = {
284
+ version: PATTERN_FILE_VERSION,
285
+ category,
286
+ patterns: storedPatterns,
287
+ lastUpdated: new Date().toISOString(),
288
+ checksum: generateChecksum(storedPatterns),
289
+ };
290
+ // Validate if enabled
291
+ if (this.config.validateSchema) {
292
+ const result = validatePatternFile(patternFile);
293
+ if (!result.valid) {
294
+ throw new SchemaValidationError(`Invalid pattern file before save: ${filePath}`, result.errors, 'PatternFile');
295
+ }
296
+ }
297
+ // Create backup if enabled
298
+ if (this.config.createBackup && (await fileExists(filePath))) {
299
+ await this.createBackup(filePath);
300
+ }
301
+ // Ensure directory exists
302
+ await ensureDir(statusDir);
303
+ // Write file
304
+ await fs.writeFile(filePath, JSON.stringify(patternFile, null, 2));
305
+ }
306
+ /**
307
+ * Create a backup of a file
308
+ */
309
+ async createBackup(filePath) {
310
+ const backupDir = path.join(path.dirname(filePath), '.backups');
311
+ await ensureDir(backupDir);
312
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
313
+ const backupPath = path.join(backupDir, `${path.basename(filePath, '.json')}-${timestamp}.json`);
314
+ await fs.copyFile(filePath, backupPath);
315
+ // Clean up old backups
316
+ await this.cleanupBackups(backupDir, path.basename(filePath, '.json'));
317
+ }
318
+ /**
319
+ * Clean up old backups, keeping only the most recent ones
320
+ */
321
+ async cleanupBackups(backupDir, prefix) {
322
+ try {
323
+ const files = await fs.readdir(backupDir);
324
+ const backups = files
325
+ .filter((f) => f.startsWith(prefix) && f.endsWith('.json'))
326
+ .sort()
327
+ .reverse();
328
+ // Remove old backups beyond maxBackups
329
+ for (const backup of backups.slice(this.config.maxBackups)) {
330
+ await fs.unlink(path.join(backupDir, backup));
331
+ }
332
+ }
333
+ catch {
334
+ // Ignore cleanup errors
335
+ }
336
+ }
337
+ /**
338
+ * Schedule an auto-save if enabled
339
+ */
340
+ scheduleAutoSave() {
341
+ if (!this.config.autoSave) {
342
+ return;
343
+ }
344
+ if (this.saveTimeout) {
345
+ clearTimeout(this.saveTimeout);
346
+ }
347
+ this.saveTimeout = setTimeout(async () => {
348
+ if (this.dirty) {
349
+ await this.saveAll();
350
+ }
351
+ }, this.config.autoSaveDebounce);
352
+ }
353
+ // ==========================================================================
354
+ // CRUD Operations
355
+ // ==========================================================================
356
+ /**
357
+ * Get a pattern by ID
358
+ *
359
+ * @param id - Pattern ID
360
+ * @returns The pattern or undefined if not found
361
+ */
362
+ get(id) {
363
+ return this.patterns.get(id);
364
+ }
365
+ /**
366
+ * Get a pattern by ID, throwing if not found
367
+ *
368
+ * @param id - Pattern ID
369
+ * @returns The pattern
370
+ * @throws PatternNotFoundError if pattern not found
371
+ */
372
+ getOrThrow(id) {
373
+ const pattern = this.patterns.get(id);
374
+ if (!pattern) {
375
+ throw new PatternNotFoundError(id);
376
+ }
377
+ return pattern;
378
+ }
379
+ /**
380
+ * Check if a pattern exists
381
+ *
382
+ * @param id - Pattern ID
383
+ * @returns True if pattern exists
384
+ */
385
+ has(id) {
386
+ return this.patterns.has(id);
387
+ }
388
+ /**
389
+ * Add a new pattern
390
+ *
391
+ * @param pattern - Pattern to add
392
+ * @throws Error if pattern with same ID already exists
393
+ */
394
+ add(pattern) {
395
+ if (this.patterns.has(pattern.id)) {
396
+ throw new PatternStoreError(`Pattern already exists: ${pattern.id}`);
397
+ }
398
+ // Validate if enabled
399
+ if (this.config.validateSchema) {
400
+ const result = validateSinglePattern(pattern);
401
+ if (!result.valid) {
402
+ throw new SchemaValidationError(`Invalid pattern: ${pattern.id}`, result.errors, 'Pattern');
403
+ }
404
+ }
405
+ this.patterns.set(pattern.id, pattern);
406
+ this.dirty = true;
407
+ this.emitEvent('pattern:created', pattern.id, pattern.category);
408
+ this.scheduleAutoSave();
409
+ }
410
+ /**
411
+ * Update an existing pattern
412
+ *
413
+ * @param id - Pattern ID
414
+ * @param updates - Partial pattern updates
415
+ * @returns The updated pattern
416
+ * @throws PatternNotFoundError if pattern not found
417
+ */
418
+ update(id, updates) {
419
+ const existing = this.getOrThrow(id);
420
+ const updated = {
421
+ ...existing,
422
+ ...updates,
423
+ id, // Ensure ID cannot be changed
424
+ };
425
+ // Validate if enabled
426
+ if (this.config.validateSchema) {
427
+ const result = validateSinglePattern(updated);
428
+ if (!result.valid) {
429
+ throw new SchemaValidationError(`Invalid pattern update: ${id}`, result.errors, 'Pattern');
430
+ }
431
+ }
432
+ this.patterns.set(id, updated);
433
+ this.dirty = true;
434
+ this.emitEvent('pattern:updated', id, updated.category);
435
+ this.scheduleAutoSave();
436
+ return updated;
437
+ }
438
+ /**
439
+ * Delete a pattern
440
+ *
441
+ * @param id - Pattern ID
442
+ * @returns True if pattern was deleted
443
+ */
444
+ delete(id) {
445
+ const pattern = this.patterns.get(id);
446
+ if (!pattern) {
447
+ return false;
448
+ }
449
+ this.patterns.delete(id);
450
+ this.dirty = true;
451
+ this.emitEvent('pattern:deleted', id, pattern.category);
452
+ this.scheduleAutoSave();
453
+ return true;
454
+ }
455
+ // ==========================================================================
456
+ // Status Transitions
457
+ // ==========================================================================
458
+ /**
459
+ * Approve a pattern (move from discovered to approved)
460
+ *
461
+ * @requirements 4.3 - Move pattern from discovered/ to approved/
462
+ *
463
+ * @param id - Pattern ID
464
+ * @param approvedBy - User who approved the pattern
465
+ * @returns The updated pattern
466
+ * @throws PatternNotFoundError if pattern not found
467
+ * @throws InvalidStateTransitionError if transition is invalid
468
+ */
469
+ approve(id, approvedBy) {
470
+ return this.transitionStatus(id, 'approved', approvedBy);
471
+ }
472
+ /**
473
+ * Ignore a pattern (move to ignored)
474
+ *
475
+ * @param id - Pattern ID
476
+ * @returns The updated pattern
477
+ * @throws PatternNotFoundError if pattern not found
478
+ * @throws InvalidStateTransitionError if transition is invalid
479
+ */
480
+ ignore(id) {
481
+ return this.transitionStatus(id, 'ignored');
482
+ }
483
+ /**
484
+ * Transition a pattern to a new status
485
+ *
486
+ * @requirements 4.3 - Move patterns between status directories
487
+ *
488
+ * @param id - Pattern ID
489
+ * @param newStatus - Target status
490
+ * @param user - User performing the transition
491
+ * @returns The updated pattern
492
+ */
493
+ transitionStatus(id, newStatus, user) {
494
+ const pattern = this.getOrThrow(id);
495
+ const currentStatus = pattern.status;
496
+ // Validate transition
497
+ if (!VALID_TRANSITIONS[currentStatus].includes(newStatus)) {
498
+ throw new InvalidStateTransitionError(id, currentStatus, newStatus);
499
+ }
500
+ // Update pattern
501
+ const now = new Date().toISOString();
502
+ const updatedMetadata = {
503
+ ...pattern.metadata,
504
+ lastSeen: now,
505
+ };
506
+ if (newStatus === 'approved') {
507
+ updatedMetadata.approvedAt = now;
508
+ if (user) {
509
+ updatedMetadata.approvedBy = user;
510
+ }
511
+ }
512
+ const updated = {
513
+ ...pattern,
514
+ status: newStatus,
515
+ metadata: updatedMetadata,
516
+ };
517
+ this.patterns.set(id, updated);
518
+ this.dirty = true;
519
+ // Emit appropriate event
520
+ if (newStatus === 'approved') {
521
+ this.emitEvent('pattern:approved', id, pattern.category);
522
+ }
523
+ else if (newStatus === 'ignored') {
524
+ this.emitEvent('pattern:ignored', id, pattern.category);
525
+ }
526
+ this.scheduleAutoSave();
527
+ return updated;
528
+ }
529
+ // ==========================================================================
530
+ // Querying
531
+ // ==========================================================================
532
+ /**
533
+ * Query patterns with filtering, sorting, and pagination
534
+ *
535
+ * @requirements 4.6 - Support querying by category, confidence, status
536
+ *
537
+ * @param options - Query options
538
+ * @returns Query result with matching patterns
539
+ */
540
+ query(options = {}) {
541
+ const startTime = Date.now();
542
+ const { filter, sort, pagination } = options;
543
+ // Start with all patterns
544
+ let results = Array.from(this.patterns.values());
545
+ // Apply filters
546
+ if (filter) {
547
+ results = this.applyFilters(results, filter);
548
+ }
549
+ // Get total before pagination
550
+ const total = results.length;
551
+ // Apply sorting
552
+ if (sort) {
553
+ results = this.applySorting(results, sort);
554
+ }
555
+ // Apply pagination
556
+ const offset = pagination?.offset ?? 0;
557
+ const limit = pagination?.limit ?? results.length;
558
+ const hasMore = offset + limit < total;
559
+ results = results.slice(offset, offset + limit);
560
+ return {
561
+ patterns: results,
562
+ total,
563
+ hasMore,
564
+ executionTime: Date.now() - startTime,
565
+ };
566
+ }
567
+ /**
568
+ * Apply filters to patterns
569
+ */
570
+ applyFilters(patterns, filter) {
571
+ return patterns.filter((pattern) => {
572
+ // Filter by IDs
573
+ if (filter.ids && !filter.ids.includes(pattern.id)) {
574
+ return false;
575
+ }
576
+ // Filter by category
577
+ if (filter.category) {
578
+ const categories = Array.isArray(filter.category) ? filter.category : [filter.category];
579
+ if (!categories.includes(pattern.category)) {
580
+ return false;
581
+ }
582
+ }
583
+ // Filter by subcategory
584
+ if (filter.subcategory) {
585
+ const subcategories = Array.isArray(filter.subcategory)
586
+ ? filter.subcategory
587
+ : [filter.subcategory];
588
+ if (!subcategories.includes(pattern.subcategory)) {
589
+ return false;
590
+ }
591
+ }
592
+ // Filter by status
593
+ if (filter.status) {
594
+ const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
595
+ if (!statuses.includes(pattern.status)) {
596
+ return false;
597
+ }
598
+ }
599
+ // Filter by confidence score range
600
+ if (filter.minConfidence !== undefined && pattern.confidence.score < filter.minConfidence) {
601
+ return false;
602
+ }
603
+ if (filter.maxConfidence !== undefined && pattern.confidence.score > filter.maxConfidence) {
604
+ return false;
605
+ }
606
+ // Filter by confidence level
607
+ if (filter.confidenceLevel) {
608
+ const levels = Array.isArray(filter.confidenceLevel)
609
+ ? filter.confidenceLevel
610
+ : [filter.confidenceLevel];
611
+ if (!levels.includes(pattern.confidence.level)) {
612
+ return false;
613
+ }
614
+ }
615
+ // Filter by severity
616
+ if (filter.severity) {
617
+ const severities = Array.isArray(filter.severity) ? filter.severity : [filter.severity];
618
+ if (!severities.includes(pattern.severity)) {
619
+ return false;
620
+ }
621
+ }
622
+ // Filter by auto-fixable
623
+ if (filter.autoFixable !== undefined && pattern.autoFixable !== filter.autoFixable) {
624
+ return false;
625
+ }
626
+ // Filter by file
627
+ if (filter.file) {
628
+ const hasFile = pattern.locations.some((loc) => loc.file === filter.file);
629
+ if (!hasFile) {
630
+ return false;
631
+ }
632
+ }
633
+ // Filter by files
634
+ if (filter.files && filter.files.length > 0) {
635
+ const hasAnyFile = pattern.locations.some((loc) => filter.files.includes(loc.file));
636
+ if (!hasAnyFile) {
637
+ return false;
638
+ }
639
+ }
640
+ // Filter by outliers
641
+ if (filter.hasOutliers !== undefined) {
642
+ const hasOutliers = pattern.outliers.length > 0;
643
+ if (filter.hasOutliers !== hasOutliers) {
644
+ return false;
645
+ }
646
+ }
647
+ // Filter by minimum outlier count
648
+ if (filter.minOutliers !== undefined && pattern.outliers.length < filter.minOutliers) {
649
+ return false;
650
+ }
651
+ // Filter by tags
652
+ if (filter.tags && filter.tags.length > 0) {
653
+ const patternTags = pattern.metadata.tags ?? [];
654
+ const hasAllTags = filter.tags.every((tag) => patternTags.includes(tag));
655
+ if (!hasAllTags) {
656
+ return false;
657
+ }
658
+ }
659
+ // Filter by source
660
+ if (filter.source && pattern.metadata.source !== filter.source) {
661
+ return false;
662
+ }
663
+ // Search in name and description
664
+ if (filter.search) {
665
+ const searchLower = filter.search.toLowerCase();
666
+ const nameMatch = pattern.name.toLowerCase().includes(searchLower);
667
+ const descMatch = pattern.description.toLowerCase().includes(searchLower);
668
+ if (!nameMatch && !descMatch) {
669
+ return false;
670
+ }
671
+ }
672
+ // Filter by date ranges
673
+ if (filter.createdAfter) {
674
+ const firstSeen = new Date(pattern.metadata.firstSeen);
675
+ const after = new Date(filter.createdAfter);
676
+ if (firstSeen < after) {
677
+ return false;
678
+ }
679
+ }
680
+ if (filter.createdBefore) {
681
+ const firstSeen = new Date(pattern.metadata.firstSeen);
682
+ const before = new Date(filter.createdBefore);
683
+ if (firstSeen > before) {
684
+ return false;
685
+ }
686
+ }
687
+ if (filter.seenAfter) {
688
+ const lastSeen = new Date(pattern.metadata.lastSeen);
689
+ const after = new Date(filter.seenAfter);
690
+ if (lastSeen < after) {
691
+ return false;
692
+ }
693
+ }
694
+ if (filter.seenBefore) {
695
+ const lastSeen = new Date(pattern.metadata.lastSeen);
696
+ const before = new Date(filter.seenBefore);
697
+ if (lastSeen > before) {
698
+ return false;
699
+ }
700
+ }
701
+ return true;
702
+ });
703
+ }
704
+ /**
705
+ * Apply sorting to patterns
706
+ */
707
+ applySorting(patterns, sort) {
708
+ const { field, direction } = sort;
709
+ const multiplier = direction === 'asc' ? 1 : -1;
710
+ return [...patterns].sort((a, b) => {
711
+ let comparison = 0;
712
+ switch (field) {
713
+ case 'name':
714
+ comparison = a.name.localeCompare(b.name);
715
+ break;
716
+ case 'confidence':
717
+ comparison = a.confidence.score - b.confidence.score;
718
+ break;
719
+ case 'severity':
720
+ const severityOrder = {
721
+ error: 4,
722
+ warning: 3,
723
+ info: 2,
724
+ hint: 1,
725
+ };
726
+ comparison = severityOrder[a.severity] - severityOrder[b.severity];
727
+ break;
728
+ case 'firstSeen':
729
+ comparison =
730
+ new Date(a.metadata.firstSeen).getTime() -
731
+ new Date(b.metadata.firstSeen).getTime();
732
+ break;
733
+ case 'lastSeen':
734
+ comparison =
735
+ new Date(a.metadata.lastSeen).getTime() -
736
+ new Date(b.metadata.lastSeen).getTime();
737
+ break;
738
+ case 'outlierCount':
739
+ comparison = a.outliers.length - b.outliers.length;
740
+ break;
741
+ case 'locationCount':
742
+ comparison = a.locations.length - b.locations.length;
743
+ break;
744
+ }
745
+ return comparison * multiplier;
746
+ });
747
+ }
748
+ // ==========================================================================
749
+ // Convenience Query Methods
750
+ // ==========================================================================
751
+ /**
752
+ * Get all patterns
753
+ */
754
+ getAll() {
755
+ return Array.from(this.patterns.values());
756
+ }
757
+ /**
758
+ * Get patterns by category
759
+ *
760
+ * @requirements 4.6 - Query by category
761
+ */
762
+ getByCategory(category) {
763
+ return this.query({ filter: { category } }).patterns;
764
+ }
765
+ /**
766
+ * Get patterns by status
767
+ *
768
+ * @requirements 4.6 - Query by status
769
+ */
770
+ getByStatus(status) {
771
+ return this.query({ filter: { status } }).patterns;
772
+ }
773
+ /**
774
+ * Get patterns by confidence level
775
+ *
776
+ * @requirements 4.6 - Query by confidence
777
+ */
778
+ getByConfidenceLevel(level) {
779
+ return this.query({ filter: { confidenceLevel: level } }).patterns;
780
+ }
781
+ /**
782
+ * Get patterns with minimum confidence score
783
+ *
784
+ * @requirements 4.6 - Query by confidence
785
+ */
786
+ getByMinConfidence(minScore) {
787
+ return this.query({ filter: { minConfidence: minScore } }).patterns;
788
+ }
789
+ /**
790
+ * Get approved patterns
791
+ */
792
+ getApproved() {
793
+ return this.getByStatus('approved');
794
+ }
795
+ /**
796
+ * Get discovered patterns
797
+ */
798
+ getDiscovered() {
799
+ return this.getByStatus('discovered');
800
+ }
801
+ /**
802
+ * Get ignored patterns
803
+ */
804
+ getIgnored() {
805
+ return this.getByStatus('ignored');
806
+ }
807
+ /**
808
+ * Get patterns that have locations in a specific file
809
+ */
810
+ getByFile(file) {
811
+ return this.query({ filter: { file } }).patterns;
812
+ }
813
+ /**
814
+ * Get patterns with outliers
815
+ */
816
+ getWithOutliers() {
817
+ return this.query({ filter: { hasOutliers: true } }).patterns;
818
+ }
819
+ /**
820
+ * Get high confidence patterns
821
+ */
822
+ getHighConfidence() {
823
+ return this.getByConfidenceLevel('high');
824
+ }
825
+ // ==========================================================================
826
+ // Statistics
827
+ // ==========================================================================
828
+ /**
829
+ * Get statistics about the pattern store
830
+ */
831
+ getStats() {
832
+ const patterns = Array.from(this.patterns.values());
833
+ const byStatus = {
834
+ discovered: 0,
835
+ approved: 0,
836
+ ignored: 0,
837
+ };
838
+ const byCategory = {};
839
+ for (const category of PATTERN_CATEGORIES) {
840
+ byCategory[category] = 0;
841
+ }
842
+ const byConfidenceLevel = {
843
+ high: 0,
844
+ medium: 0,
845
+ low: 0,
846
+ uncertain: 0,
847
+ };
848
+ let totalLocations = 0;
849
+ let totalOutliers = 0;
850
+ for (const pattern of patterns) {
851
+ byStatus[pattern.status]++;
852
+ byCategory[pattern.category]++;
853
+ byConfidenceLevel[pattern.confidence.level]++;
854
+ totalLocations += pattern.locations.length;
855
+ totalOutliers += pattern.outliers.length;
856
+ }
857
+ return {
858
+ totalPatterns: patterns.length,
859
+ byStatus,
860
+ byCategory,
861
+ byConfidenceLevel,
862
+ totalLocations,
863
+ totalOutliers,
864
+ totalVariants: 0, // Variants are managed separately
865
+ lastUpdated: new Date().toISOString(),
866
+ };
867
+ }
868
+ // ==========================================================================
869
+ // Event Handling
870
+ // ==========================================================================
871
+ /**
872
+ * Emit a pattern store event
873
+ */
874
+ emitEvent(type, patternId, category, data) {
875
+ const event = {
876
+ type,
877
+ timestamp: new Date().toISOString(),
878
+ };
879
+ if (patternId !== undefined) {
880
+ event.patternId = patternId;
881
+ }
882
+ if (category !== undefined) {
883
+ event.category = category;
884
+ }
885
+ if (data !== undefined) {
886
+ event.data = data;
887
+ }
888
+ this.emit(type, event);
889
+ this.emit('*', event); // Wildcard for all events
890
+ }
891
+ // ==========================================================================
892
+ // Utility Methods
893
+ // ==========================================================================
894
+ /**
895
+ * Get the number of patterns in the store
896
+ */
897
+ get size() {
898
+ return this.patterns.size;
899
+ }
900
+ /**
901
+ * Check if the store has been loaded
902
+ */
903
+ get isLoaded() {
904
+ return this.loaded;
905
+ }
906
+ /**
907
+ * Check if there are unsaved changes
908
+ */
909
+ get isDirty() {
910
+ return this.dirty;
911
+ }
912
+ /**
913
+ * Get the patterns directory path
914
+ */
915
+ get path() {
916
+ return this.patternsDir;
917
+ }
918
+ /**
919
+ * Clear all patterns from memory (does not affect disk)
920
+ */
921
+ clear() {
922
+ this.patterns.clear();
923
+ this.dirty = true;
924
+ }
925
+ /**
926
+ * Dispose of the pattern store
927
+ */
928
+ dispose() {
929
+ if (this.saveTimeout) {
930
+ clearTimeout(this.saveTimeout);
931
+ this.saveTimeout = null;
932
+ }
933
+ this.removeAllListeners();
934
+ }
935
+ }
936
+ //# sourceMappingURL=pattern-store.js.map