@rigour-labs/core 4.3.6 → 5.0.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/README.md +46 -10
- package/dist/gates/base.d.ts +3 -0
- package/dist/gates/checkpoint.d.ts +23 -8
- package/dist/gates/checkpoint.js +109 -45
- package/dist/gates/checkpoint.test.js +6 -3
- package/dist/gates/dependency.d.ts +39 -0
- package/dist/gates/dependency.js +212 -5
- package/dist/gates/duplication-drift.d.ts +101 -6
- package/dist/gates/duplication-drift.js +427 -33
- package/dist/gates/logic-drift.d.ts +70 -0
- package/dist/gates/logic-drift.js +280 -0
- package/dist/gates/runner.js +29 -1
- package/dist/gates/style-drift.d.ts +53 -0
- package/dist/gates/style-drift.js +305 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/services/adaptive-thresholds.d.ts +54 -10
- package/dist/services/adaptive-thresholds.js +161 -35
- package/dist/services/adaptive-thresholds.test.js +24 -20
- package/dist/services/filesystem-cache.d.ts +50 -0
- package/dist/services/filesystem-cache.js +124 -0
- package/dist/services/temporal-drift.d.ts +101 -0
- package/dist/services/temporal-drift.js +386 -0
- package/dist/templates/universal-config.js +17 -0
- package/dist/types/index.d.ts +196 -0
- package/dist/types/index.js +19 -0
- package/dist/utils/scanner.d.ts +6 -1
- package/dist/utils/scanner.js +8 -1
- package/package.json +6 -6
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Style Drift Detection Gate
|
|
3
|
+
*
|
|
4
|
+
* Detects when AI-generated code gradually drifts away from the project's
|
|
5
|
+
* established coding conventions. AI models tend to use their own "default"
|
|
6
|
+
* style which may differ from the project norm.
|
|
7
|
+
*
|
|
8
|
+
* What it checks:
|
|
9
|
+
* 1. Naming conventions — camelCase vs snake_case vs PascalCase consistency
|
|
10
|
+
* 2. Error handling patterns — try-catch vs .catch() vs Result type consistency
|
|
11
|
+
* 3. Import style — named vs default vs wildcard import consistency
|
|
12
|
+
* 4. Quote style — single vs double quote consistency
|
|
13
|
+
*
|
|
14
|
+
* How it works:
|
|
15
|
+
* 1. First scan: sample source files → compute a style fingerprint → store baseline
|
|
16
|
+
* 2. Subsequent scans: compare new/changed files against baseline
|
|
17
|
+
* 3. If a file deviates >25% on any dimension → flag as style drift
|
|
18
|
+
*
|
|
19
|
+
* The baseline is stored in .rigour/style-baseline.json and evolves with
|
|
20
|
+
* human-approved changes (not AI drift).
|
|
21
|
+
*
|
|
22
|
+
* @since v5.0.0
|
|
23
|
+
*/
|
|
24
|
+
import { Gate } from './base.js';
|
|
25
|
+
import { FileScanner } from '../utils/scanner.js';
|
|
26
|
+
import { Logger } from '../utils/logger.js';
|
|
27
|
+
import fs from 'fs-extra';
|
|
28
|
+
import path from 'path';
|
|
29
|
+
export class StyleDriftGate extends Gate {
|
|
30
|
+
config;
|
|
31
|
+
constructor(config = {}) {
|
|
32
|
+
super('style-drift', 'Style Drift Detection');
|
|
33
|
+
this.config = {
|
|
34
|
+
enabled: config.enabled ?? true,
|
|
35
|
+
deviation_threshold: config.deviation_threshold ?? 0.25,
|
|
36
|
+
sample_size: config.sample_size ?? 100,
|
|
37
|
+
baseline_path: config.baseline_path ?? '.rigour/style-baseline.json',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
get provenance() { return 'ai-drift'; }
|
|
41
|
+
async run(context) {
|
|
42
|
+
if (!this.config.enabled)
|
|
43
|
+
return [];
|
|
44
|
+
const failures = [];
|
|
45
|
+
const baselinePath = path.join(context.cwd, this.config.baseline_path);
|
|
46
|
+
// Find source files
|
|
47
|
+
const files = await FileScanner.findFiles({
|
|
48
|
+
cwd: context.cwd,
|
|
49
|
+
patterns: context.patterns || ['**/*.{ts,tsx,js,jsx,py}'],
|
|
50
|
+
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*', '**/*.d.ts'],
|
|
51
|
+
});
|
|
52
|
+
if (files.length === 0)
|
|
53
|
+
return [];
|
|
54
|
+
// Load or create baseline
|
|
55
|
+
let baseline = null;
|
|
56
|
+
if (await fs.pathExists(baselinePath)) {
|
|
57
|
+
try {
|
|
58
|
+
baseline = await fs.readJson(baselinePath);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
Logger.debug('Failed to load style baseline, will create new one');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!baseline) {
|
|
65
|
+
// First scan: create baseline from sampled files
|
|
66
|
+
const sampled = files.slice(0, this.config.sample_size);
|
|
67
|
+
baseline = await this.computeFingerprint(context, sampled);
|
|
68
|
+
baseline.createdAt = new Date().toISOString();
|
|
69
|
+
// Ensure directory exists and save baseline
|
|
70
|
+
await fs.ensureDir(path.dirname(baselinePath));
|
|
71
|
+
await fs.writeJson(baselinePath, baseline, { spaces: 2 });
|
|
72
|
+
Logger.info(`Style Drift: Created baseline from ${sampled.length} files → ${baselinePath}`);
|
|
73
|
+
return []; // No failures on first scan
|
|
74
|
+
}
|
|
75
|
+
// Subsequent scan: compare each file against baseline
|
|
76
|
+
const contents = await FileScanner.readFiles(context.cwd, files, context.fileCache);
|
|
77
|
+
for (const [file, content] of contents) {
|
|
78
|
+
const ext = path.extname(file);
|
|
79
|
+
if (!['.ts', '.tsx', '.js', '.jsx', '.py'].includes(ext))
|
|
80
|
+
continue;
|
|
81
|
+
const fileFingerprint = this.analyzeFile(content, ext);
|
|
82
|
+
const deviations = this.compareToBaseline(fileFingerprint, baseline);
|
|
83
|
+
for (const deviation of deviations) {
|
|
84
|
+
if (deviation.score > this.config.deviation_threshold) {
|
|
85
|
+
failures.push(this.createFailure(`Style drift in ${file}: ${deviation.dimension} deviates ${(deviation.score * 100).toFixed(0)}% from project baseline (${deviation.detail}).`, [file], `This file's ${deviation.dimension} doesn't match the project's established convention. ${deviation.suggestion}`, 'Style Drift', undefined, undefined, 'low'));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (failures.length > 0) {
|
|
90
|
+
Logger.info(`Style Drift: Found ${failures.length} convention deviations`);
|
|
91
|
+
}
|
|
92
|
+
return failures;
|
|
93
|
+
}
|
|
94
|
+
// ─── Fingerprint Computation ─────────────────────────────────────
|
|
95
|
+
async computeFingerprint(context, files) {
|
|
96
|
+
const fingerprint = {
|
|
97
|
+
naming: {
|
|
98
|
+
functions: { camelCase: 0, snake_case: 0, PascalCase: 0, SCREAMING_SNAKE: 0 },
|
|
99
|
+
variables: { camelCase: 0, snake_case: 0, PascalCase: 0, SCREAMING_SNAKE: 0 },
|
|
100
|
+
},
|
|
101
|
+
errorHandling: { tryCatch: 0, promiseCatch: 0, resultType: 0 },
|
|
102
|
+
importStyle: { named: 0, default: 0, wildcard: 0, sideEffect: 0 },
|
|
103
|
+
quoteStyle: { single: 0, double: 0, backtick: 0 },
|
|
104
|
+
totalFilesAnalyzed: 0,
|
|
105
|
+
createdAt: '',
|
|
106
|
+
};
|
|
107
|
+
const contents = await FileScanner.readFiles(context.cwd, files, context.fileCache);
|
|
108
|
+
for (const [file, content] of contents) {
|
|
109
|
+
const ext = path.extname(file);
|
|
110
|
+
const fileAnalysis = this.analyzeFile(content, ext);
|
|
111
|
+
this.mergeIntoFingerprint(fingerprint, fileAnalysis);
|
|
112
|
+
fingerprint.totalFilesAnalyzed++;
|
|
113
|
+
}
|
|
114
|
+
return fingerprint;
|
|
115
|
+
}
|
|
116
|
+
analyzeFile(content, ext) {
|
|
117
|
+
const fp = {
|
|
118
|
+
naming: {
|
|
119
|
+
functions: { camelCase: 0, snake_case: 0, PascalCase: 0, SCREAMING_SNAKE: 0 },
|
|
120
|
+
variables: { camelCase: 0, snake_case: 0, PascalCase: 0, SCREAMING_SNAKE: 0 },
|
|
121
|
+
},
|
|
122
|
+
errorHandling: { tryCatch: 0, promiseCatch: 0, resultType: 0 },
|
|
123
|
+
importStyle: { named: 0, default: 0, wildcard: 0, sideEffect: 0 },
|
|
124
|
+
quoteStyle: { single: 0, double: 0, backtick: 0 },
|
|
125
|
+
totalFilesAnalyzed: 1,
|
|
126
|
+
createdAt: '',
|
|
127
|
+
};
|
|
128
|
+
const lines = content.split('\n');
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
// ── Naming conventions ──
|
|
131
|
+
// Function declarations
|
|
132
|
+
const fnMatch = line.match(/(?:function|async\s+function)\s+(\w+)/);
|
|
133
|
+
if (fnMatch)
|
|
134
|
+
this.classifyCasing(fnMatch[1], fp.naming.functions);
|
|
135
|
+
// Method definitions
|
|
136
|
+
const methodMatch = line.match(/^\s+(?:async\s+)?(\w+)\s*\([^)]*\)\s*[{:]/);
|
|
137
|
+
if (methodMatch && !['if', 'for', 'while', 'switch', 'catch', 'constructor'].includes(methodMatch[1])) {
|
|
138
|
+
this.classifyCasing(methodMatch[1], fp.naming.functions);
|
|
139
|
+
}
|
|
140
|
+
// Arrow function assignments
|
|
141
|
+
const arrowMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|(\w+))\s*=>/);
|
|
142
|
+
if (arrowMatch)
|
|
143
|
+
this.classifyCasing(arrowMatch[1], fp.naming.functions);
|
|
144
|
+
// Variable declarations (non-function)
|
|
145
|
+
const varMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=/);
|
|
146
|
+
if (varMatch && !arrowMatch)
|
|
147
|
+
this.classifyCasing(varMatch[1], fp.naming.variables);
|
|
148
|
+
// Python function/variable
|
|
149
|
+
if (ext === '.py') {
|
|
150
|
+
const pyFn = line.match(/def\s+(\w+)/);
|
|
151
|
+
if (pyFn)
|
|
152
|
+
this.classifyCasing(pyFn[1], fp.naming.functions);
|
|
153
|
+
const pyVar = line.match(/^(\w+)\s*=/);
|
|
154
|
+
if (pyVar && !pyFn)
|
|
155
|
+
this.classifyCasing(pyVar[1], fp.naming.variables);
|
|
156
|
+
}
|
|
157
|
+
// ── Error handling ──
|
|
158
|
+
if (/\btry\s*\{/.test(line) || /\btry\s*:/.test(line))
|
|
159
|
+
fp.errorHandling.tryCatch++;
|
|
160
|
+
if (/\.catch\s*\(/.test(line))
|
|
161
|
+
fp.errorHandling.promiseCatch++;
|
|
162
|
+
if (/Result<|Result\[|Err\(|Ok\(|Either</.test(line))
|
|
163
|
+
fp.errorHandling.resultType++;
|
|
164
|
+
// ── Import style ──
|
|
165
|
+
if (/^import\s+\{/.test(line.trim()))
|
|
166
|
+
fp.importStyle.named++;
|
|
167
|
+
else if (/^import\s+\*\s+as/.test(line.trim()))
|
|
168
|
+
fp.importStyle.wildcard++;
|
|
169
|
+
else if (/^import\s+['"]/.test(line.trim()))
|
|
170
|
+
fp.importStyle.sideEffect++;
|
|
171
|
+
else if (/^import\s+\w/.test(line.trim()))
|
|
172
|
+
fp.importStyle.default++;
|
|
173
|
+
// ── Quote style ──
|
|
174
|
+
// Count quotes in non-import lines (imports are already counted above)
|
|
175
|
+
if (!line.trim().startsWith('import')) {
|
|
176
|
+
const singles = (line.match(/'/g) || []).length;
|
|
177
|
+
const doubles = (line.match(/"/g) || []).length;
|
|
178
|
+
const backticks = (line.match(/`/g) || []).length;
|
|
179
|
+
fp.quoteStyle.single += singles;
|
|
180
|
+
fp.quoteStyle.double += doubles;
|
|
181
|
+
fp.quoteStyle.backtick += backticks;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return fp;
|
|
185
|
+
}
|
|
186
|
+
classifyCasing(name, dist) {
|
|
187
|
+
if (name.startsWith('_') || name.length <= 1)
|
|
188
|
+
return; // Skip private/single char
|
|
189
|
+
if (/^[A-Z][A-Z0-9_]+$/.test(name)) {
|
|
190
|
+
dist.SCREAMING_SNAKE++;
|
|
191
|
+
}
|
|
192
|
+
else if (/^[A-Z]/.test(name)) {
|
|
193
|
+
dist.PascalCase++;
|
|
194
|
+
}
|
|
195
|
+
else if (name.includes('_')) {
|
|
196
|
+
dist.snake_case++;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
dist.camelCase++;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
mergeIntoFingerprint(target, source) {
|
|
203
|
+
// Naming
|
|
204
|
+
for (const key of Object.keys(target.naming.functions)) {
|
|
205
|
+
target.naming.functions[key] += source.naming.functions[key];
|
|
206
|
+
target.naming.variables[key] += source.naming.variables[key];
|
|
207
|
+
}
|
|
208
|
+
// Error handling
|
|
209
|
+
target.errorHandling.tryCatch += source.errorHandling.tryCatch;
|
|
210
|
+
target.errorHandling.promiseCatch += source.errorHandling.promiseCatch;
|
|
211
|
+
target.errorHandling.resultType += source.errorHandling.resultType;
|
|
212
|
+
// Import style
|
|
213
|
+
target.importStyle.named += source.importStyle.named;
|
|
214
|
+
target.importStyle.default += source.importStyle.default;
|
|
215
|
+
target.importStyle.wildcard += source.importStyle.wildcard;
|
|
216
|
+
target.importStyle.sideEffect += source.importStyle.sideEffect;
|
|
217
|
+
// Quote style
|
|
218
|
+
target.quoteStyle.single += source.quoteStyle.single;
|
|
219
|
+
target.quoteStyle.double += source.quoteStyle.double;
|
|
220
|
+
target.quoteStyle.backtick += source.quoteStyle.backtick;
|
|
221
|
+
}
|
|
222
|
+
// ─── Baseline Comparison ─────────────────────────────────────────
|
|
223
|
+
compareToBaseline(file, baseline) {
|
|
224
|
+
const deviations = [];
|
|
225
|
+
// Compare function naming
|
|
226
|
+
const fnDev = this.distributionDeviation(this.toRecord(file.naming.functions), this.toRecord(baseline.naming.functions));
|
|
227
|
+
if (fnDev.score > 0) {
|
|
228
|
+
deviations.push({
|
|
229
|
+
dimension: 'function naming',
|
|
230
|
+
score: fnDev.score,
|
|
231
|
+
detail: `file uses ${fnDev.filePredominant}, project uses ${fnDev.baselinePredominant}`,
|
|
232
|
+
suggestion: `Use ${fnDev.baselinePredominant} for function names to match project conventions.`,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
// Compare variable naming
|
|
236
|
+
const varDev = this.distributionDeviation(this.toRecord(file.naming.variables), this.toRecord(baseline.naming.variables));
|
|
237
|
+
if (varDev.score > 0) {
|
|
238
|
+
deviations.push({
|
|
239
|
+
dimension: 'variable naming',
|
|
240
|
+
score: varDev.score,
|
|
241
|
+
detail: `file uses ${varDev.filePredominant}, project uses ${varDev.baselinePredominant}`,
|
|
242
|
+
suggestion: `Use ${varDev.baselinePredominant} for variable names to match project conventions.`,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
// Compare error handling
|
|
246
|
+
const errDev = this.distributionDeviation(file.errorHandling, baseline.errorHandling);
|
|
247
|
+
if (errDev.score > 0 && this.hasSignificantData(file.errorHandling)) {
|
|
248
|
+
deviations.push({
|
|
249
|
+
dimension: 'error handling',
|
|
250
|
+
score: errDev.score,
|
|
251
|
+
detail: `file uses ${errDev.filePredominant}, project uses ${errDev.baselinePredominant}`,
|
|
252
|
+
suggestion: `Use ${errDev.baselinePredominant} error handling pattern to match project conventions.`,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
// Compare import style
|
|
256
|
+
const impDev = this.distributionDeviation(file.importStyle, baseline.importStyle);
|
|
257
|
+
if (impDev.score > 0 && this.hasSignificantData(file.importStyle)) {
|
|
258
|
+
deviations.push({
|
|
259
|
+
dimension: 'import style',
|
|
260
|
+
score: impDev.score,
|
|
261
|
+
detail: `file uses ${impDev.filePredominant}, project uses ${impDev.baselinePredominant}`,
|
|
262
|
+
suggestion: `Use ${impDev.baselinePredominant} imports to match project conventions.`,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return deviations;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Compare two distributions and return a deviation score (0-1).
|
|
269
|
+
* 0 = perfect match, 1 = completely different predominant style.
|
|
270
|
+
*
|
|
271
|
+
* Method: find the predominant category in each distribution.
|
|
272
|
+
* If they differ, score = how far the file is from the baseline's predominant category.
|
|
273
|
+
*/
|
|
274
|
+
distributionDeviation(file, baseline) {
|
|
275
|
+
const fileTotal = Object.values(file).reduce((a, b) => a + b, 0);
|
|
276
|
+
const baselineTotal = Object.values(baseline).reduce((a, b) => a + b, 0);
|
|
277
|
+
if (fileTotal < 3 || baselineTotal < 5) {
|
|
278
|
+
return { score: 0, filePredominant: 'N/A', baselinePredominant: 'N/A' };
|
|
279
|
+
}
|
|
280
|
+
// Find predominant category
|
|
281
|
+
const filePredominant = Object.entries(file).sort((a, b) => b[1] - a[1])[0][0];
|
|
282
|
+
const baselinePredominant = Object.entries(baseline).sort((a, b) => b[1] - a[1])[0][0];
|
|
283
|
+
if (filePredominant === baselinePredominant) {
|
|
284
|
+
return { score: 0, filePredominant, baselinePredominant };
|
|
285
|
+
}
|
|
286
|
+
// Calculate how much the file uses the baseline's predominant style
|
|
287
|
+
const fileUseOfBaseline = (file[baselinePredominant] || 0) / fileTotal;
|
|
288
|
+
const baselineUseOfBaseline = (baseline[baselinePredominant] || 0) / baselineTotal;
|
|
289
|
+
// Score = how far the file deviates from baseline's predominant ratio
|
|
290
|
+
const deviation = Math.max(0, baselineUseOfBaseline - fileUseOfBaseline);
|
|
291
|
+
return { score: deviation, filePredominant, baselinePredominant };
|
|
292
|
+
}
|
|
293
|
+
hasSignificantData(obj) {
|
|
294
|
+
return Object.values(obj).reduce((a, b) => a + b, 0) >= 3;
|
|
295
|
+
}
|
|
296
|
+
/** Convert a typed distribution to a generic Record for comparison */
|
|
297
|
+
toRecord(dist) {
|
|
298
|
+
return {
|
|
299
|
+
camelCase: dist.camelCase,
|
|
300
|
+
snake_case: dist.snake_case,
|
|
301
|
+
PascalCase: dist.PascalCase,
|
|
302
|
+
SCREAMING_SNAKE: dist.SCREAMING_SNAKE,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -23,3 +23,7 @@ export { openDatabase, isSQLiteAvailable, compactDatabase, getDatabaseSize, rese
|
|
|
23
23
|
export type { RigourDB, CompactResult } from './storage/index.js';
|
|
24
24
|
export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './storage/index.js';
|
|
25
25
|
export type { ProjectStats } from './storage/index.js';
|
|
26
|
+
export { generateTemporalDriftReport, formatDriftSummary } from './services/temporal-drift.js';
|
|
27
|
+
export type { TemporalDriftReport, ProvenanceStream, MonthlyBucket, WeeklyBucket, DriftDirection } from './services/temporal-drift.js';
|
|
28
|
+
export { getProvenanceTrends, getQualityTrend } from './services/adaptive-thresholds.js';
|
|
29
|
+
export type { ProvenanceTrends, ProvenanceRunData } from './services/adaptive-thresholds.js';
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,10 @@ export { extractFacts, factsToPromptString } from './deep/fact-extractor.js';
|
|
|
23
23
|
export { openDatabase, isSQLiteAvailable, compactDatabase, getDatabaseSize, resetDatabase, insertScan, insertFindings, getRecentScans, getScoreTrendFromDB, getTopIssues, reinforcePattern, getStrongPatterns } from './storage/index.js';
|
|
24
24
|
// Local Project Memory (hybrid intelligence — SQLite-backed per-project learning)
|
|
25
25
|
export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './storage/index.js';
|
|
26
|
+
// Temporal Drift Engine (v5 — cross-session trend analysis + per-provenance EWMA)
|
|
27
|
+
export { generateTemporalDriftReport, formatDriftSummary } from './services/temporal-drift.js';
|
|
28
|
+
// Adaptive Thresholds (v5 — Z-score + per-provenance trends)
|
|
29
|
+
export { getProvenanceTrends, getQualityTrend } from './services/adaptive-thresholds.js';
|
|
26
30
|
// Pattern Index is intentionally NOT exported here to prevent
|
|
27
31
|
// native dependency issues (sharp/transformers) from leaking into
|
|
28
32
|
// non-AI parts of the system.
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Adaptive Thresholds Service
|
|
2
|
+
* Adaptive Thresholds Service (v2)
|
|
3
3
|
*
|
|
4
4
|
* Dynamically adjusts quality gate thresholds based on:
|
|
5
5
|
* - Project maturity (age, commit count, file count)
|
|
6
|
-
* - Historical failure rates
|
|
6
|
+
* - Historical failure rates with Z-score anomaly detection
|
|
7
7
|
* - Complexity tier (hobby/startup/enterprise)
|
|
8
|
-
* -
|
|
8
|
+
* - Per-provenance trend analysis (ai-drift, structural, security separate)
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* v2 upgrades:
|
|
11
|
+
* - Z-score replaces naive delta comparison for trend detection
|
|
12
|
+
* - Per-provenance failure tracking (AI drift vs structural vs security)
|
|
13
|
+
* - Statistical anomaly detection normalizes across project sizes
|
|
12
14
|
*
|
|
13
|
-
* @since v2.14.0
|
|
15
|
+
* @since v2.14.0 (original)
|
|
16
|
+
* @since v5.0.0 (Z-score + provenance-aware trends)
|
|
14
17
|
*/
|
|
15
18
|
export type ComplexityTier = 'hobby' | 'startup' | 'enterprise';
|
|
16
19
|
export type QualityTrend = 'improving' | 'stable' | 'degrading';
|
|
@@ -36,21 +39,62 @@ export interface ThresholdAdjustments {
|
|
|
36
39
|
securityBlockLevel: 'critical' | 'high' | 'medium' | 'low';
|
|
37
40
|
leniencyFactor: number;
|
|
38
41
|
reasoning: string[];
|
|
42
|
+
/** Per-provenance trend breakdown (new in v5) */
|
|
43
|
+
provenanceTrends?: ProvenanceTrends;
|
|
44
|
+
}
|
|
45
|
+
export interface ProvenanceRunData {
|
|
46
|
+
aiDriftFailures: number;
|
|
47
|
+
structuralFailures: number;
|
|
48
|
+
securityFailures: number;
|
|
49
|
+
}
|
|
50
|
+
export interface ProvenanceTrends {
|
|
51
|
+
aiDrift: QualityTrend;
|
|
52
|
+
structural: QualityTrend;
|
|
53
|
+
security: QualityTrend;
|
|
54
|
+
aiDriftZScore: number;
|
|
55
|
+
structuralZScore: number;
|
|
56
|
+
securityZScore: number;
|
|
39
57
|
}
|
|
40
58
|
/**
|
|
41
|
-
* Record a gate run for historical tracking
|
|
59
|
+
* Record a gate run for historical tracking.
|
|
60
|
+
* v5: accepts optional per-provenance breakdown.
|
|
42
61
|
*/
|
|
43
|
-
export declare function recordGateRun(cwd: string, passedGates: number, failedGates: number, totalFailures: number): void;
|
|
62
|
+
export declare function recordGateRun(cwd: string, passedGates: number, failedGates: number, totalFailures: number, provenance?: ProvenanceRunData): void;
|
|
44
63
|
/**
|
|
45
|
-
* Get quality trend
|
|
64
|
+
* Get quality trend using Z-score analysis (v5).
|
|
65
|
+
*
|
|
66
|
+
* How it works:
|
|
67
|
+
* 1. Take the last N runs (baseline window, default 20)
|
|
68
|
+
* 2. Compute mean and std of failure counts
|
|
69
|
+
* 3. Take the most recent window (last 5 runs)
|
|
70
|
+
* 4. Compute the average failure count in the recent window
|
|
71
|
+
* 5. Z-score = (recent_avg - baseline_mean) / baseline_std
|
|
72
|
+
*
|
|
73
|
+
* Z > 2.0 → statistically abnormal spike → DEGRADING
|
|
74
|
+
* Z < -2.0 → statistically abnormal drop → IMPROVING
|
|
75
|
+
*
|
|
76
|
+
* Why better than delta: A project with 100 failures/run and a spike to 108
|
|
77
|
+
* is stable (Z ≈ 0.5). A project with 2 failures/run and a spike to 8
|
|
78
|
+
* is degrading (Z ≈ 3.0). Z-score normalizes for project size.
|
|
46
79
|
*/
|
|
47
80
|
export declare function getQualityTrend(cwd: string): QualityTrend;
|
|
81
|
+
/**
|
|
82
|
+
* Get per-provenance trend analysis (v5).
|
|
83
|
+
*
|
|
84
|
+
* Runs separate Z-score analysis for each provenance category.
|
|
85
|
+
* This is the core differentiator: Rigour can tell you
|
|
86
|
+
* "your AI is getting worse" separately from "your code quality is dropping."
|
|
87
|
+
*
|
|
88
|
+
* Falls back gracefully for legacy history data without provenance.
|
|
89
|
+
*/
|
|
90
|
+
export declare function getProvenanceTrends(cwd: string): ProvenanceTrends;
|
|
48
91
|
/**
|
|
49
92
|
* Detect project complexity tier based on metrics
|
|
50
93
|
*/
|
|
51
94
|
export declare function detectComplexityTier(metrics: ProjectMetrics): ComplexityTier;
|
|
52
95
|
/**
|
|
53
|
-
* Calculate adaptive thresholds based on project state
|
|
96
|
+
* Calculate adaptive thresholds based on project state.
|
|
97
|
+
* v5: Uses Z-score trending and per-provenance analysis.
|
|
54
98
|
*/
|
|
55
99
|
export declare function calculateAdaptiveThresholds(cwd: string, metrics: ProjectMetrics, config?: AdaptiveConfig): ThresholdAdjustments;
|
|
56
100
|
/**
|