@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,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logic Drift Foundation Gate
|
|
3
|
+
*
|
|
4
|
+
* Detects when AI subtly changes business logic in functions:
|
|
5
|
+
* - Comparison operator mutations: >= became > (off-by-one)
|
|
6
|
+
* - Return statement additions/removals
|
|
7
|
+
* - Branch count changes (new if/else added or removed)
|
|
8
|
+
* - Call sequence changes (function calls reordered)
|
|
9
|
+
*
|
|
10
|
+
* This is the HARDEST drift to catch because:
|
|
11
|
+
* - Code still compiles
|
|
12
|
+
* - Tests might still pass (if they don't cover edge cases)
|
|
13
|
+
* - The change looks intentional ("AI refactored the function")
|
|
14
|
+
*
|
|
15
|
+
* Strategy: Collect baselines for critical functions, then detect
|
|
16
|
+
* mutations between scans. This foundation enables future LLM-powered
|
|
17
|
+
* deeper analysis (feeding baselines into DriftBench training).
|
|
18
|
+
*
|
|
19
|
+
* @since v5.1.0
|
|
20
|
+
*/
|
|
21
|
+
import { Gate } from './base.js';
|
|
22
|
+
import { FileScanner } from '../utils/scanner.js';
|
|
23
|
+
import { Logger } from '../utils/logger.js';
|
|
24
|
+
import fs from 'fs-extra';
|
|
25
|
+
import path from 'path';
|
|
26
|
+
import crypto from 'crypto';
|
|
27
|
+
export class LogicDriftGate extends Gate {
|
|
28
|
+
config;
|
|
29
|
+
constructor(config = {}) {
|
|
30
|
+
super('logic-drift', 'Logic Drift Detection');
|
|
31
|
+
this.config = {
|
|
32
|
+
enabled: config.enabled ?? true,
|
|
33
|
+
baseline_path: config.baseline_path ?? '.rigour/logic-baseline.json',
|
|
34
|
+
track_operators: config.track_operators ?? true,
|
|
35
|
+
track_branches: config.track_branches ?? true,
|
|
36
|
+
track_returns: config.track_returns ?? true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
get provenance() { return 'ai-drift'; }
|
|
40
|
+
async run(context) {
|
|
41
|
+
if (!this.config.enabled)
|
|
42
|
+
return [];
|
|
43
|
+
const failures = [];
|
|
44
|
+
const baselinePath = path.join(context.cwd, this.config.baseline_path);
|
|
45
|
+
// Find source files
|
|
46
|
+
const files = await FileScanner.findFiles({
|
|
47
|
+
cwd: context.cwd,
|
|
48
|
+
patterns: context.patterns || ['**/*.{ts,tsx,js,jsx}'],
|
|
49
|
+
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*', '**/*.d.ts'],
|
|
50
|
+
});
|
|
51
|
+
if (files.length === 0)
|
|
52
|
+
return [];
|
|
53
|
+
// Extract current function baselines
|
|
54
|
+
const currentFunctions = [];
|
|
55
|
+
const contents = await FileScanner.readFiles(context.cwd, files, context.fileCache);
|
|
56
|
+
for (const [file, content] of contents) {
|
|
57
|
+
const extracted = this.extractFunctionBaselines(content, file);
|
|
58
|
+
currentFunctions.push(...extracted);
|
|
59
|
+
}
|
|
60
|
+
// Load previous baseline
|
|
61
|
+
let previousBaseline = null;
|
|
62
|
+
if (await fs.pathExists(baselinePath)) {
|
|
63
|
+
try {
|
|
64
|
+
previousBaseline = await fs.readJson(baselinePath);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
Logger.debug('Failed to load logic baseline');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!previousBaseline) {
|
|
71
|
+
// First scan: save baseline, no comparisons yet
|
|
72
|
+
const baseline = {
|
|
73
|
+
functions: currentFunctions,
|
|
74
|
+
createdAt: new Date().toISOString(),
|
|
75
|
+
lastUpdated: new Date().toISOString(),
|
|
76
|
+
scanCount: 1,
|
|
77
|
+
};
|
|
78
|
+
await fs.ensureDir(path.dirname(baselinePath));
|
|
79
|
+
await fs.writeJson(baselinePath, baseline, { spaces: 2 });
|
|
80
|
+
Logger.info(`Logic Drift: Created baseline with ${currentFunctions.length} functions → ${baselinePath}`);
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
// Compare current functions against previous baselines
|
|
84
|
+
const prevMap = new Map();
|
|
85
|
+
for (const fn of previousBaseline.functions) {
|
|
86
|
+
// Key by file:name for matching
|
|
87
|
+
prevMap.set(`${fn.file}:${fn.name}`, fn);
|
|
88
|
+
}
|
|
89
|
+
for (const current of currentFunctions) {
|
|
90
|
+
const key = `${current.file}:${current.name}`;
|
|
91
|
+
const prev = prevMap.get(key);
|
|
92
|
+
if (!prev)
|
|
93
|
+
continue; // New function, no baseline to compare
|
|
94
|
+
// Skip if body hash is identical (no changes at all)
|
|
95
|
+
if (current.bodyHash === prev.bodyHash)
|
|
96
|
+
continue;
|
|
97
|
+
// ── Operator Mutations ──
|
|
98
|
+
if (this.config.track_operators) {
|
|
99
|
+
const opChanges = this.detectOperatorMutations(prev.comparisonOps, current.comparisonOps);
|
|
100
|
+
for (const change of opChanges) {
|
|
101
|
+
failures.push(this.createFailure(`Logic drift: Comparison operator changed from '${change.from}' to '${change.to}' in function '${current.name}'.`, [current.file], `Operator mutation in ${current.file}:${current.line} — '${change.from}' → '${change.to}'. This could introduce off-by-one errors or change boundary conditions. Verify this change is intentional.`, 'Logic Drift: Operator Mutation', current.line, undefined, 'high'));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// ── Return Count Changes ──
|
|
105
|
+
if (this.config.track_returns && current.returnCount !== prev.returnCount) {
|
|
106
|
+
const diff = current.returnCount - prev.returnCount;
|
|
107
|
+
const direction = diff > 0 ? 'added' : 'removed';
|
|
108
|
+
failures.push(this.createFailure(`Logic drift: ${Math.abs(diff)} return statement(s) ${direction} in function '${current.name}' (was ${prev.returnCount}, now ${current.returnCount}).`, [current.file], `Return statement count changed in ${current.file}:${current.line}. This may alter control flow or error handling paths.`, 'Logic Drift: Return Change', current.line, undefined, 'medium'));
|
|
109
|
+
}
|
|
110
|
+
// ── Branch Count Changes ──
|
|
111
|
+
if (this.config.track_branches) {
|
|
112
|
+
const branchDiff = Math.abs(current.branchCount - prev.branchCount);
|
|
113
|
+
if (branchDiff >= 2) {
|
|
114
|
+
// Only alert on significant branch changes (±2+)
|
|
115
|
+
const direction = current.branchCount > prev.branchCount ? 'added' : 'removed';
|
|
116
|
+
failures.push(this.createFailure(`Logic drift: ${branchDiff} branch(es) ${direction} in function '${current.name}' (was ${prev.branchCount}, now ${current.branchCount}).`, [current.file], `Significant branch count change in ${current.file}:${current.line}. Review whether all code paths are still correct.`, 'Logic Drift: Branch Change', current.line, undefined, 'low'));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Update baseline with current data
|
|
121
|
+
const updatedBaseline = {
|
|
122
|
+
functions: currentFunctions,
|
|
123
|
+
createdAt: previousBaseline.createdAt,
|
|
124
|
+
lastUpdated: new Date().toISOString(),
|
|
125
|
+
scanCount: previousBaseline.scanCount + 1,
|
|
126
|
+
};
|
|
127
|
+
await fs.writeJson(baselinePath, updatedBaseline, { spaces: 2 });
|
|
128
|
+
if (failures.length > 0) {
|
|
129
|
+
Logger.info(`Logic Drift: Found ${failures.length} logic mutations`);
|
|
130
|
+
}
|
|
131
|
+
return failures;
|
|
132
|
+
}
|
|
133
|
+
// ─── Function Baseline Extraction ────────────────────────────────
|
|
134
|
+
extractFunctionBaselines(content, file) {
|
|
135
|
+
const baselines = [];
|
|
136
|
+
const lines = content.split('\n');
|
|
137
|
+
const fnPatterns = [
|
|
138
|
+
/^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/,
|
|
139
|
+
/^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|(\w+))\s*=>/,
|
|
140
|
+
/^\s+(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{/,
|
|
141
|
+
];
|
|
142
|
+
for (let i = 0; i < lines.length; i++) {
|
|
143
|
+
const line = lines[i];
|
|
144
|
+
for (const pattern of fnPatterns) {
|
|
145
|
+
const match = line.match(pattern);
|
|
146
|
+
if (match) {
|
|
147
|
+
const name = match[1];
|
|
148
|
+
if (['if', 'for', 'while', 'switch', 'catch', 'constructor'].includes(name))
|
|
149
|
+
continue;
|
|
150
|
+
const body = this.extractBody(lines, i);
|
|
151
|
+
if (body.length < 3)
|
|
152
|
+
continue; // Skip trivial functions
|
|
153
|
+
const bodyText = body.join('\n');
|
|
154
|
+
const normalized = bodyText
|
|
155
|
+
.replace(/\/\/.*/g, '')
|
|
156
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
157
|
+
.replace(/\s+/g, ' ')
|
|
158
|
+
.trim();
|
|
159
|
+
baselines.push({
|
|
160
|
+
name,
|
|
161
|
+
file,
|
|
162
|
+
line: i + 1,
|
|
163
|
+
comparisonOps: this.extractComparisonOps(bodyText),
|
|
164
|
+
branchCount: this.countBranches(bodyText),
|
|
165
|
+
returnCount: this.countReturns(bodyText),
|
|
166
|
+
callSequence: this.extractCallSequence(bodyText),
|
|
167
|
+
bodyHash: crypto.createHash('md5').update(normalized).digest('hex'),
|
|
168
|
+
});
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return baselines;
|
|
174
|
+
}
|
|
175
|
+
extractBody(lines, startIndex) {
|
|
176
|
+
let braceDepth = 0;
|
|
177
|
+
let started = false;
|
|
178
|
+
const body = [];
|
|
179
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
180
|
+
const line = lines[i];
|
|
181
|
+
for (const ch of line) {
|
|
182
|
+
if (ch === '{') {
|
|
183
|
+
braceDepth++;
|
|
184
|
+
started = true;
|
|
185
|
+
}
|
|
186
|
+
if (ch === '}')
|
|
187
|
+
braceDepth--;
|
|
188
|
+
}
|
|
189
|
+
if (started)
|
|
190
|
+
body.push(line);
|
|
191
|
+
if (started && braceDepth === 0)
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
return body;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Extract all comparison operators from function body in order.
|
|
198
|
+
* These are the most critical mutations: >= to > causes off-by-one.
|
|
199
|
+
*/
|
|
200
|
+
extractComparisonOps(body) {
|
|
201
|
+
const ops = [];
|
|
202
|
+
const matches = body.matchAll(/(===|!==|==|!=|>=|<=|>(?!=)|<(?!=))/g);
|
|
203
|
+
for (const m of matches) {
|
|
204
|
+
ops.push(m[1]);
|
|
205
|
+
}
|
|
206
|
+
return ops;
|
|
207
|
+
}
|
|
208
|
+
countBranches(body) {
|
|
209
|
+
let count = 0;
|
|
210
|
+
// Count if, else if, else, switch, case, ternary
|
|
211
|
+
count += (body.match(/\bif\s*\(/g) || []).length;
|
|
212
|
+
count += (body.match(/\belse\s+if\s*\(/g) || []).length;
|
|
213
|
+
count += (body.match(/\belse\s*\{/g) || []).length;
|
|
214
|
+
count += (body.match(/\bswitch\s*\(/g) || []).length;
|
|
215
|
+
count += (body.match(/\bcase\s+/g) || []).length;
|
|
216
|
+
count += (body.match(/\?\s*[^?]/g) || []).length; // ternary (approximate)
|
|
217
|
+
return count;
|
|
218
|
+
}
|
|
219
|
+
countReturns(body) {
|
|
220
|
+
return (body.match(/\breturn\b/g) || []).length;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Extract ordered sequence of function calls.
|
|
224
|
+
* Useful for detecting when AI reorders operations.
|
|
225
|
+
*/
|
|
226
|
+
extractCallSequence(body) {
|
|
227
|
+
const calls = [];
|
|
228
|
+
const matches = body.matchAll(/\b(\w+)\s*\(/g);
|
|
229
|
+
const keywords = new Set(['if', 'for', 'while', 'switch', 'catch', 'function', 'async', 'await', 'return', 'new', 'typeof', 'instanceof']);
|
|
230
|
+
for (const m of matches) {
|
|
231
|
+
if (!keywords.has(m[1])) {
|
|
232
|
+
calls.push(m[1]);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return calls;
|
|
236
|
+
}
|
|
237
|
+
// ─── Mutation Detection ──────────────────────────────────────────
|
|
238
|
+
/**
|
|
239
|
+
* Detect specific operator mutations between two ordered operator lists.
|
|
240
|
+
* Only reports CHANGED operators, not added/removed ones (those are
|
|
241
|
+
* covered by branch count changes).
|
|
242
|
+
*
|
|
243
|
+
* Example:
|
|
244
|
+
* prev: ['>=', '===', '!==']
|
|
245
|
+
* curr: ['>', '===', '!==']
|
|
246
|
+
* → [{from: '>=', to: '>'}]
|
|
247
|
+
*/
|
|
248
|
+
detectOperatorMutations(prev, curr) {
|
|
249
|
+
const mutations = [];
|
|
250
|
+
// Use LCS-like alignment for best matching
|
|
251
|
+
const minLen = Math.min(prev.length, curr.length);
|
|
252
|
+
for (let i = 0; i < minLen; i++) {
|
|
253
|
+
if (prev[i] !== curr[i]) {
|
|
254
|
+
// Check if this is a "dangerous" mutation (same family, different strictness)
|
|
255
|
+
if (this.isDangerousMutation(prev[i], curr[i])) {
|
|
256
|
+
mutations.push({ from: prev[i], to: curr[i] });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return mutations;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Classify whether an operator change is "dangerous" (likely unintentional).
|
|
264
|
+
*
|
|
265
|
+
* Dangerous mutations:
|
|
266
|
+
* - >= to > (boundary change, off-by-one)
|
|
267
|
+
* - <= to < (boundary change)
|
|
268
|
+
* - === to == (type coercion change)
|
|
269
|
+
* - !== to != (type coercion change)
|
|
270
|
+
*/
|
|
271
|
+
isDangerousMutation(from, to) {
|
|
272
|
+
const dangerous = new Set([
|
|
273
|
+
'>=:>', '>:>=', // Boundary mutations
|
|
274
|
+
'<=:<', '<:<=', // Boundary mutations
|
|
275
|
+
'===:==', '==:===', // Type coercion mutations
|
|
276
|
+
'!==:!=', '!=:!==', // Type coercion mutations
|
|
277
|
+
]);
|
|
278
|
+
return dangerous.has(`${from}:${to}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
package/dist/gates/runner.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SEVERITY_WEIGHTS } from '../types/index.js';
|
|
2
2
|
import { DeepAnalysisGate } from './deep-analysis.js';
|
|
3
3
|
import { persistAndReinforce } from '../storage/local-memory.js';
|
|
4
|
+
import { recordGateRun } from '../services/adaptive-thresholds.js';
|
|
4
5
|
import { FileGate } from './file.js';
|
|
5
6
|
import { ContentGate } from './content.js';
|
|
6
7
|
import { StructureGate } from './structure.js';
|
|
@@ -25,8 +26,11 @@ import { PhantomApisGate } from './phantom-apis.js';
|
|
|
25
26
|
import { DeprecatedApisGate } from './deprecated-apis.js';
|
|
26
27
|
import { TestQualityGate } from './test-quality.js';
|
|
27
28
|
import { SideEffectAnalysisGate } from './side-effect-analysis.js';
|
|
29
|
+
import { StyleDriftGate } from './style-drift.js';
|
|
30
|
+
import { LogicDriftGate } from './logic-drift.js';
|
|
28
31
|
import { execa } from 'execa';
|
|
29
32
|
import { Logger } from '../utils/logger.js';
|
|
33
|
+
import { FileSystemCache } from '../services/filesystem-cache.js';
|
|
30
34
|
export class GateRunner {
|
|
31
35
|
config;
|
|
32
36
|
gates = [];
|
|
@@ -102,6 +106,14 @@ export class GateRunner {
|
|
|
102
106
|
if (this.config.gates.side_effect_analysis?.enabled !== false) {
|
|
103
107
|
this.gates.push(new SideEffectAnalysisGate(this.config.gates.side_effect_analysis));
|
|
104
108
|
}
|
|
109
|
+
// v5.0+ Style Drift Detection (enabled by default)
|
|
110
|
+
if (this.config.gates.style_drift?.enabled !== false) {
|
|
111
|
+
this.gates.push(new StyleDriftGate(this.config.gates.style_drift));
|
|
112
|
+
}
|
|
113
|
+
// v5.0+ Logic Drift Foundation (enabled by default)
|
|
114
|
+
if (this.config.gates.logic_drift?.enabled !== false) {
|
|
115
|
+
this.gates.push(new LogicDriftGate(this.config.gates.logic_drift));
|
|
116
|
+
}
|
|
105
117
|
// Environment Alignment Gate (Should be prioritized)
|
|
106
118
|
if (this.config.gates.environment?.enabled) {
|
|
107
119
|
this.gates.unshift(new EnvironmentGate(this.config.gates));
|
|
@@ -124,10 +136,12 @@ export class GateRunner {
|
|
|
124
136
|
const engine = new ContextEngine(this.config);
|
|
125
137
|
record = await engine.discover(cwd);
|
|
126
138
|
}
|
|
139
|
+
// Create shared file cache for all gates (solves memory bloat on large repos)
|
|
140
|
+
const fileCache = new FileSystemCache();
|
|
127
141
|
// 1. Run internal gates
|
|
128
142
|
for (const gate of this.gates) {
|
|
129
143
|
try {
|
|
130
|
-
const gateFailures = await gate.run({ cwd, record, ignore, patterns });
|
|
144
|
+
const gateFailures = await gate.run({ cwd, record, ignore, patterns, fileCache });
|
|
131
145
|
if (gateFailures.length > 0) {
|
|
132
146
|
failures.push(...gateFailures);
|
|
133
147
|
summary[gate.id] = 'FAIL';
|
|
@@ -290,6 +304,20 @@ export class GateRunner {
|
|
|
290
304
|
catch {
|
|
291
305
|
// Silent — local memory is advisory, never blocks scans
|
|
292
306
|
}
|
|
307
|
+
// v5: Record per-provenance data for adaptive thresholds + temporal drift
|
|
308
|
+
try {
|
|
309
|
+
const passedCount = Object.values(summary).filter(s => s === 'PASS').length;
|
|
310
|
+
const failedCount = Object.values(summary).filter(s => s === 'FAIL').length;
|
|
311
|
+
const provenanceData = {
|
|
312
|
+
aiDriftFailures: provenanceCounts['ai-drift'],
|
|
313
|
+
structuralFailures: provenanceCounts['traditional'],
|
|
314
|
+
securityFailures: provenanceCounts['security'],
|
|
315
|
+
};
|
|
316
|
+
recordGateRun(cwd, passedCount, failedCount, failures.length, provenanceData);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
// Silent — adaptive history is advisory
|
|
320
|
+
}
|
|
293
321
|
return report;
|
|
294
322
|
}
|
|
295
323
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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, GateContext } from './base.js';
|
|
25
|
+
import { Failure, Provenance } from '../types/index.js';
|
|
26
|
+
export interface StyleDriftConfig {
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
deviation_threshold?: number;
|
|
29
|
+
sample_size?: number;
|
|
30
|
+
baseline_path?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare class StyleDriftGate extends Gate {
|
|
33
|
+
private config;
|
|
34
|
+
constructor(config?: StyleDriftConfig);
|
|
35
|
+
protected get provenance(): Provenance;
|
|
36
|
+
run(context: GateContext): Promise<Failure[]>;
|
|
37
|
+
private computeFingerprint;
|
|
38
|
+
private analyzeFile;
|
|
39
|
+
private classifyCasing;
|
|
40
|
+
private mergeIntoFingerprint;
|
|
41
|
+
private compareToBaseline;
|
|
42
|
+
/**
|
|
43
|
+
* Compare two distributions and return a deviation score (0-1).
|
|
44
|
+
* 0 = perfect match, 1 = completely different predominant style.
|
|
45
|
+
*
|
|
46
|
+
* Method: find the predominant category in each distribution.
|
|
47
|
+
* If they differ, score = how far the file is from the baseline's predominant category.
|
|
48
|
+
*/
|
|
49
|
+
private distributionDeviation;
|
|
50
|
+
private hasSignificantData;
|
|
51
|
+
/** Convert a typed distribution to a generic Record for comparison */
|
|
52
|
+
private toRecord;
|
|
53
|
+
}
|