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,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
|