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.
- package/dist/analyzers/ast-analyzer.d.ts +251 -0
- package/dist/analyzers/ast-analyzer.d.ts.map +1 -0
- package/dist/analyzers/ast-analyzer.js +548 -0
- package/dist/analyzers/ast-analyzer.js.map +1 -0
- package/dist/analyzers/flow-analyzer.d.ts +241 -0
- package/dist/analyzers/flow-analyzer.d.ts.map +1 -0
- package/dist/analyzers/flow-analyzer.js +1219 -0
- package/dist/analyzers/flow-analyzer.js.map +1 -0
- package/dist/analyzers/index.d.ts +18 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/index.js +19 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/analyzers/semantic-analyzer.d.ts +252 -0
- package/dist/analyzers/semantic-analyzer.d.ts.map +1 -0
- package/dist/analyzers/semantic-analyzer.js +1182 -0
- package/dist/analyzers/semantic-analyzer.js.map +1 -0
- package/dist/analyzers/type-analyzer.d.ts +289 -0
- package/dist/analyzers/type-analyzer.d.ts.map +1 -0
- package/dist/analyzers/type-analyzer.js +1269 -0
- package/dist/analyzers/type-analyzer.js.map +1 -0
- package/dist/analyzers/types.d.ts +537 -0
- package/dist/analyzers/types.d.ts.map +1 -0
- package/dist/analyzers/types.js +11 -0
- package/dist/analyzers/types.js.map +1 -0
- package/dist/config/config-loader.d.ts +166 -0
- package/dist/config/config-loader.d.ts.map +1 -0
- package/dist/config/config-loader.js +429 -0
- package/dist/config/config-loader.js.map +1 -0
- package/dist/config/config-validator.d.ts +204 -0
- package/dist/config/config-validator.d.ts.map +1 -0
- package/dist/config/config-validator.js +632 -0
- package/dist/config/config-validator.js.map +1 -0
- package/dist/config/defaults.d.ts +8 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +26 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +10 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/types.d.ts +47 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +7 -0
- package/dist/config/types.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest/exporter.d.ts +21 -0
- package/dist/manifest/exporter.d.ts.map +1 -0
- package/dist/manifest/exporter.js +339 -0
- package/dist/manifest/exporter.js.map +1 -0
- package/dist/manifest/index.d.ts +14 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/manifest/index.js +15 -0
- package/dist/manifest/index.js.map +1 -0
- package/dist/manifest/manifest-store.d.ts +111 -0
- package/dist/manifest/manifest-store.d.ts.map +1 -0
- package/dist/manifest/manifest-store.js +418 -0
- package/dist/manifest/manifest-store.js.map +1 -0
- package/dist/manifest/types.d.ts +238 -0
- package/dist/manifest/types.d.ts.map +1 -0
- package/dist/manifest/types.js +11 -0
- package/dist/manifest/types.js.map +1 -0
- package/dist/matcher/confidence-scorer.d.ts +188 -0
- package/dist/matcher/confidence-scorer.d.ts.map +1 -0
- package/dist/matcher/confidence-scorer.js +302 -0
- package/dist/matcher/confidence-scorer.js.map +1 -0
- package/dist/matcher/index.d.ts +24 -0
- package/dist/matcher/index.d.ts.map +1 -0
- package/dist/matcher/index.js +26 -0
- package/dist/matcher/index.js.map +1 -0
- package/dist/matcher/outlier-detector.d.ts +252 -0
- package/dist/matcher/outlier-detector.d.ts.map +1 -0
- package/dist/matcher/outlier-detector.js +544 -0
- package/dist/matcher/outlier-detector.js.map +1 -0
- package/dist/matcher/pattern-matcher.d.ts +169 -0
- package/dist/matcher/pattern-matcher.d.ts.map +1 -0
- package/dist/matcher/pattern-matcher.js +692 -0
- package/dist/matcher/pattern-matcher.js.map +1 -0
- package/dist/matcher/types.d.ts +476 -0
- package/dist/matcher/types.d.ts.map +1 -0
- package/dist/matcher/types.js +36 -0
- package/dist/matcher/types.js.map +1 -0
- package/dist/parsers/base-parser.d.ts +282 -0
- package/dist/parsers/base-parser.d.ts.map +1 -0
- package/dist/parsers/base-parser.js +421 -0
- package/dist/parsers/base-parser.js.map +1 -0
- package/dist/parsers/css-parser.d.ts +225 -0
- package/dist/parsers/css-parser.d.ts.map +1 -0
- package/dist/parsers/css-parser.js +477 -0
- package/dist/parsers/css-parser.js.map +1 -0
- package/dist/parsers/index.d.ts +15 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/index.js +15 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/parsers/json-parser.d.ts +219 -0
- package/dist/parsers/json-parser.d.ts.map +1 -0
- package/dist/parsers/json-parser.js +602 -0
- package/dist/parsers/json-parser.js.map +1 -0
- package/dist/parsers/markdown-parser.d.ts +276 -0
- package/dist/parsers/markdown-parser.d.ts.map +1 -0
- package/dist/parsers/markdown-parser.js +731 -0
- package/dist/parsers/markdown-parser.js.map +1 -0
- package/dist/parsers/parser-manager.d.ts +294 -0
- package/dist/parsers/parser-manager.d.ts.map +1 -0
- package/dist/parsers/parser-manager.js +738 -0
- package/dist/parsers/parser-manager.js.map +1 -0
- package/dist/parsers/python-parser.d.ts +204 -0
- package/dist/parsers/python-parser.d.ts.map +1 -0
- package/dist/parsers/python-parser.js +517 -0
- package/dist/parsers/python-parser.js.map +1 -0
- package/dist/parsers/types.d.ts +43 -0
- package/dist/parsers/types.d.ts.map +1 -0
- package/dist/parsers/types.js +7 -0
- package/dist/parsers/types.js.map +1 -0
- package/dist/parsers/typescript-parser.d.ts +264 -0
- package/dist/parsers/typescript-parser.d.ts.map +1 -0
- package/dist/parsers/typescript-parser.js +658 -0
- package/dist/parsers/typescript-parser.js.map +1 -0
- package/dist/rules/evaluator.d.ts +305 -0
- package/dist/rules/evaluator.d.ts.map +1 -0
- package/dist/rules/evaluator.js +579 -0
- package/dist/rules/evaluator.js.map +1 -0
- package/dist/rules/index.d.ts +13 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +13 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/quick-fix-generator.d.ts +334 -0
- package/dist/rules/quick-fix-generator.d.ts.map +1 -0
- package/dist/rules/quick-fix-generator.js +1075 -0
- package/dist/rules/quick-fix-generator.js.map +1 -0
- package/dist/rules/rule-engine.d.ts +241 -0
- package/dist/rules/rule-engine.d.ts.map +1 -0
- package/dist/rules/rule-engine.js +585 -0
- package/dist/rules/rule-engine.js.map +1 -0
- package/dist/rules/severity-manager.d.ts +394 -0
- package/dist/rules/severity-manager.d.ts.map +1 -0
- package/dist/rules/severity-manager.js +619 -0
- package/dist/rules/severity-manager.js.map +1 -0
- package/dist/rules/types.d.ts +370 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/rules/types.js +133 -0
- package/dist/rules/types.js.map +1 -0
- package/dist/rules/variant-manager.d.ts +388 -0
- package/dist/rules/variant-manager.d.ts.map +1 -0
- package/dist/rules/variant-manager.js +777 -0
- package/dist/rules/variant-manager.js.map +1 -0
- package/dist/scanner/change-detector.d.ts +164 -0
- package/dist/scanner/change-detector.d.ts.map +1 -0
- package/dist/scanner/change-detector.js +263 -0
- package/dist/scanner/change-detector.js.map +1 -0
- package/dist/scanner/dependency-graph.d.ts +270 -0
- package/dist/scanner/dependency-graph.d.ts.map +1 -0
- package/dist/scanner/dependency-graph.js +436 -0
- package/dist/scanner/dependency-graph.js.map +1 -0
- package/dist/scanner/file-walker.d.ts +127 -0
- package/dist/scanner/file-walker.d.ts.map +1 -0
- package/dist/scanner/file-walker.js +526 -0
- package/dist/scanner/file-walker.js.map +1 -0
- package/dist/scanner/index.d.ts +12 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +12 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/types.d.ts +218 -0
- package/dist/scanner/types.d.ts.map +1 -0
- package/dist/scanner/types.js +10 -0
- package/dist/scanner/types.js.map +1 -0
- package/dist/scanner/worker-pool.d.ts +317 -0
- package/dist/scanner/worker-pool.d.ts.map +1 -0
- package/dist/scanner/worker-pool.js +571 -0
- package/dist/scanner/worker-pool.js.map +1 -0
- package/dist/store/cache-manager.d.ts +179 -0
- package/dist/store/cache-manager.d.ts.map +1 -0
- package/dist/store/cache-manager.js +391 -0
- package/dist/store/cache-manager.js.map +1 -0
- package/dist/store/history-store.d.ts +314 -0
- package/dist/store/history-store.d.ts.map +1 -0
- package/dist/store/history-store.js +707 -0
- package/dist/store/history-store.js.map +1 -0
- package/dist/store/index.d.ts +20 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +26 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/lock-file-manager.d.ts +202 -0
- package/dist/store/lock-file-manager.d.ts.map +1 -0
- package/dist/store/lock-file-manager.js +475 -0
- package/dist/store/lock-file-manager.js.map +1 -0
- package/dist/store/pattern-store.d.ts +289 -0
- package/dist/store/pattern-store.d.ts.map +1 -0
- package/dist/store/pattern-store.js +936 -0
- package/dist/store/pattern-store.js.map +1 -0
- package/dist/store/schema-validator.d.ts +159 -0
- package/dist/store/schema-validator.d.ts.map +1 -0
- package/dist/store/schema-validator.js +1096 -0
- package/dist/store/schema-validator.js.map +1 -0
- package/dist/store/types.d.ts +585 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/types.js +82 -0
- package/dist/store/types.js.map +1 -0
- package/dist/types/analysis.d.ts +19 -0
- package/dist/types/analysis.d.ts.map +1 -0
- package/dist/types/analysis.js +5 -0
- package/dist/types/analysis.js.map +1 -0
- package/dist/types/common.d.ts +7 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +5 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +10 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/patterns.d.ts +40 -0
- package/dist/types/patterns.d.ts.map +1 -0
- package/dist/types/patterns.js +7 -0
- package/dist/types/patterns.js.map +1 -0
- package/dist/types/violations.d.ts +7 -0
- package/dist/types/violations.d.ts.map +1 -0
- package/dist/types/violations.js +7 -0
- package/dist/types/violations.js.map +1 -0
- 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
|