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,777 @@
1
+ /**
2
+ * Variant Manager - Manages intentional deviations from patterns
3
+ *
4
+ * Variants allow developers to mark code as intentionally deviating from
5
+ * established patterns. Once a variant is created, the enforcement system
6
+ * will stop flagging matching code.
7
+ *
8
+ * @requirements 26.1 - THE Variant_System SHALL allow creating named variants of patterns
9
+ * @requirements 26.2 - THE Variant SHALL specify scope: global, directory, or file
10
+ * @requirements 26.3 - THE Variant SHALL include a reason explaining why it's intentional
11
+ * @requirements 26.5 - THE Variant_System SHALL store variants in .drift/patterns/variants/
12
+ */
13
+ import * as fs from 'node:fs/promises';
14
+ import * as path from 'node:path';
15
+ import * as crypto from 'node:crypto';
16
+ import { EventEmitter } from 'node:events';
17
+ import { VARIANTS_FILE_VERSION } from '../store/types.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
+ /** Directory name for variants */
26
+ const VARIANTS_DIR = 'variants';
27
+ /** File name for variants index */
28
+ const VARIANTS_INDEX_FILE = 'index.json';
29
+ /**
30
+ * Default variant manager configuration
31
+ */
32
+ export const DEFAULT_VARIANT_MANAGER_CONFIG = {
33
+ rootDir: '.',
34
+ autoSave: false,
35
+ autoSaveDebounce: 1000,
36
+ createBackup: true,
37
+ maxBackups: 5,
38
+ };
39
+ // ============================================================================
40
+ // Error Classes
41
+ // ============================================================================
42
+ /**
43
+ * Error thrown when a variant is not found
44
+ */
45
+ export class VariantNotFoundError extends Error {
46
+ variantId;
47
+ constructor(variantId) {
48
+ super(`Variant not found: ${variantId}`);
49
+ this.variantId = variantId;
50
+ this.name = 'VariantNotFoundError';
51
+ }
52
+ }
53
+ /**
54
+ * Error thrown when a variant operation fails
55
+ */
56
+ export class VariantManagerError extends Error {
57
+ errorCause;
58
+ constructor(message, errorCause) {
59
+ super(message);
60
+ this.name = 'VariantManagerError';
61
+ this.errorCause = errorCause;
62
+ }
63
+ }
64
+ /**
65
+ * Error thrown when variant input is invalid
66
+ */
67
+ export class InvalidVariantInputError extends Error {
68
+ field;
69
+ constructor(message, field) {
70
+ super(message);
71
+ this.field = field;
72
+ this.name = 'InvalidVariantInputError';
73
+ }
74
+ }
75
+ // ============================================================================
76
+ // Helper Functions
77
+ // ============================================================================
78
+ /**
79
+ * Generate a unique variant ID
80
+ */
81
+ function generateVariantId() {
82
+ const timestamp = Date.now().toString(36);
83
+ const random = crypto.randomBytes(4).toString('hex');
84
+ return `var_${timestamp}_${random}`;
85
+ }
86
+ /**
87
+ * Check if a file exists
88
+ */
89
+ async function fileExists(filePath) {
90
+ try {
91
+ await fs.access(filePath);
92
+ return true;
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ /**
99
+ * Ensure a directory exists
100
+ */
101
+ async function ensureDir(dirPath) {
102
+ await fs.mkdir(dirPath, { recursive: true });
103
+ }
104
+ /**
105
+ * Normalize a path for comparison
106
+ */
107
+ function normalizePath(p) {
108
+ return path.normalize(p).replace(/\\/g, '/');
109
+ }
110
+ /**
111
+ * Check if a file path is within a directory
112
+ */
113
+ function isFileInDirectory(filePath, dirPath) {
114
+ const normalizedFile = normalizePath(filePath);
115
+ const normalizedDir = normalizePath(dirPath);
116
+ return normalizedFile.startsWith(normalizedDir + '/') || normalizedFile === normalizedDir;
117
+ }
118
+ // ============================================================================
119
+ // Variant Manager Class
120
+ // ============================================================================
121
+ /**
122
+ * Variant Manager - Manages intentional deviations from patterns
123
+ *
124
+ * Variants are stored in .drift/patterns/variants/ directory.
125
+ * Each variant specifies a scope (global, directory, or file) and
126
+ * includes a reason explaining why the deviation is intentional.
127
+ *
128
+ * @requirements 26.1 - Create named variants of patterns
129
+ * @requirements 26.2 - Variants specify scope: global, directory, or file
130
+ * @requirements 26.3 - Variants include reason for deviation
131
+ * @requirements 26.5 - Variants stored in .drift/patterns/variants/
132
+ */
133
+ export class VariantManager extends EventEmitter {
134
+ config;
135
+ variantsDir;
136
+ variants = new Map();
137
+ loaded = false;
138
+ dirty = false;
139
+ saveTimeout = null;
140
+ constructor(config = {}) {
141
+ super();
142
+ this.config = { ...DEFAULT_VARIANT_MANAGER_CONFIG, ...config };
143
+ this.variantsDir = path.join(this.config.rootDir, DRIFT_DIR, PATTERNS_DIR, VARIANTS_DIR);
144
+ }
145
+ // ==========================================================================
146
+ // Initialization
147
+ // ==========================================================================
148
+ /**
149
+ * Initialize the variant manager
150
+ *
151
+ * Creates necessary directories and loads existing variants.
152
+ */
153
+ async initialize() {
154
+ // Create directory structure
155
+ await this.ensureDirectoryStructure();
156
+ // Load all variants
157
+ await this.loadAll();
158
+ this.loaded = true;
159
+ }
160
+ /**
161
+ * Ensure the directory structure exists
162
+ *
163
+ * @requirements 26.5 - Store variants in .drift/patterns/variants/
164
+ */
165
+ async ensureDirectoryStructure() {
166
+ await ensureDir(this.variantsDir);
167
+ }
168
+ /**
169
+ * Check if the manager is initialized
170
+ */
171
+ isInitialized() {
172
+ return this.loaded;
173
+ }
174
+ // ==========================================================================
175
+ // Loading
176
+ // ==========================================================================
177
+ /**
178
+ * Load all variants from disk
179
+ *
180
+ * @requirements 26.5 - Load variants from .drift/patterns/variants/
181
+ */
182
+ async loadAll() {
183
+ this.variants.clear();
184
+ const indexPath = path.join(this.variantsDir, VARIANTS_INDEX_FILE);
185
+ if (!(await fileExists(indexPath))) {
186
+ this.emitEvent('file:loaded', undefined, undefined, { count: 0 });
187
+ return;
188
+ }
189
+ try {
190
+ const content = await fs.readFile(indexPath, 'utf-8');
191
+ const data = JSON.parse(content);
192
+ // Load variants from the file
193
+ for (const variant of data.variants) {
194
+ this.variants.set(variant.id, variant);
195
+ }
196
+ this.emitEvent('file:loaded', undefined, undefined, { count: this.variants.size });
197
+ }
198
+ catch (error) {
199
+ if (error.code === 'ENOENT') {
200
+ return; // File doesn't exist, skip
201
+ }
202
+ throw new VariantManagerError(`Failed to load variants file: ${indexPath}`, error);
203
+ }
204
+ }
205
+ // ==========================================================================
206
+ // Saving
207
+ // ==========================================================================
208
+ /**
209
+ * Save all variants to disk
210
+ *
211
+ * @requirements 26.5 - Persist variants in .drift/patterns/variants/
212
+ */
213
+ async saveAll() {
214
+ const indexPath = path.join(this.variantsDir, VARIANTS_INDEX_FILE);
215
+ // Create backup if enabled
216
+ if (this.config.createBackup && (await fileExists(indexPath))) {
217
+ await this.createBackup(indexPath);
218
+ }
219
+ // Ensure directory exists
220
+ await ensureDir(this.variantsDir);
221
+ // Create variants file
222
+ const variantsFile = {
223
+ version: VARIANTS_FILE_VERSION,
224
+ variants: Array.from(this.variants.values()),
225
+ lastUpdated: new Date().toISOString(),
226
+ };
227
+ // Write file
228
+ await fs.writeFile(indexPath, JSON.stringify(variantsFile, null, 2));
229
+ this.dirty = false;
230
+ this.emitEvent('file:saved', undefined, undefined, { count: this.variants.size });
231
+ }
232
+ /**
233
+ * Create a backup of a file
234
+ */
235
+ async createBackup(filePath) {
236
+ const backupDir = path.join(path.dirname(filePath), '.backups');
237
+ await ensureDir(backupDir);
238
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
239
+ const backupPath = path.join(backupDir, `${path.basename(filePath, '.json')}-${timestamp}.json`);
240
+ await fs.copyFile(filePath, backupPath);
241
+ // Clean up old backups
242
+ await this.cleanupBackups(backupDir, path.basename(filePath, '.json'));
243
+ }
244
+ /**
245
+ * Clean up old backups, keeping only the most recent ones
246
+ */
247
+ async cleanupBackups(backupDir, prefix) {
248
+ try {
249
+ const files = await fs.readdir(backupDir);
250
+ const backups = files
251
+ .filter((f) => f.startsWith(prefix) && f.endsWith('.json'))
252
+ .sort()
253
+ .reverse();
254
+ // Remove old backups beyond maxBackups
255
+ for (const backup of backups.slice(this.config.maxBackups)) {
256
+ await fs.unlink(path.join(backupDir, backup));
257
+ }
258
+ }
259
+ catch {
260
+ // Ignore cleanup errors
261
+ }
262
+ }
263
+ /**
264
+ * Schedule an auto-save if enabled
265
+ */
266
+ scheduleAutoSave() {
267
+ if (!this.config.autoSave) {
268
+ return;
269
+ }
270
+ if (this.saveTimeout) {
271
+ clearTimeout(this.saveTimeout);
272
+ }
273
+ this.saveTimeout = setTimeout(async () => {
274
+ if (this.dirty) {
275
+ await this.saveAll();
276
+ }
277
+ }, this.config.autoSaveDebounce);
278
+ }
279
+ // ==========================================================================
280
+ // CRUD Operations
281
+ // ==========================================================================
282
+ /**
283
+ * Create a new variant
284
+ *
285
+ * @requirements 26.1 - Create named variants of patterns
286
+ * @requirements 26.2 - Specify scope: global, directory, or file
287
+ * @requirements 26.3 - Include reason explaining why it's intentional
288
+ *
289
+ * @param input - Variant creation input
290
+ * @returns The created variant
291
+ */
292
+ create(input) {
293
+ // Validate input
294
+ this.validateCreateInput(input);
295
+ const now = new Date().toISOString();
296
+ const variant = {
297
+ id: generateVariantId(),
298
+ patternId: input.patternId,
299
+ name: input.name,
300
+ reason: input.reason,
301
+ scope: input.scope,
302
+ locations: input.locations,
303
+ createdAt: now,
304
+ active: true,
305
+ };
306
+ // Only add optional properties if they have values
307
+ if (input.scopeValue !== undefined) {
308
+ variant.scopeValue = input.scopeValue;
309
+ }
310
+ if (input.createdBy !== undefined) {
311
+ variant.createdBy = input.createdBy;
312
+ }
313
+ this.variants.set(variant.id, variant);
314
+ this.dirty = true;
315
+ this.emitEvent('variant:created', variant.id, variant.patternId);
316
+ this.scheduleAutoSave();
317
+ return variant;
318
+ }
319
+ /**
320
+ * Validate create input
321
+ */
322
+ validateCreateInput(input) {
323
+ if (!input.patternId || input.patternId.trim() === '') {
324
+ throw new InvalidVariantInputError('Pattern ID is required', 'patternId');
325
+ }
326
+ if (!input.name || input.name.trim() === '') {
327
+ throw new InvalidVariantInputError('Variant name is required', 'name');
328
+ }
329
+ if (!input.reason || input.reason.trim() === '') {
330
+ throw new InvalidVariantInputError('Reason is required', 'reason');
331
+ }
332
+ if (!input.scope) {
333
+ throw new InvalidVariantInputError('Scope is required', 'scope');
334
+ }
335
+ const validScopes = ['global', 'directory', 'file'];
336
+ if (!validScopes.includes(input.scope)) {
337
+ throw new InvalidVariantInputError(`Invalid scope: ${input.scope}. Must be one of: ${validScopes.join(', ')}`, 'scope');
338
+ }
339
+ // Validate scope value for non-global scopes
340
+ if (input.scope !== 'global' && (!input.scopeValue || input.scopeValue.trim() === '')) {
341
+ throw new InvalidVariantInputError(`Scope value is required for ${input.scope} scope`, 'scopeValue');
342
+ }
343
+ if (!input.locations || input.locations.length === 0) {
344
+ throw new InvalidVariantInputError('At least one location is required', 'locations');
345
+ }
346
+ }
347
+ /**
348
+ * Get a variant by ID
349
+ *
350
+ * @param id - Variant ID
351
+ * @returns The variant or undefined if not found
352
+ */
353
+ get(id) {
354
+ return this.variants.get(id);
355
+ }
356
+ /**
357
+ * Get a variant by ID, throwing if not found
358
+ *
359
+ * @param id - Variant ID
360
+ * @returns The variant
361
+ * @throws VariantNotFoundError if variant not found
362
+ */
363
+ getOrThrow(id) {
364
+ const variant = this.variants.get(id);
365
+ if (!variant) {
366
+ throw new VariantNotFoundError(id);
367
+ }
368
+ return variant;
369
+ }
370
+ /**
371
+ * Check if a variant exists
372
+ *
373
+ * @param id - Variant ID
374
+ * @returns True if variant exists
375
+ */
376
+ has(id) {
377
+ return this.variants.has(id);
378
+ }
379
+ /**
380
+ * Update an existing variant
381
+ *
382
+ * @param id - Variant ID
383
+ * @param updates - Partial variant updates
384
+ * @returns The updated variant
385
+ * @throws VariantNotFoundError if variant not found
386
+ */
387
+ update(id, updates) {
388
+ const existing = this.getOrThrow(id);
389
+ // Build the updated variant, preserving immutable fields
390
+ const updated = {
391
+ id, // Ensure ID cannot be changed
392
+ patternId: existing.patternId, // Ensure pattern ID cannot be changed
393
+ name: updates.name ?? existing.name,
394
+ reason: updates.reason ?? existing.reason,
395
+ scope: updates.scope ?? existing.scope,
396
+ locations: updates.locations ?? existing.locations,
397
+ createdAt: existing.createdAt, // Ensure creation time cannot be changed
398
+ active: updates.active ?? existing.active,
399
+ };
400
+ // Handle optional scopeValue
401
+ const newScopeValue = updates.scopeValue !== undefined ? updates.scopeValue : existing.scopeValue;
402
+ if (newScopeValue !== undefined) {
403
+ updated.scopeValue = newScopeValue;
404
+ }
405
+ // Handle optional createdBy (preserve from existing)
406
+ if (existing.createdBy !== undefined) {
407
+ updated.createdBy = existing.createdBy;
408
+ }
409
+ // Validate scope value if scope is being updated
410
+ if (updates.scope && updates.scope !== 'global') {
411
+ const scopeValue = updated.scopeValue;
412
+ if (!scopeValue || scopeValue.trim() === '') {
413
+ throw new InvalidVariantInputError(`Scope value is required for ${updates.scope} scope`, 'scopeValue');
414
+ }
415
+ }
416
+ this.variants.set(id, updated);
417
+ this.dirty = true;
418
+ this.emitEvent('variant:updated', id, updated.patternId);
419
+ this.scheduleAutoSave();
420
+ return updated;
421
+ }
422
+ /**
423
+ * Delete a variant
424
+ *
425
+ * @param id - Variant ID
426
+ * @returns True if variant was deleted
427
+ */
428
+ delete(id) {
429
+ const variant = this.variants.get(id);
430
+ if (!variant) {
431
+ return false;
432
+ }
433
+ this.variants.delete(id);
434
+ this.dirty = true;
435
+ this.emitEvent('variant:deleted', id, variant.patternId);
436
+ this.scheduleAutoSave();
437
+ return true;
438
+ }
439
+ // ==========================================================================
440
+ // Activation/Deactivation
441
+ // ==========================================================================
442
+ /**
443
+ * Activate a variant
444
+ *
445
+ * @param id - Variant ID
446
+ * @returns The updated variant
447
+ */
448
+ activate(id) {
449
+ const variant = this.getOrThrow(id);
450
+ if (variant.active) {
451
+ return variant; // Already active
452
+ }
453
+ const updated = {
454
+ ...variant,
455
+ active: true,
456
+ };
457
+ this.variants.set(id, updated);
458
+ this.dirty = true;
459
+ this.emitEvent('variant:activated', id, variant.patternId);
460
+ this.scheduleAutoSave();
461
+ return updated;
462
+ }
463
+ /**
464
+ * Deactivate a variant
465
+ *
466
+ * @param id - Variant ID
467
+ * @returns The updated variant
468
+ */
469
+ deactivate(id) {
470
+ const variant = this.getOrThrow(id);
471
+ if (!variant.active) {
472
+ return variant; // Already inactive
473
+ }
474
+ const updated = {
475
+ ...variant,
476
+ active: false,
477
+ };
478
+ this.variants.set(id, updated);
479
+ this.dirty = true;
480
+ this.emitEvent('variant:deactivated', id, variant.patternId);
481
+ this.scheduleAutoSave();
482
+ return updated;
483
+ }
484
+ // ==========================================================================
485
+ // Querying
486
+ // ==========================================================================
487
+ /**
488
+ * Query variants with filtering
489
+ *
490
+ * @param query - Query options
491
+ * @returns Matching variants
492
+ */
493
+ query(query = {}) {
494
+ let results = Array.from(this.variants.values());
495
+ // Filter by pattern ID
496
+ if (query.patternId) {
497
+ results = results.filter((v) => v.patternId === query.patternId);
498
+ }
499
+ // Filter by pattern IDs
500
+ if (query.patternIds && query.patternIds.length > 0) {
501
+ results = results.filter((v) => query.patternIds.includes(v.patternId));
502
+ }
503
+ // Filter by scope
504
+ if (query.scope) {
505
+ const scopes = Array.isArray(query.scope) ? query.scope : [query.scope];
506
+ results = results.filter((v) => scopes.includes(v.scope));
507
+ }
508
+ // Filter by active status
509
+ if (query.active !== undefined) {
510
+ results = results.filter((v) => v.active === query.active);
511
+ }
512
+ // Filter by file path
513
+ if (query.file) {
514
+ results = results.filter((v) => this.variantCoversFile(v, query.file));
515
+ }
516
+ // Filter by directory path
517
+ if (query.directory) {
518
+ results = results.filter((v) => this.variantCoversDirectory(v, query.directory));
519
+ }
520
+ // Search in name and reason
521
+ if (query.search) {
522
+ const searchLower = query.search.toLowerCase();
523
+ results = results.filter((v) => v.name.toLowerCase().includes(searchLower) ||
524
+ v.reason.toLowerCase().includes(searchLower));
525
+ }
526
+ // Filter by creator
527
+ if (query.createdBy) {
528
+ results = results.filter((v) => v.createdBy === query.createdBy);
529
+ }
530
+ return results;
531
+ }
532
+ /**
533
+ * Check if a variant covers a specific file
534
+ */
535
+ variantCoversFile(variant, filePath) {
536
+ switch (variant.scope) {
537
+ case 'global':
538
+ return true;
539
+ case 'directory':
540
+ return variant.scopeValue
541
+ ? isFileInDirectory(filePath, variant.scopeValue)
542
+ : false;
543
+ case 'file':
544
+ return variant.scopeValue
545
+ ? normalizePath(filePath) === normalizePath(variant.scopeValue)
546
+ : false;
547
+ default:
548
+ return false;
549
+ }
550
+ }
551
+ /**
552
+ * Check if a variant covers a specific directory
553
+ */
554
+ variantCoversDirectory(variant, dirPath) {
555
+ switch (variant.scope) {
556
+ case 'global':
557
+ return true;
558
+ case 'directory':
559
+ return variant.scopeValue
560
+ ? isFileInDirectory(dirPath, variant.scopeValue) ||
561
+ normalizePath(dirPath) === normalizePath(variant.scopeValue)
562
+ : false;
563
+ case 'file':
564
+ return variant.scopeValue
565
+ ? isFileInDirectory(variant.scopeValue, dirPath)
566
+ : false;
567
+ default:
568
+ return false;
569
+ }
570
+ }
571
+ // ==========================================================================
572
+ // Convenience Query Methods
573
+ // ==========================================================================
574
+ /**
575
+ * Get all variants
576
+ */
577
+ getAll() {
578
+ return Array.from(this.variants.values());
579
+ }
580
+ /**
581
+ * Get all active variants
582
+ */
583
+ getActive() {
584
+ return this.query({ active: true });
585
+ }
586
+ /**
587
+ * Get all inactive variants
588
+ */
589
+ getInactive() {
590
+ return this.query({ active: false });
591
+ }
592
+ /**
593
+ * Get variants for a specific pattern
594
+ *
595
+ * @param patternId - Pattern ID
596
+ * @returns Variants for the pattern
597
+ */
598
+ getByPatternId(patternId) {
599
+ return this.query({ patternId });
600
+ }
601
+ /**
602
+ * Get active variants for a specific pattern
603
+ *
604
+ * @param patternId - Pattern ID
605
+ * @returns Active variants for the pattern
606
+ */
607
+ getActiveByPatternId(patternId) {
608
+ return this.query({ patternId, active: true });
609
+ }
610
+ /**
611
+ * Get variants by scope
612
+ *
613
+ * @param scope - Variant scope
614
+ * @returns Variants with the specified scope
615
+ */
616
+ getByScope(scope) {
617
+ return this.query({ scope });
618
+ }
619
+ /**
620
+ * Get variants that cover a specific file
621
+ *
622
+ * @param filePath - File path
623
+ * @returns Variants that cover the file
624
+ */
625
+ getByFile(filePath) {
626
+ return this.query({ file: filePath });
627
+ }
628
+ /**
629
+ * Get active variants that cover a specific file
630
+ *
631
+ * @param filePath - File path
632
+ * @returns Active variants that cover the file
633
+ */
634
+ getActiveByFile(filePath) {
635
+ return this.query({ file: filePath, active: true });
636
+ }
637
+ // ==========================================================================
638
+ // Coverage Checking
639
+ // ==========================================================================
640
+ /**
641
+ * Check if a location is covered by any active variant for a pattern
642
+ *
643
+ * This is the primary method used by the enforcement system to determine
644
+ * if a violation should be suppressed.
645
+ *
646
+ * @requirements 26.4 - WHEN a variant is created, THE Enforcement_System SHALL stop flagging matching code
647
+ *
648
+ * @param patternId - Pattern ID
649
+ * @param location - Location to check
650
+ * @returns True if the location is covered by an active variant
651
+ */
652
+ isLocationCovered(patternId, location) {
653
+ const variants = this.getActiveByPatternId(patternId);
654
+ for (const variant of variants) {
655
+ if (this.variantCoversLocation(variant, location)) {
656
+ return true;
657
+ }
658
+ }
659
+ return false;
660
+ }
661
+ /**
662
+ * Check if a variant covers a specific location
663
+ *
664
+ * A variant covers a location if:
665
+ * 1. The file is within the variant's scope (global, directory, or file)
666
+ * 2. AND either:
667
+ * a. The variant has only one location (the "anchor" location used for validation)
668
+ * which means it covers the entire scope
669
+ * b. The variant has multiple locations that include this specific location
670
+ */
671
+ variantCoversLocation(variant, location) {
672
+ // First check if the file is in scope
673
+ if (!this.variantCoversFile(variant, location.file)) {
674
+ return false;
675
+ }
676
+ // If the variant has only one location (the anchor), it covers the entire scope
677
+ // This is the common case where a variant is created to cover all violations
678
+ // in a file/directory/globally
679
+ if (variant.locations.length === 1) {
680
+ return true;
681
+ }
682
+ // If the variant has multiple specific locations, check if this location matches
683
+ return variant.locations.some((loc) => normalizePath(loc.file) === normalizePath(location.file) &&
684
+ loc.line === location.line &&
685
+ loc.column === location.column);
686
+ }
687
+ /**
688
+ * Get the variant that covers a specific location (if any)
689
+ *
690
+ * @param patternId - Pattern ID
691
+ * @param location - Location to check
692
+ * @returns The covering variant or undefined
693
+ */
694
+ getCoveringVariant(patternId, location) {
695
+ const variants = this.getActiveByPatternId(patternId);
696
+ for (const variant of variants) {
697
+ if (this.variantCoversLocation(variant, location)) {
698
+ return variant;
699
+ }
700
+ }
701
+ return undefined;
702
+ }
703
+ // ==========================================================================
704
+ // Statistics
705
+ // ==========================================================================
706
+ /**
707
+ * Get statistics about variants
708
+ */
709
+ getStats() {
710
+ const variants = Array.from(this.variants.values());
711
+ const byScope = {
712
+ global: 0,
713
+ directory: 0,
714
+ file: 0,
715
+ };
716
+ const byPattern = {};
717
+ let activeCount = 0;
718
+ let inactiveCount = 0;
719
+ for (const variant of variants) {
720
+ byScope[variant.scope]++;
721
+ const patternCount = byPattern[variant.patternId];
722
+ byPattern[variant.patternId] = (patternCount ?? 0) + 1;
723
+ if (variant.active) {
724
+ activeCount++;
725
+ }
726
+ else {
727
+ inactiveCount++;
728
+ }
729
+ }
730
+ return {
731
+ total: variants.length,
732
+ active: activeCount,
733
+ inactive: inactiveCount,
734
+ byScope,
735
+ byPattern,
736
+ patternsWithVariants: Object.keys(byPattern).length,
737
+ };
738
+ }
739
+ // ==========================================================================
740
+ // Event Emission
741
+ // ==========================================================================
742
+ /**
743
+ * Emit a variant manager event
744
+ */
745
+ emitEvent(type, variantId, patternId, data) {
746
+ const event = {
747
+ type,
748
+ timestamp: new Date().toISOString(),
749
+ };
750
+ // Only add optional properties if they have values
751
+ if (variantId !== undefined) {
752
+ event.variantId = variantId;
753
+ }
754
+ if (patternId !== undefined) {
755
+ event.patternId = patternId;
756
+ }
757
+ if (data !== undefined) {
758
+ event.data = data;
759
+ }
760
+ this.emit(type, event);
761
+ this.emit('event', event);
762
+ }
763
+ // ==========================================================================
764
+ // Cleanup
765
+ // ==========================================================================
766
+ /**
767
+ * Clean up resources
768
+ */
769
+ dispose() {
770
+ if (this.saveTimeout) {
771
+ clearTimeout(this.saveTimeout);
772
+ this.saveTimeout = null;
773
+ }
774
+ this.removeAllListeners();
775
+ }
776
+ }
777
+ //# sourceMappingURL=variant-manager.js.map