driftdetect-core 0.1.2 → 0.3.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/boundaries/boundary-store.d.ts +92 -0
- package/dist/boundaries/boundary-store.d.ts.map +1 -0
- package/dist/boundaries/boundary-store.js +458 -0
- package/dist/boundaries/boundary-store.js.map +1 -0
- package/dist/boundaries/index.d.ts +8 -0
- package/dist/boundaries/index.d.ts.map +1 -0
- package/dist/boundaries/index.js +8 -0
- package/dist/boundaries/index.js.map +1 -0
- package/dist/boundaries/types.d.ts +237 -0
- package/dist/boundaries/types.d.ts.map +1 -0
- package/dist/boundaries/types.js +8 -0
- package/dist/boundaries/types.js.map +1 -0
- package/dist/dna/ai-context.d.ts +10 -0
- package/dist/dna/ai-context.d.ts.map +1 -0
- package/dist/dna/ai-context.js +46 -0
- package/dist/dna/ai-context.js.map +1 -0
- package/dist/dna/dna-analyzer.d.ts +34 -0
- package/dist/dna/dna-analyzer.d.ts.map +1 -0
- package/dist/dna/dna-analyzer.js +110 -0
- package/dist/dna/dna-analyzer.js.map +1 -0
- package/dist/dna/dna-store.d.ts +13 -0
- package/dist/dna/dna-store.d.ts.map +1 -0
- package/dist/dna/dna-store.js +43 -0
- package/dist/dna/dna-store.js.map +1 -0
- package/dist/dna/gene-extractors/animation-approach.d.ts +15 -0
- package/dist/dna/gene-extractors/animation-approach.d.ts.map +1 -0
- package/dist/dna/gene-extractors/animation-approach.js +97 -0
- package/dist/dna/gene-extractors/animation-approach.js.map +1 -0
- package/dist/dna/gene-extractors/base-extractor.d.ts +53 -0
- package/dist/dna/gene-extractors/base-extractor.d.ts.map +1 -0
- package/dist/dna/gene-extractors/base-extractor.js +76 -0
- package/dist/dna/gene-extractors/base-extractor.js.map +1 -0
- package/dist/dna/gene-extractors/index.d.ts +16 -0
- package/dist/dna/gene-extractors/index.d.ts.map +1 -0
- package/dist/dna/gene-extractors/index.js +38 -0
- package/dist/dna/gene-extractors/index.js.map +1 -0
- package/dist/dna/gene-extractors/responsive-approach.d.ts +10 -0
- package/dist/dna/gene-extractors/responsive-approach.d.ts.map +1 -0
- package/dist/dna/gene-extractors/responsive-approach.js +30 -0
- package/dist/dna/gene-extractors/responsive-approach.js.map +1 -0
- package/dist/dna/gene-extractors/spacing-philosophy.d.ts +10 -0
- package/dist/dna/gene-extractors/spacing-philosophy.d.ts.map +1 -0
- package/dist/dna/gene-extractors/spacing-philosophy.js +30 -0
- package/dist/dna/gene-extractors/spacing-philosophy.js.map +1 -0
- package/dist/dna/gene-extractors/state-styling.d.ts +10 -0
- package/dist/dna/gene-extractors/state-styling.d.ts.map +1 -0
- package/dist/dna/gene-extractors/state-styling.js +29 -0
- package/dist/dna/gene-extractors/state-styling.js.map +1 -0
- package/dist/dna/gene-extractors/theming.d.ts +10 -0
- package/dist/dna/gene-extractors/theming.d.ts.map +1 -0
- package/dist/dna/gene-extractors/theming.js +30 -0
- package/dist/dna/gene-extractors/theming.js.map +1 -0
- package/dist/dna/gene-extractors/variant-handling.d.ts +13 -0
- package/dist/dna/gene-extractors/variant-handling.d.ts.map +1 -0
- package/dist/dna/gene-extractors/variant-handling.js +38 -0
- package/dist/dna/gene-extractors/variant-handling.js.map +1 -0
- package/dist/dna/health-calculator.d.ts +21 -0
- package/dist/dna/health-calculator.d.ts.map +1 -0
- package/dist/dna/health-calculator.js +113 -0
- package/dist/dna/health-calculator.js.map +1 -0
- package/dist/dna/index.d.ts +21 -0
- package/dist/dna/index.d.ts.map +1 -0
- package/dist/dna/index.js +19 -0
- package/dist/dna/index.js.map +1 -0
- package/dist/dna/mutation-detector.d.ts +10 -0
- package/dist/dna/mutation-detector.d.ts.map +1 -0
- package/dist/dna/mutation-detector.js +39 -0
- package/dist/dna/mutation-detector.js.map +1 -0
- package/dist/dna/playbook-generator.d.ts +6 -0
- package/dist/dna/playbook-generator.d.ts.map +1 -0
- package/dist/dna/playbook-generator.js +53 -0
- package/dist/dna/playbook-generator.js.map +1 -0
- package/dist/dna/types.d.ts +95 -0
- package/dist/dna/types.d.ts.map +1 -0
- package/dist/dna/types.js +8 -0
- package/dist/dna/types.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/parsers/parser-manager.d.ts.map +1 -1
- package/dist/parsers/parser-manager.js +2 -0
- package/dist/parsers/parser-manager.js.map +1 -1
- package/dist/parsers/tree-sitter/config.d.ts +82 -0
- package/dist/parsers/tree-sitter/config.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/config.js +200 -0
- package/dist/parsers/tree-sitter/config.js.map +1 -0
- package/dist/parsers/tree-sitter/csharp-ast-converter.d.ts +64 -0
- package/dist/parsers/tree-sitter/csharp-ast-converter.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/csharp-ast-converter.js +271 -0
- package/dist/parsers/tree-sitter/csharp-ast-converter.js.map +1 -0
- package/dist/parsers/tree-sitter/csharp-loader.d.ts +43 -0
- package/dist/parsers/tree-sitter/csharp-loader.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/csharp-loader.js +146 -0
- package/dist/parsers/tree-sitter/csharp-loader.js.map +1 -0
- package/dist/parsers/tree-sitter/index.d.ts +26 -0
- package/dist/parsers/tree-sitter/index.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/index.js +47 -0
- package/dist/parsers/tree-sitter/index.js.map +1 -0
- package/dist/parsers/tree-sitter/java/annotation-extractor.d.ts +79 -0
- package/dist/parsers/tree-sitter/java/annotation-extractor.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/java/annotation-extractor.js +540 -0
- package/dist/parsers/tree-sitter/java/annotation-extractor.js.map +1 -0
- package/dist/parsers/tree-sitter/java/class-extractor.d.ts +40 -0
- package/dist/parsers/tree-sitter/java/class-extractor.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/java/class-extractor.js +770 -0
- package/dist/parsers/tree-sitter/java/class-extractor.js.map +1 -0
- package/dist/parsers/tree-sitter/java/index.d.ts +14 -0
- package/dist/parsers/tree-sitter/java/index.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/java/index.js +25 -0
- package/dist/parsers/tree-sitter/java/index.js.map +1 -0
- package/dist/parsers/tree-sitter/java/method-extractor.d.ts +88 -0
- package/dist/parsers/tree-sitter/java/method-extractor.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/java/method-extractor.js +551 -0
- package/dist/parsers/tree-sitter/java/method-extractor.js.map +1 -0
- package/dist/parsers/tree-sitter/java/types.d.ts +545 -0
- package/dist/parsers/tree-sitter/java/types.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/java/types.js +81 -0
- package/dist/parsers/tree-sitter/java/types.js.map +1 -0
- package/dist/parsers/tree-sitter/loader.d.ts +50 -0
- package/dist/parsers/tree-sitter/loader.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/loader.js +156 -0
- package/dist/parsers/tree-sitter/loader.js.map +1 -0
- package/dist/parsers/tree-sitter/pydantic/config-extractor.d.ts +78 -0
- package/dist/parsers/tree-sitter/pydantic/config-extractor.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/pydantic/config-extractor.js +278 -0
- package/dist/parsers/tree-sitter/pydantic/config-extractor.js.map +1 -0
- package/dist/parsers/tree-sitter/pydantic/constraint-parser.d.ts +84 -0
- package/dist/parsers/tree-sitter/pydantic/constraint-parser.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/pydantic/constraint-parser.js +321 -0
- package/dist/parsers/tree-sitter/pydantic/constraint-parser.js.map +1 -0
- package/dist/parsers/tree-sitter/pydantic/field-extractor.d.ts +74 -0
- package/dist/parsers/tree-sitter/pydantic/field-extractor.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/pydantic/field-extractor.js +285 -0
- package/dist/parsers/tree-sitter/pydantic/field-extractor.js.map +1 -0
- package/dist/parsers/tree-sitter/pydantic/index.d.ts +18 -0
- package/dist/parsers/tree-sitter/pydantic/index.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/pydantic/index.js +23 -0
- package/dist/parsers/tree-sitter/pydantic/index.js.map +1 -0
- package/dist/parsers/tree-sitter/pydantic/inheritance-resolver.d.ts +70 -0
- package/dist/parsers/tree-sitter/pydantic/inheritance-resolver.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/pydantic/inheritance-resolver.js +251 -0
- package/dist/parsers/tree-sitter/pydantic/inheritance-resolver.js.map +1 -0
- package/dist/parsers/tree-sitter/pydantic/pydantic-extractor.d.ts +102 -0
- package/dist/parsers/tree-sitter/pydantic/pydantic-extractor.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/pydantic/pydantic-extractor.js +399 -0
- package/dist/parsers/tree-sitter/pydantic/pydantic-extractor.js.map +1 -0
- package/dist/parsers/tree-sitter/pydantic/type-resolver.d.ts +89 -0
- package/dist/parsers/tree-sitter/pydantic/type-resolver.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/pydantic/type-resolver.js +426 -0
- package/dist/parsers/tree-sitter/pydantic/type-resolver.js.map +1 -0
- package/dist/parsers/tree-sitter/pydantic/types.d.ts +177 -0
- package/dist/parsers/tree-sitter/pydantic/types.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/pydantic/types.js +139 -0
- package/dist/parsers/tree-sitter/pydantic/types.js.map +1 -0
- package/dist/parsers/tree-sitter/pydantic/validator-extractor.d.ts +88 -0
- package/dist/parsers/tree-sitter/pydantic/validator-extractor.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/pydantic/validator-extractor.js +315 -0
- package/dist/parsers/tree-sitter/pydantic/validator-extractor.js.map +1 -0
- package/dist/parsers/tree-sitter/python-ast-converter.d.ts +140 -0
- package/dist/parsers/tree-sitter/python-ast-converter.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/python-ast-converter.js +360 -0
- package/dist/parsers/tree-sitter/python-ast-converter.js.map +1 -0
- package/dist/parsers/tree-sitter/tree-sitter-csharp-parser.d.ts +465 -0
- package/dist/parsers/tree-sitter/tree-sitter-csharp-parser.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/tree-sitter-csharp-parser.js +1146 -0
- package/dist/parsers/tree-sitter/tree-sitter-csharp-parser.js.map +1 -0
- package/dist/parsers/tree-sitter/tree-sitter-python-parser.d.ts +86 -0
- package/dist/parsers/tree-sitter/tree-sitter-python-parser.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/tree-sitter-python-parser.js +177 -0
- package/dist/parsers/tree-sitter/tree-sitter-python-parser.js.map +1 -0
- package/dist/parsers/tree-sitter/types.d.ts +399 -0
- package/dist/parsers/tree-sitter/types.d.ts.map +1 -0
- package/dist/parsers/tree-sitter/types.js +20 -0
- package/dist/parsers/tree-sitter/types.js.map +1 -0
- package/dist/parsers/types.d.ts +1 -1
- package/dist/parsers/types.d.ts.map +1 -1
- package/dist/scanner/file-walker.d.ts.map +1 -1
- package/dist/scanner/file-walker.js +5 -0
- package/dist/scanner/file-walker.js.map +1 -1
- package/dist/store/history-store.d.ts +85 -269
- package/dist/store/history-store.d.ts.map +1 -1
- package/dist/store/history-store.js +272 -624
- package/dist/store/history-store.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/java-type-mapping.d.ts +79 -0
- package/dist/types/java-type-mapping.d.ts.map +1 -0
- package/dist/types/java-type-mapping.js +290 -0
- package/dist/types/java-type-mapping.js.map +1 -0
- package/package.json +8 -3
|
@@ -1,69 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* History Store - Pattern
|
|
2
|
+
* History Store - Pattern snapshot and trend tracking
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Captures pattern state over time to detect regressions and improvements.
|
|
5
|
+
* Stores daily snapshots in .drift/history/snapshots/
|
|
6
6
|
*
|
|
7
|
-
* @requirements 4.4 -
|
|
7
|
+
* @requirements 4.4 - Pattern history SHALL be tracked in .drift/history/
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from 'node:fs/promises';
|
|
10
10
|
import * as path from 'node:path';
|
|
11
11
|
import { EventEmitter } from 'node:events';
|
|
12
|
-
|
|
12
|
+
const DEFAULT_CONFIG = {
|
|
13
|
+
rootDir: '.',
|
|
14
|
+
maxSnapshots: 90,
|
|
15
|
+
snapshotInterval: 'scan',
|
|
16
|
+
};
|
|
13
17
|
// ============================================================================
|
|
14
18
|
// Constants
|
|
15
19
|
// ============================================================================
|
|
16
|
-
/** Directory name for drift configuration */
|
|
17
20
|
const DRIFT_DIR = '.drift';
|
|
18
|
-
/** Directory name for history */
|
|
19
21
|
const HISTORY_DIR = 'history';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
* Error thrown when a history operation fails
|
|
31
|
-
*/
|
|
32
|
-
export class HistoryStoreError extends Error {
|
|
33
|
-
errorCause;
|
|
34
|
-
constructor(message, errorCause) {
|
|
35
|
-
super(message);
|
|
36
|
-
this.name = 'HistoryStoreError';
|
|
37
|
-
this.errorCause = errorCause;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Error thrown when a pattern history is not found
|
|
42
|
-
*/
|
|
43
|
-
export class PatternHistoryNotFoundError extends Error {
|
|
44
|
-
patternId;
|
|
45
|
-
constructor(patternId) {
|
|
46
|
-
super(`Pattern history not found: ${patternId}`);
|
|
47
|
-
this.patternId = patternId;
|
|
48
|
-
this.name = 'PatternHistoryNotFoundError';
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Default history store configuration
|
|
53
|
-
*/
|
|
54
|
-
export const DEFAULT_HISTORY_STORE_CONFIG = {
|
|
55
|
-
rootDir: '.',
|
|
56
|
-
maxEntriesPerPattern: DEFAULT_MAX_ENTRIES_PER_PATTERN,
|
|
57
|
-
maxAgeDays: DEFAULT_MAX_AGE_DAYS,
|
|
58
|
-
autoSave: false,
|
|
59
|
-
autoSaveDebounce: 1000,
|
|
22
|
+
const SNAPSHOTS_DIR = 'snapshots';
|
|
23
|
+
// Thresholds for detecting significant changes
|
|
24
|
+
const REGRESSION_THRESHOLDS = {
|
|
25
|
+
confidence: -0.05, // 5% drop in confidence
|
|
26
|
+
compliance: -0.10, // 10% drop in compliance
|
|
27
|
+
outliers: 3, // 3+ new outliers
|
|
28
|
+
};
|
|
29
|
+
const CRITICAL_THRESHOLDS = {
|
|
30
|
+
confidence: -0.15, // 15% drop = critical
|
|
31
|
+
compliance: -0.20, // 20% drop = critical
|
|
60
32
|
};
|
|
61
33
|
// ============================================================================
|
|
62
34
|
// Helper Functions
|
|
63
35
|
// ============================================================================
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
36
|
+
async function ensureDir(dirPath) {
|
|
37
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
38
|
+
}
|
|
67
39
|
async function fileExists(filePath) {
|
|
68
40
|
try {
|
|
69
41
|
await fs.access(filePath);
|
|
@@ -73,635 +45,311 @@ async function fileExists(filePath) {
|
|
|
73
45
|
return false;
|
|
74
46
|
}
|
|
75
47
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
48
|
+
function getDateString(date = new Date()) {
|
|
49
|
+
return date.toISOString().split('T')[0];
|
|
50
|
+
}
|
|
51
|
+
function calculateComplianceRate(locations, outliers) {
|
|
52
|
+
const total = locations + outliers;
|
|
53
|
+
return total > 0 ? locations / total : 1;
|
|
81
54
|
}
|
|
82
55
|
// ============================================================================
|
|
83
56
|
// History Store Class
|
|
84
57
|
// ============================================================================
|
|
85
|
-
/**
|
|
86
|
-
* History Store - Manages pattern change history
|
|
87
|
-
*
|
|
88
|
-
* Tracks all pattern changes including:
|
|
89
|
-
* - Pattern created
|
|
90
|
-
* - Pattern updated
|
|
91
|
-
* - Pattern approved
|
|
92
|
-
* - Pattern ignored
|
|
93
|
-
* - Pattern deleted
|
|
94
|
-
* - Confidence changed
|
|
95
|
-
* - Locations changed
|
|
96
|
-
* - Severity changed
|
|
97
|
-
*
|
|
98
|
-
* History is stored in .drift/history/patterns.json
|
|
99
|
-
*
|
|
100
|
-
* @requirements 4.4 - Pattern history tracked in .drift/history/
|
|
101
|
-
*/
|
|
102
58
|
export class HistoryStore extends EventEmitter {
|
|
103
59
|
config;
|
|
104
60
|
historyDir;
|
|
105
|
-
|
|
106
|
-
histories = new Map();
|
|
107
|
-
loaded = false;
|
|
108
|
-
dirty = false;
|
|
109
|
-
saveTimeout = null;
|
|
61
|
+
snapshotsDir;
|
|
110
62
|
constructor(config = {}) {
|
|
111
63
|
super();
|
|
112
|
-
this.config = { ...
|
|
64
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
113
65
|
this.historyDir = path.join(this.config.rootDir, DRIFT_DIR, HISTORY_DIR);
|
|
114
|
-
this.
|
|
66
|
+
this.snapshotsDir = path.join(this.historyDir, SNAPSHOTS_DIR);
|
|
115
67
|
}
|
|
116
|
-
// ==========================================================================
|
|
117
|
-
// Initialization
|
|
118
|
-
// ==========================================================================
|
|
119
68
|
/**
|
|
120
69
|
* Initialize the history store
|
|
121
|
-
*
|
|
122
|
-
* Creates necessary directories and loads existing history.
|
|
123
70
|
*/
|
|
124
71
|
async initialize() {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
this.emitEvent('file:loaded', undefined, { count: this.histories.size });
|
|
153
|
-
}
|
|
154
|
-
catch (error) {
|
|
155
|
-
if (error.code === 'ENOENT') {
|
|
156
|
-
return; // File doesn't exist, skip
|
|
157
|
-
}
|
|
158
|
-
throw new HistoryStoreError(`Failed to load history file: ${this.historyFilePath}`, error);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
// ==========================================================================
|
|
162
|
-
// Saving
|
|
163
|
-
// ==========================================================================
|
|
164
|
-
/**
|
|
165
|
-
* Save history to disk
|
|
166
|
-
*
|
|
167
|
-
* @requirements 4.4 - Persist history in .drift/history/
|
|
168
|
-
*/
|
|
169
|
-
async save() {
|
|
170
|
-
const historyFile = {
|
|
171
|
-
version: HISTORY_FILE_VERSION,
|
|
172
|
-
patterns: Array.from(this.histories.values()),
|
|
173
|
-
lastUpdated: new Date().toISOString(),
|
|
72
|
+
await ensureDir(this.snapshotsDir);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Create a snapshot from current patterns
|
|
76
|
+
*/
|
|
77
|
+
async createSnapshot(patterns) {
|
|
78
|
+
const now = new Date();
|
|
79
|
+
const timestamp = now.toISOString();
|
|
80
|
+
const date = getDateString(now);
|
|
81
|
+
// Convert patterns to snapshots
|
|
82
|
+
const patternSnapshots = patterns.map(p => ({
|
|
83
|
+
patternId: p.id,
|
|
84
|
+
patternName: p.name,
|
|
85
|
+
category: p.category,
|
|
86
|
+
confidence: p.confidence.score,
|
|
87
|
+
locationCount: p.locations.length,
|
|
88
|
+
outlierCount: p.outliers.length,
|
|
89
|
+
complianceRate: calculateComplianceRate(p.locations.length, p.outliers.length),
|
|
90
|
+
status: p.status,
|
|
91
|
+
}));
|
|
92
|
+
// Calculate summary
|
|
93
|
+
const summary = this.calculateSummary(patternSnapshots);
|
|
94
|
+
const snapshot = {
|
|
95
|
+
timestamp,
|
|
96
|
+
date,
|
|
97
|
+
patterns: patternSnapshots,
|
|
98
|
+
summary,
|
|
174
99
|
};
|
|
175
|
-
//
|
|
176
|
-
await
|
|
177
|
-
//
|
|
178
|
-
await
|
|
179
|
-
this.
|
|
180
|
-
|
|
100
|
+
// Save snapshot
|
|
101
|
+
await this.saveSnapshot(snapshot);
|
|
102
|
+
// Cleanup old snapshots
|
|
103
|
+
await this.cleanupOldSnapshots();
|
|
104
|
+
this.emit('snapshot:created', snapshot);
|
|
105
|
+
return snapshot;
|
|
181
106
|
}
|
|
182
107
|
/**
|
|
183
|
-
*
|
|
108
|
+
* Get snapshots for a date range
|
|
184
109
|
*/
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (this.saveTimeout) {
|
|
190
|
-
clearTimeout(this.saveTimeout);
|
|
110
|
+
async getSnapshots(startDate, endDate) {
|
|
111
|
+
const snapshots = [];
|
|
112
|
+
if (!(await fileExists(this.snapshotsDir))) {
|
|
113
|
+
return snapshots;
|
|
191
114
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
115
|
+
const files = await fs.readdir(this.snapshotsDir);
|
|
116
|
+
const jsonFiles = files.filter(f => f.endsWith('.json')).sort();
|
|
117
|
+
for (const file of jsonFiles) {
|
|
118
|
+
const date = file.replace('.json', '');
|
|
119
|
+
// Filter by date range
|
|
120
|
+
if (startDate && date < startDate)
|
|
121
|
+
continue;
|
|
122
|
+
if (endDate && date > endDate)
|
|
123
|
+
continue;
|
|
124
|
+
try {
|
|
125
|
+
const content = await fs.readFile(path.join(this.snapshotsDir, file), 'utf-8');
|
|
126
|
+
const snapshot = JSON.parse(content);
|
|
127
|
+
snapshots.push(snapshot);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
console.error(`Error reading snapshot ${file}:`, error);
|
|
195
131
|
}
|
|
196
|
-
}, this.config.autoSaveDebounce);
|
|
197
|
-
}
|
|
198
|
-
// ==========================================================================
|
|
199
|
-
// Recording Events
|
|
200
|
-
// ==========================================================================
|
|
201
|
-
/**
|
|
202
|
-
* Record a history event for a pattern
|
|
203
|
-
*
|
|
204
|
-
* @param patternId - Pattern ID
|
|
205
|
-
* @param category - Pattern category
|
|
206
|
-
* @param eventType - Type of event
|
|
207
|
-
* @param options - Additional event options
|
|
208
|
-
*/
|
|
209
|
-
recordEvent(patternId, category, eventType, options = {}) {
|
|
210
|
-
const now = new Date().toISOString();
|
|
211
|
-
// Create the event
|
|
212
|
-
const event = {
|
|
213
|
-
timestamp: now,
|
|
214
|
-
type: eventType,
|
|
215
|
-
patternId,
|
|
216
|
-
...(options.user && { user: options.user }),
|
|
217
|
-
...(options.previousValue !== undefined && { previousValue: options.previousValue }),
|
|
218
|
-
...(options.newValue !== undefined && { newValue: options.newValue }),
|
|
219
|
-
...(options.details && { details: options.details }),
|
|
220
|
-
};
|
|
221
|
-
// Get or create pattern history
|
|
222
|
-
let history = this.histories.get(patternId);
|
|
223
|
-
if (!history) {
|
|
224
|
-
history = {
|
|
225
|
-
patternId,
|
|
226
|
-
category,
|
|
227
|
-
events: [],
|
|
228
|
-
createdAt: now,
|
|
229
|
-
lastModified: now,
|
|
230
|
-
};
|
|
231
|
-
this.histories.set(patternId, history);
|
|
232
|
-
}
|
|
233
|
-
// Add event to history
|
|
234
|
-
history.events.push(event);
|
|
235
|
-
history.lastModified = now;
|
|
236
|
-
// Prune if needed
|
|
237
|
-
this.prunePatternHistory(patternId);
|
|
238
|
-
this.dirty = true;
|
|
239
|
-
this.emitEvent('event:recorded', patternId, { eventType });
|
|
240
|
-
this.scheduleAutoSave();
|
|
241
|
-
return event;
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Record a pattern creation event
|
|
245
|
-
*/
|
|
246
|
-
recordCreated(pattern, user) {
|
|
247
|
-
const options = {
|
|
248
|
-
newValue: {
|
|
249
|
-
name: pattern.name,
|
|
250
|
-
description: pattern.description,
|
|
251
|
-
confidence: pattern.confidence.score,
|
|
252
|
-
severity: pattern.severity,
|
|
253
|
-
},
|
|
254
|
-
};
|
|
255
|
-
if (user !== undefined) {
|
|
256
|
-
options.user = user;
|
|
257
|
-
}
|
|
258
|
-
return this.recordEvent(pattern.id, pattern.category, 'created', options);
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Record a pattern update event
|
|
262
|
-
*/
|
|
263
|
-
recordUpdated(pattern, previousPattern, user) {
|
|
264
|
-
const options = {
|
|
265
|
-
previousValue: {
|
|
266
|
-
name: previousPattern.name,
|
|
267
|
-
description: previousPattern.description,
|
|
268
|
-
},
|
|
269
|
-
newValue: {
|
|
270
|
-
name: pattern.name,
|
|
271
|
-
description: pattern.description,
|
|
272
|
-
},
|
|
273
|
-
};
|
|
274
|
-
if (user !== undefined) {
|
|
275
|
-
options.user = user;
|
|
276
|
-
}
|
|
277
|
-
return this.recordEvent(pattern.id, pattern.category, 'updated', options);
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Record a pattern approval event
|
|
281
|
-
*/
|
|
282
|
-
recordApproved(pattern, user) {
|
|
283
|
-
const options = {
|
|
284
|
-
details: {
|
|
285
|
-
confidence: pattern.confidence.score,
|
|
286
|
-
severity: pattern.severity,
|
|
287
|
-
},
|
|
288
|
-
};
|
|
289
|
-
if (user !== undefined) {
|
|
290
|
-
options.user = user;
|
|
291
|
-
}
|
|
292
|
-
return this.recordEvent(pattern.id, pattern.category, 'approved', options);
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Record a pattern ignore event
|
|
296
|
-
*/
|
|
297
|
-
recordIgnored(pattern, user) {
|
|
298
|
-
const options = {};
|
|
299
|
-
if (user !== undefined) {
|
|
300
|
-
options.user = user;
|
|
301
|
-
}
|
|
302
|
-
return this.recordEvent(pattern.id, pattern.category, 'ignored', options);
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Record a pattern deletion event
|
|
306
|
-
*/
|
|
307
|
-
recordDeleted(pattern, user) {
|
|
308
|
-
const options = {
|
|
309
|
-
previousValue: {
|
|
310
|
-
name: pattern.name,
|
|
311
|
-
status: pattern.status,
|
|
312
|
-
},
|
|
313
|
-
};
|
|
314
|
-
if (user !== undefined) {
|
|
315
|
-
options.user = user;
|
|
316
|
-
}
|
|
317
|
-
return this.recordEvent(pattern.id, pattern.category, 'deleted', options);
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Record a confidence change event
|
|
321
|
-
*/
|
|
322
|
-
recordConfidenceChanged(pattern, previousScore, user) {
|
|
323
|
-
const options = {
|
|
324
|
-
previousValue: previousScore,
|
|
325
|
-
newValue: pattern.confidence.score,
|
|
326
|
-
};
|
|
327
|
-
if (user !== undefined) {
|
|
328
|
-
options.user = user;
|
|
329
|
-
}
|
|
330
|
-
return this.recordEvent(pattern.id, pattern.category, 'confidence_changed', options);
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
|
-
* Record a locations change event
|
|
334
|
-
*/
|
|
335
|
-
recordLocationsChanged(pattern, previousCount, user) {
|
|
336
|
-
const options = {
|
|
337
|
-
previousValue: previousCount,
|
|
338
|
-
newValue: pattern.locations.length,
|
|
339
|
-
details: {
|
|
340
|
-
added: Math.max(0, pattern.locations.length - previousCount),
|
|
341
|
-
removed: Math.max(0, previousCount - pattern.locations.length),
|
|
342
|
-
},
|
|
343
|
-
};
|
|
344
|
-
if (user !== undefined) {
|
|
345
|
-
options.user = user;
|
|
346
|
-
}
|
|
347
|
-
return this.recordEvent(pattern.id, pattern.category, 'locations_changed', options);
|
|
348
|
-
}
|
|
349
|
-
/**
|
|
350
|
-
* Record a severity change event
|
|
351
|
-
*/
|
|
352
|
-
recordSeverityChanged(pattern, previousSeverity, user) {
|
|
353
|
-
const options = {
|
|
354
|
-
previousValue: previousSeverity,
|
|
355
|
-
newValue: pattern.severity,
|
|
356
|
-
};
|
|
357
|
-
if (user !== undefined) {
|
|
358
|
-
options.user = user;
|
|
359
|
-
}
|
|
360
|
-
return this.recordEvent(pattern.id, pattern.category, 'severity_changed', options);
|
|
361
|
-
}
|
|
362
|
-
// ==========================================================================
|
|
363
|
-
// Querying
|
|
364
|
-
// ==========================================================================
|
|
365
|
-
/**
|
|
366
|
-
* Get history for a specific pattern
|
|
367
|
-
*
|
|
368
|
-
* @param patternId - Pattern ID
|
|
369
|
-
* @returns Pattern history or undefined if not found
|
|
370
|
-
*/
|
|
371
|
-
getPatternHistory(patternId) {
|
|
372
|
-
return this.histories.get(patternId);
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Get history for a specific pattern, throwing if not found
|
|
376
|
-
*
|
|
377
|
-
* @param patternId - Pattern ID
|
|
378
|
-
* @returns Pattern history
|
|
379
|
-
* @throws PatternHistoryNotFoundError if not found
|
|
380
|
-
*/
|
|
381
|
-
getPatternHistoryOrThrow(patternId) {
|
|
382
|
-
const history = this.histories.get(patternId);
|
|
383
|
-
if (!history) {
|
|
384
|
-
throw new PatternHistoryNotFoundError(patternId);
|
|
385
132
|
}
|
|
386
|
-
return
|
|
133
|
+
return snapshots;
|
|
387
134
|
}
|
|
388
135
|
/**
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
* @param patternId - Pattern ID
|
|
392
|
-
* @returns True if history exists
|
|
136
|
+
* Get the most recent snapshot
|
|
393
137
|
*/
|
|
394
|
-
|
|
395
|
-
|
|
138
|
+
async getLatestSnapshot() {
|
|
139
|
+
const snapshots = await this.getSnapshots();
|
|
140
|
+
return snapshots.length > 0 ? snapshots[snapshots.length - 1] : null;
|
|
396
141
|
}
|
|
397
142
|
/**
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
* @param query - Query options
|
|
401
|
-
* @returns Query result with matching events
|
|
143
|
+
* Get snapshot from N days ago
|
|
402
144
|
*/
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
if (
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
145
|
+
async getSnapshotFromDaysAgo(days) {
|
|
146
|
+
const targetDate = new Date();
|
|
147
|
+
targetDate.setDate(targetDate.getDate() - days);
|
|
148
|
+
const dateStr = getDateString(targetDate);
|
|
149
|
+
const filePath = path.join(this.snapshotsDir, `${dateStr}.json`);
|
|
150
|
+
if (!(await fileExists(filePath))) {
|
|
151
|
+
// Find closest snapshot before target date
|
|
152
|
+
const snapshots = await this.getSnapshots(undefined, dateStr);
|
|
153
|
+
return snapshots.length > 0 ? snapshots[snapshots.length - 1] : null;
|
|
413
154
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if (history) {
|
|
418
|
-
events.push(...history.events);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
155
|
+
try {
|
|
156
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
157
|
+
return JSON.parse(content);
|
|
421
158
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
for (const history of this.histories.values()) {
|
|
425
|
-
events.push(...history.events);
|
|
426
|
-
}
|
|
159
|
+
catch {
|
|
160
|
+
return null;
|
|
427
161
|
}
|
|
428
|
-
// Apply filters
|
|
429
|
-
events = this.applyFilters(events, query);
|
|
430
|
-
// Sort by timestamp descending (most recent first)
|
|
431
|
-
events.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
432
|
-
// Get total before pagination
|
|
433
|
-
const total = events.length;
|
|
434
|
-
// Apply pagination
|
|
435
|
-
const offset = query.offset ?? 0;
|
|
436
|
-
const limit = query.limit ?? events.length;
|
|
437
|
-
const hasMore = offset + limit < total;
|
|
438
|
-
events = events.slice(offset, offset + limit);
|
|
439
|
-
return {
|
|
440
|
-
events,
|
|
441
|
-
total,
|
|
442
|
-
hasMore,
|
|
443
|
-
executionTime: Date.now() - startTime,
|
|
444
|
-
};
|
|
445
162
|
}
|
|
446
163
|
/**
|
|
447
|
-
*
|
|
164
|
+
* Calculate trends between two snapshots
|
|
448
165
|
*/
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
return false;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
// Filter by category
|
|
461
|
-
if (query.category) {
|
|
462
|
-
const categories = Array.isArray(query.category)
|
|
463
|
-
? query.category
|
|
464
|
-
: [query.category];
|
|
465
|
-
const history = this.histories.get(event.patternId);
|
|
466
|
-
if (!history || !categories.includes(history.category)) {
|
|
467
|
-
return false;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
// Filter by user
|
|
471
|
-
if (query.user && event.user !== query.user) {
|
|
472
|
-
return false;
|
|
166
|
+
calculateTrends(current, previous) {
|
|
167
|
+
const trends = [];
|
|
168
|
+
const previousMap = new Map(previous.patterns.map(p => [p.patternId, p]));
|
|
169
|
+
for (const currentPattern of current.patterns) {
|
|
170
|
+
const prevPattern = previousMap.get(currentPattern.patternId);
|
|
171
|
+
if (!prevPattern) {
|
|
172
|
+
// New pattern, skip for now
|
|
173
|
+
continue;
|
|
473
174
|
}
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
175
|
+
// Check confidence change
|
|
176
|
+
const confidenceChange = currentPattern.confidence - prevPattern.confidence;
|
|
177
|
+
if (Math.abs(confidenceChange) >= Math.abs(REGRESSION_THRESHOLDS.confidence)) {
|
|
178
|
+
const isRegression = confidenceChange < 0;
|
|
179
|
+
const isCritical = confidenceChange <= CRITICAL_THRESHOLDS.confidence;
|
|
180
|
+
trends.push({
|
|
181
|
+
patternId: currentPattern.patternId,
|
|
182
|
+
patternName: currentPattern.patternName,
|
|
183
|
+
category: currentPattern.category,
|
|
184
|
+
type: isRegression ? 'regression' : 'improvement',
|
|
185
|
+
metric: 'confidence',
|
|
186
|
+
previousValue: prevPattern.confidence,
|
|
187
|
+
currentValue: currentPattern.confidence,
|
|
188
|
+
change: confidenceChange,
|
|
189
|
+
changePercent: (confidenceChange / prevPattern.confidence) * 100,
|
|
190
|
+
severity: isCritical ? 'critical' : isRegression ? 'warning' : 'info',
|
|
191
|
+
firstSeen: previous.timestamp,
|
|
192
|
+
details: `Confidence ${isRegression ? 'dropped' : 'improved'} from ${(prevPattern.confidence * 100).toFixed(0)}% to ${(currentPattern.confidence * 100).toFixed(0)}%`,
|
|
193
|
+
});
|
|
481
194
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
195
|
+
// Check compliance change
|
|
196
|
+
const complianceChange = currentPattern.complianceRate - prevPattern.complianceRate;
|
|
197
|
+
if (Math.abs(complianceChange) >= Math.abs(REGRESSION_THRESHOLDS.compliance)) {
|
|
198
|
+
const isRegression = complianceChange < 0;
|
|
199
|
+
const isCritical = complianceChange <= CRITICAL_THRESHOLDS.compliance;
|
|
200
|
+
trends.push({
|
|
201
|
+
patternId: currentPattern.patternId,
|
|
202
|
+
patternName: currentPattern.patternName,
|
|
203
|
+
category: currentPattern.category,
|
|
204
|
+
type: isRegression ? 'regression' : 'improvement',
|
|
205
|
+
metric: 'compliance',
|
|
206
|
+
previousValue: prevPattern.complianceRate,
|
|
207
|
+
currentValue: currentPattern.complianceRate,
|
|
208
|
+
change: complianceChange,
|
|
209
|
+
changePercent: prevPattern.complianceRate > 0
|
|
210
|
+
? (complianceChange / prevPattern.complianceRate) * 100
|
|
211
|
+
: 0,
|
|
212
|
+
severity: isCritical ? 'critical' : isRegression ? 'warning' : 'info',
|
|
213
|
+
firstSeen: previous.timestamp,
|
|
214
|
+
details: `Compliance ${isRegression ? 'dropped' : 'improved'} from ${(prevPattern.complianceRate * 100).toFixed(0)}% to ${(currentPattern.complianceRate * 100).toFixed(0)}%`,
|
|
215
|
+
});
|
|
488
216
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
* Get events by category
|
|
509
|
-
*/
|
|
510
|
-
getEventsByCategory(category) {
|
|
511
|
-
return this.query({ category }).events;
|
|
512
|
-
}
|
|
513
|
-
/**
|
|
514
|
-
* Get events in date range
|
|
515
|
-
*/
|
|
516
|
-
getEventsInDateRange(after, before) {
|
|
517
|
-
return this.query({ after, before }).events;
|
|
518
|
-
}
|
|
519
|
-
/**
|
|
520
|
-
* Get recent events
|
|
521
|
-
*
|
|
522
|
-
* @param limit - Maximum number of events to return
|
|
523
|
-
*/
|
|
524
|
-
getRecentEvents(limit = 50) {
|
|
525
|
-
return this.query({ limit }).events;
|
|
526
|
-
}
|
|
527
|
-
/**
|
|
528
|
-
* Get approval history
|
|
529
|
-
*/
|
|
530
|
-
getApprovalHistory() {
|
|
531
|
-
return this.getEventsByType('approved');
|
|
532
|
-
}
|
|
533
|
-
/**
|
|
534
|
-
* Get events by user
|
|
535
|
-
*/
|
|
536
|
-
getEventsByUser(user) {
|
|
537
|
-
return this.query({ user }).events;
|
|
538
|
-
}
|
|
539
|
-
// ==========================================================================
|
|
540
|
-
// Pruning
|
|
541
|
-
// ==========================================================================
|
|
542
|
-
/**
|
|
543
|
-
* Prune history for a specific pattern
|
|
544
|
-
*
|
|
545
|
-
* Removes old entries based on maxEntriesPerPattern and maxAgeDays config.
|
|
546
|
-
*
|
|
547
|
-
* @param patternId - Pattern ID to prune
|
|
548
|
-
*/
|
|
549
|
-
prunePatternHistory(patternId) {
|
|
550
|
-
const history = this.histories.get(patternId);
|
|
551
|
-
if (!history) {
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
const now = Date.now();
|
|
555
|
-
const maxAgeMs = this.config.maxAgeDays * 24 * 60 * 60 * 1000;
|
|
556
|
-
// Filter out old events
|
|
557
|
-
history.events = history.events.filter((event) => {
|
|
558
|
-
const eventTime = new Date(event.timestamp).getTime();
|
|
559
|
-
return now - eventTime < maxAgeMs;
|
|
560
|
-
});
|
|
561
|
-
// Limit number of entries
|
|
562
|
-
if (history.events.length > this.config.maxEntriesPerPattern) {
|
|
563
|
-
// Keep most recent events
|
|
564
|
-
history.events = history.events
|
|
565
|
-
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
|
566
|
-
.slice(0, this.config.maxEntriesPerPattern);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
/**
|
|
570
|
-
* Prune all history
|
|
571
|
-
*
|
|
572
|
-
* Removes old entries based on maxEntriesPerPattern and maxAgeDays config.
|
|
573
|
-
*/
|
|
574
|
-
prune() {
|
|
575
|
-
for (const patternId of this.histories.keys()) {
|
|
576
|
-
this.prunePatternHistory(patternId);
|
|
577
|
-
}
|
|
578
|
-
// Remove empty histories
|
|
579
|
-
for (const [patternId, history] of this.histories.entries()) {
|
|
580
|
-
if (history.events.length === 0) {
|
|
581
|
-
this.histories.delete(patternId);
|
|
217
|
+
// Check outlier increase
|
|
218
|
+
const outlierChange = currentPattern.outlierCount - prevPattern.outlierCount;
|
|
219
|
+
if (outlierChange >= REGRESSION_THRESHOLDS.outliers) {
|
|
220
|
+
trends.push({
|
|
221
|
+
patternId: currentPattern.patternId,
|
|
222
|
+
patternName: currentPattern.patternName,
|
|
223
|
+
category: currentPattern.category,
|
|
224
|
+
type: 'regression',
|
|
225
|
+
metric: 'outliers',
|
|
226
|
+
previousValue: prevPattern.outlierCount,
|
|
227
|
+
currentValue: currentPattern.outlierCount,
|
|
228
|
+
change: outlierChange,
|
|
229
|
+
changePercent: prevPattern.outlierCount > 0
|
|
230
|
+
? (outlierChange / prevPattern.outlierCount) * 100
|
|
231
|
+
: 100,
|
|
232
|
+
severity: outlierChange >= 10 ? 'critical' : 'warning',
|
|
233
|
+
firstSeen: previous.timestamp,
|
|
234
|
+
details: `${outlierChange} new outliers detected (${prevPattern.outlierCount} → ${currentPattern.outlierCount})`,
|
|
235
|
+
});
|
|
582
236
|
}
|
|
583
237
|
}
|
|
584
|
-
|
|
585
|
-
this.emitEvent('history:pruned', undefined, { count: this.histories.size });
|
|
586
|
-
this.scheduleAutoSave();
|
|
238
|
+
return trends;
|
|
587
239
|
}
|
|
588
240
|
/**
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
* @param patternId - Pattern ID
|
|
592
|
-
* @returns True if history was deleted
|
|
241
|
+
* Get trend summary for a period
|
|
593
242
|
*/
|
|
594
|
-
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
243
|
+
async getTrendSummary(period = '7d') {
|
|
244
|
+
const days = period === '7d' ? 7 : period === '30d' ? 30 : 90;
|
|
245
|
+
const current = await this.getLatestSnapshot();
|
|
246
|
+
const previous = await this.getSnapshotFromDaysAgo(days);
|
|
247
|
+
if (!current || !previous) {
|
|
248
|
+
return null;
|
|
599
249
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
for (const history of this.histories.values()) {
|
|
623
|
-
for (const event of history.events) {
|
|
624
|
-
totalEvents++;
|
|
625
|
-
eventsByType[event.type]++;
|
|
626
|
-
if (!oldestEvent || event.timestamp < oldestEvent) {
|
|
627
|
-
oldestEvent = event.timestamp;
|
|
628
|
-
}
|
|
629
|
-
if (!newestEvent || event.timestamp > newestEvent) {
|
|
630
|
-
newestEvent = event.timestamp;
|
|
631
|
-
}
|
|
250
|
+
const trends = this.calculateTrends(current, previous);
|
|
251
|
+
const regressions = trends.filter(t => t.type === 'regression');
|
|
252
|
+
const improvements = trends.filter(t => t.type === 'improvement');
|
|
253
|
+
// Calculate category trends
|
|
254
|
+
const categoryTrends = {};
|
|
255
|
+
const categories = new Set([
|
|
256
|
+
...current.patterns.map(p => p.category),
|
|
257
|
+
...previous.patterns.map(p => p.category),
|
|
258
|
+
]);
|
|
259
|
+
for (const category of categories) {
|
|
260
|
+
const currentCat = current.summary.byCategory[category];
|
|
261
|
+
const prevCat = previous.summary.byCategory[category];
|
|
262
|
+
if (currentCat && prevCat) {
|
|
263
|
+
const avgConfidenceChange = currentCat.avgConfidence - prevCat.avgConfidence;
|
|
264
|
+
const complianceChange = currentCat.complianceRate - prevCat.complianceRate;
|
|
265
|
+
categoryTrends[category] = {
|
|
266
|
+
trend: avgConfidenceChange > 0.02 ? 'improving'
|
|
267
|
+
: avgConfidenceChange < -0.02 ? 'declining'
|
|
268
|
+
: 'stable',
|
|
269
|
+
avgConfidenceChange,
|
|
270
|
+
complianceChange,
|
|
271
|
+
};
|
|
632
272
|
}
|
|
633
273
|
}
|
|
274
|
+
// Calculate overall trend
|
|
275
|
+
const healthDelta = current.summary.overallComplianceRate - previous.summary.overallComplianceRate;
|
|
276
|
+
const overallTrend = healthDelta > 0.02 ? 'improving'
|
|
277
|
+
: healthDelta < -0.02 ? 'declining'
|
|
278
|
+
: 'stable';
|
|
279
|
+
// Count stable patterns
|
|
280
|
+
const changedPatternIds = new Set(trends.map(t => t.patternId));
|
|
281
|
+
const stableCount = current.patterns.filter(p => !changedPatternIds.has(p.patternId)).length;
|
|
634
282
|
return {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
283
|
+
period,
|
|
284
|
+
startDate: previous.date,
|
|
285
|
+
endDate: current.date,
|
|
286
|
+
regressions,
|
|
287
|
+
improvements,
|
|
288
|
+
stable: stableCount,
|
|
289
|
+
overallTrend,
|
|
290
|
+
healthDelta,
|
|
291
|
+
categoryTrends,
|
|
640
292
|
};
|
|
641
293
|
}
|
|
642
294
|
// ==========================================================================
|
|
643
|
-
//
|
|
295
|
+
// Private Methods
|
|
644
296
|
// ==========================================================================
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
297
|
+
calculateSummary(patterns) {
|
|
298
|
+
const byCategory = {};
|
|
299
|
+
let totalLocations = 0;
|
|
300
|
+
let totalOutliers = 0;
|
|
301
|
+
let totalConfidence = 0;
|
|
302
|
+
for (const pattern of patterns) {
|
|
303
|
+
totalLocations += pattern.locationCount;
|
|
304
|
+
totalOutliers += pattern.outlierCount;
|
|
305
|
+
totalConfidence += pattern.confidence;
|
|
306
|
+
// Aggregate by category
|
|
307
|
+
if (!byCategory[pattern.category]) {
|
|
308
|
+
byCategory[pattern.category] = {
|
|
309
|
+
patternCount: 0,
|
|
310
|
+
avgConfidence: 0,
|
|
311
|
+
totalLocations: 0,
|
|
312
|
+
totalOutliers: 0,
|
|
313
|
+
complianceRate: 0,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const cat = byCategory[pattern.category];
|
|
317
|
+
cat.patternCount++;
|
|
318
|
+
cat.avgConfidence += pattern.confidence;
|
|
319
|
+
cat.totalLocations += pattern.locationCount;
|
|
320
|
+
cat.totalOutliers += pattern.outlierCount;
|
|
655
321
|
}
|
|
656
|
-
|
|
657
|
-
|
|
322
|
+
// Finalize category averages
|
|
323
|
+
for (const cat of Object.values(byCategory)) {
|
|
324
|
+
if (cat.patternCount > 0) {
|
|
325
|
+
cat.avgConfidence /= cat.patternCount;
|
|
326
|
+
}
|
|
327
|
+
cat.complianceRate = calculateComplianceRate(cat.totalLocations, cat.totalOutliers);
|
|
658
328
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
*/
|
|
668
|
-
get size() {
|
|
669
|
-
return this.histories.size;
|
|
670
|
-
}
|
|
671
|
-
/**
|
|
672
|
-
* Check if the store has been loaded
|
|
673
|
-
*/
|
|
674
|
-
get isLoaded() {
|
|
675
|
-
return this.loaded;
|
|
676
|
-
}
|
|
677
|
-
/**
|
|
678
|
-
* Check if there are unsaved changes
|
|
679
|
-
*/
|
|
680
|
-
get isDirty() {
|
|
681
|
-
return this.dirty;
|
|
329
|
+
return {
|
|
330
|
+
totalPatterns: patterns.length,
|
|
331
|
+
avgConfidence: patterns.length > 0 ? totalConfidence / patterns.length : 0,
|
|
332
|
+
totalLocations,
|
|
333
|
+
totalOutliers,
|
|
334
|
+
overallComplianceRate: calculateComplianceRate(totalLocations, totalOutliers),
|
|
335
|
+
byCategory,
|
|
336
|
+
};
|
|
682
337
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
get path() {
|
|
687
|
-
return this.historyDir;
|
|
338
|
+
async saveSnapshot(snapshot) {
|
|
339
|
+
const filePath = path.join(this.snapshotsDir, `${snapshot.date}.json`);
|
|
340
|
+
await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2));
|
|
688
341
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
this.
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
dispose() {
|
|
700
|
-
if (this.saveTimeout) {
|
|
701
|
-
clearTimeout(this.saveTimeout);
|
|
702
|
-
this.saveTimeout = null;
|
|
342
|
+
async cleanupOldSnapshots() {
|
|
343
|
+
if (!(await fileExists(this.snapshotsDir))) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const files = await fs.readdir(this.snapshotsDir);
|
|
347
|
+
const jsonFiles = files.filter(f => f.endsWith('.json')).sort();
|
|
348
|
+
// Remove oldest files if over limit
|
|
349
|
+
const toRemove = jsonFiles.slice(0, Math.max(0, jsonFiles.length - this.config.maxSnapshots));
|
|
350
|
+
for (const file of toRemove) {
|
|
351
|
+
await fs.unlink(path.join(this.snapshotsDir, file));
|
|
703
352
|
}
|
|
704
|
-
this.removeAllListeners();
|
|
705
353
|
}
|
|
706
354
|
}
|
|
707
355
|
//# sourceMappingURL=history-store.js.map
|