@rigour-labs/core 4.3.6 → 5.0.1
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/hooks/checker.js +1 -1
- package/dist/hooks/templates.d.ts +6 -0
- package/dist/hooks/templates.js +6 -0
- package/dist/hooks/types.d.ts +1 -1
- package/dist/hooks/types.js +1 -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,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporal Drift Engine (v5)
|
|
3
|
+
*
|
|
4
|
+
* The "bank statement" for code quality.
|
|
5
|
+
* Reads from SQLite scan history and computes:
|
|
6
|
+
*
|
|
7
|
+
* 1. Cross-session temporal trends — how is quality changing over weeks/months?
|
|
8
|
+
* 2. Per-provenance EWMA streams — is AI getting worse? Structural? Security?
|
|
9
|
+
* 3. Anomaly detection — is today's scan statistically unusual?
|
|
10
|
+
*
|
|
11
|
+
* This is Rigour's core differentiator:
|
|
12
|
+
* No other tool can tell a CTO "your AI contributions are degrading
|
|
13
|
+
* your codebase at 3x the rate of human contributions."
|
|
14
|
+
*
|
|
15
|
+
* Data source: ~/.rigour/rigour.db (scans + findings tables)
|
|
16
|
+
* All computation is read-only — no writes to DB.
|
|
17
|
+
*
|
|
18
|
+
* @since v5.0.0
|
|
19
|
+
*/
|
|
20
|
+
import { openDatabase } from '../storage/db.js';
|
|
21
|
+
import { Logger } from '../utils/logger.js';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
// ─── EWMA + Statistical Utilities ───────────────────────────────────
|
|
24
|
+
const DEFAULT_ALPHA = 0.3;
|
|
25
|
+
function computeEWMASeries(values, alpha = DEFAULT_ALPHA) {
|
|
26
|
+
if (values.length === 0)
|
|
27
|
+
return [];
|
|
28
|
+
const series = [];
|
|
29
|
+
let ewma = values[0].value;
|
|
30
|
+
for (const point of values) {
|
|
31
|
+
ewma = alpha * point.value + (1 - alpha) * ewma;
|
|
32
|
+
series.push({
|
|
33
|
+
timestamp: point.timestamp,
|
|
34
|
+
value: point.value,
|
|
35
|
+
ewma: Math.round(ewma * 10) / 10,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return series;
|
|
39
|
+
}
|
|
40
|
+
function meanAndStd(values) {
|
|
41
|
+
if (values.length === 0)
|
|
42
|
+
return { mean: 0, std: 0 };
|
|
43
|
+
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
44
|
+
const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;
|
|
45
|
+
return { mean, std: Math.sqrt(variance) };
|
|
46
|
+
}
|
|
47
|
+
function zScore(value, mean, std) {
|
|
48
|
+
if (std === 0)
|
|
49
|
+
return 0;
|
|
50
|
+
return Math.round(((value - mean) / std) * 100) / 100;
|
|
51
|
+
}
|
|
52
|
+
function directionFromZ(z) {
|
|
53
|
+
// For failure counts: positive Z = more failures = degrading
|
|
54
|
+
if (z > 2.0)
|
|
55
|
+
return 'degrading';
|
|
56
|
+
if (z < -2.0)
|
|
57
|
+
return 'improving';
|
|
58
|
+
return 'stable';
|
|
59
|
+
}
|
|
60
|
+
function directionFromScoreZ(z) {
|
|
61
|
+
// For scores: positive Z = higher score = improving
|
|
62
|
+
if (z > 2.0)
|
|
63
|
+
return 'improving';
|
|
64
|
+
if (z < -2.0)
|
|
65
|
+
return 'degrading';
|
|
66
|
+
return 'stable';
|
|
67
|
+
}
|
|
68
|
+
// ─── Monthly/Weekly Bucketing ───────────────────────────────────────
|
|
69
|
+
function toMonthKey(timestamp) {
|
|
70
|
+
const d = new Date(timestamp);
|
|
71
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
|
|
72
|
+
}
|
|
73
|
+
function toWeekKey(timestamp) {
|
|
74
|
+
const d = new Date(timestamp);
|
|
75
|
+
// Get Monday of this week (don't mutate d in-place)
|
|
76
|
+
const day = d.getDay();
|
|
77
|
+
const diff = d.getDate() - day + (day === 0 ? -6 : 1);
|
|
78
|
+
const monday = new Date(d);
|
|
79
|
+
monday.setDate(diff);
|
|
80
|
+
return monday.toISOString().split('T')[0];
|
|
81
|
+
}
|
|
82
|
+
// ─── Main Engine ────────────────────────────────────────────────────
|
|
83
|
+
/**
|
|
84
|
+
* Generate a complete temporal drift report for a project.
|
|
85
|
+
*
|
|
86
|
+
* Reads from SQLite: scans (scores over time) + findings (provenance counts).
|
|
87
|
+
* Computes EWMA streams, Z-scores, monthly/weekly rollups, and a narrative.
|
|
88
|
+
*
|
|
89
|
+
* @param cwd - Project root path (used to derive repo name)
|
|
90
|
+
* @param maxScans - Max scans to analyze (default 200)
|
|
91
|
+
*/
|
|
92
|
+
export function generateTemporalDriftReport(cwd, maxScans = 200) {
|
|
93
|
+
const db = openDatabase();
|
|
94
|
+
if (!db) {
|
|
95
|
+
Logger.warn('Temporal drift: SQLite not available');
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const repo = path.basename(cwd);
|
|
99
|
+
try {
|
|
100
|
+
// 1. Load all scans (oldest first)
|
|
101
|
+
const scans = db.db.prepare(`
|
|
102
|
+
SELECT * FROM scans WHERE repo = ?
|
|
103
|
+
ORDER BY timestamp ASC LIMIT ?
|
|
104
|
+
`).all(repo, maxScans);
|
|
105
|
+
if (scans.length < 3) {
|
|
106
|
+
return createEmptyReport(repo, scans.length);
|
|
107
|
+
}
|
|
108
|
+
// 2. Load per-scan provenance failure counts
|
|
109
|
+
const provenanceByScan = new Map();
|
|
110
|
+
for (const scan of scans) {
|
|
111
|
+
const counts = db.db.prepare(`
|
|
112
|
+
SELECT provenance, COUNT(*) as cnt FROM findings
|
|
113
|
+
WHERE scan_id = ? GROUP BY provenance
|
|
114
|
+
`).all(scan.id);
|
|
115
|
+
const breakdown = { aiDrift: 0, structural: 0, security: 0 };
|
|
116
|
+
for (const row of counts) {
|
|
117
|
+
if (row.provenance === 'ai-drift')
|
|
118
|
+
breakdown.aiDrift = row.cnt;
|
|
119
|
+
else if (row.provenance === 'traditional')
|
|
120
|
+
breakdown.structural = row.cnt;
|
|
121
|
+
else if (row.provenance === 'security')
|
|
122
|
+
breakdown.security = row.cnt;
|
|
123
|
+
}
|
|
124
|
+
provenanceByScan.set(scan.id, breakdown);
|
|
125
|
+
}
|
|
126
|
+
// 3. Build EWMA streams
|
|
127
|
+
const overallStream = buildStream(scans.map(s => ({ timestamp: s.timestamp, value: s.overall_score ?? 100 })), true // higher score = better
|
|
128
|
+
);
|
|
129
|
+
const aiDriftStream = buildStream(scans.map(s => ({
|
|
130
|
+
timestamp: s.timestamp,
|
|
131
|
+
value: provenanceByScan.get(s.id)?.aiDrift ?? 0,
|
|
132
|
+
})), false // higher count = worse
|
|
133
|
+
);
|
|
134
|
+
const structuralStream = buildStream(scans.map(s => ({
|
|
135
|
+
timestamp: s.timestamp,
|
|
136
|
+
value: provenanceByScan.get(s.id)?.structural ?? 0,
|
|
137
|
+
})), false);
|
|
138
|
+
const securityStream = buildStream(scans.map(s => ({
|
|
139
|
+
timestamp: s.timestamp,
|
|
140
|
+
value: provenanceByScan.get(s.id)?.security ?? 0,
|
|
141
|
+
})), false);
|
|
142
|
+
// 4. Monthly rollups
|
|
143
|
+
const monthMap = new Map();
|
|
144
|
+
for (const scan of scans) {
|
|
145
|
+
const key = toMonthKey(scan.timestamp);
|
|
146
|
+
const prov = provenanceByScan.get(scan.id) || { aiDrift: 0, structural: 0, security: 0 };
|
|
147
|
+
if (!monthMap.has(key)) {
|
|
148
|
+
monthMap.set(key, {
|
|
149
|
+
month: key,
|
|
150
|
+
avgScore: 0,
|
|
151
|
+
avgAiHealth: 0,
|
|
152
|
+
avgStructural: 0,
|
|
153
|
+
scanCount: 0,
|
|
154
|
+
totalFailures: 0,
|
|
155
|
+
provenanceBreakdown: { aiDrift: 0, structural: 0, security: 0 },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const bucket = monthMap.get(key);
|
|
159
|
+
bucket.scanCount++;
|
|
160
|
+
bucket.avgScore += scan.overall_score ?? 100;
|
|
161
|
+
bucket.avgAiHealth += scan.ai_health_score ?? 100;
|
|
162
|
+
bucket.totalFailures += prov.aiDrift + prov.structural + prov.security;
|
|
163
|
+
bucket.provenanceBreakdown.aiDrift += prov.aiDrift;
|
|
164
|
+
bucket.provenanceBreakdown.structural += prov.structural;
|
|
165
|
+
bucket.provenanceBreakdown.security += prov.security;
|
|
166
|
+
}
|
|
167
|
+
const monthly = Array.from(monthMap.values())
|
|
168
|
+
.filter(b => b.scanCount > 0)
|
|
169
|
+
.map(b => ({
|
|
170
|
+
...b,
|
|
171
|
+
avgScore: Math.round(b.avgScore / b.scanCount),
|
|
172
|
+
avgAiHealth: Math.round(b.avgAiHealth / b.scanCount),
|
|
173
|
+
avgStructural: Math.round((100 - (b.provenanceBreakdown.structural / b.scanCount) * 5)),
|
|
174
|
+
}));
|
|
175
|
+
// 5. Weekly rollups
|
|
176
|
+
const weekMap = new Map();
|
|
177
|
+
for (const scan of scans) {
|
|
178
|
+
const key = toWeekKey(scan.timestamp);
|
|
179
|
+
if (!weekMap.has(key)) {
|
|
180
|
+
weekMap.set(key, { weekStart: key, avgScore: 0, avgAiHealth: 0, scanCount: 0 });
|
|
181
|
+
}
|
|
182
|
+
const bucket = weekMap.get(key);
|
|
183
|
+
bucket.scanCount++;
|
|
184
|
+
bucket.avgScore += scan.overall_score ?? 100;
|
|
185
|
+
bucket.avgAiHealth += scan.ai_health_score ?? 100;
|
|
186
|
+
}
|
|
187
|
+
const weekly = Array.from(weekMap.values())
|
|
188
|
+
.filter(b => b.scanCount > 0)
|
|
189
|
+
.map(b => ({
|
|
190
|
+
...b,
|
|
191
|
+
avgScore: Math.round(b.avgScore / b.scanCount),
|
|
192
|
+
avgAiHealth: Math.round(b.avgAiHealth / b.scanCount),
|
|
193
|
+
}));
|
|
194
|
+
// 6. Latest scan anomaly detection
|
|
195
|
+
const recentScores = scans.slice(-20).map(s => s.overall_score ?? 100);
|
|
196
|
+
const { mean, std } = meanAndStd(recentScores);
|
|
197
|
+
const latestScore = scans[scans.length - 1].overall_score ?? 100;
|
|
198
|
+
const latestZ = zScore(latestScore, mean, std);
|
|
199
|
+
const latestAnomaly = {
|
|
200
|
+
isAnomaly: Math.abs(latestZ) > 2.0,
|
|
201
|
+
direction: latestZ > 2.0 ? 'spike' : latestZ < -2.0 ? 'dip' : 'normal',
|
|
202
|
+
zScore: latestZ,
|
|
203
|
+
message: Math.abs(latestZ) > 2.0
|
|
204
|
+
? `Latest scan score (${latestScore}) is ${latestZ > 0 ? 'unusually high' : 'unusually low'} (Z=${latestZ})`
|
|
205
|
+
: `Latest scan score (${latestScore}) is within normal range`,
|
|
206
|
+
};
|
|
207
|
+
// 7. Time span
|
|
208
|
+
const firstTs = scans[0].timestamp;
|
|
209
|
+
const lastTs = scans[scans.length - 1].timestamp;
|
|
210
|
+
const timeSpanDays = Math.round((lastTs - firstTs) / (1000 * 60 * 60 * 24));
|
|
211
|
+
// 8. Generate narrative
|
|
212
|
+
const narrative = generateNarrative(repo, timeSpanDays, monthly, overallStream, aiDriftStream, structuralStream, securityStream);
|
|
213
|
+
return {
|
|
214
|
+
repo,
|
|
215
|
+
totalScans: scans.length,
|
|
216
|
+
timeSpanDays,
|
|
217
|
+
overallDirection: overallStream.direction,
|
|
218
|
+
overallZScore: overallStream.zScore,
|
|
219
|
+
streams: {
|
|
220
|
+
overall: overallStream,
|
|
221
|
+
aiDrift: aiDriftStream,
|
|
222
|
+
structural: structuralStream,
|
|
223
|
+
security: securityStream,
|
|
224
|
+
},
|
|
225
|
+
monthly,
|
|
226
|
+
weekly,
|
|
227
|
+
latestScanAnomaly: latestAnomaly,
|
|
228
|
+
narrative,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
Logger.warn(`Temporal drift generation failed: ${error}`);
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
db.close();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
|
240
|
+
function buildStream(data, higherIsBetter) {
|
|
241
|
+
const series = computeEWMASeries(data);
|
|
242
|
+
const values = data.map(d => d.value);
|
|
243
|
+
const { mean, std } = meanAndStd(values);
|
|
244
|
+
// Z-score of recent average (last 5) against full history
|
|
245
|
+
const recentValues = values.slice(-5);
|
|
246
|
+
const recentAvg = recentValues.length > 0
|
|
247
|
+
? recentValues.reduce((a, b) => a + b, 0) / recentValues.length
|
|
248
|
+
: 0;
|
|
249
|
+
const z = zScore(recentAvg, mean, std);
|
|
250
|
+
const direction = higherIsBetter
|
|
251
|
+
? directionFromScoreZ(z)
|
|
252
|
+
: directionFromZ(z); // for failure counts: positive Z = more = degrading
|
|
253
|
+
return {
|
|
254
|
+
direction,
|
|
255
|
+
zScore: z,
|
|
256
|
+
series,
|
|
257
|
+
currentEWMA: series.length > 0 ? series[series.length - 1].ewma : 0,
|
|
258
|
+
historicalAvg: Math.round(mean * 10) / 10,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function createEmptyReport(repo, scanCount) {
|
|
262
|
+
const emptyStream = {
|
|
263
|
+
direction: 'stable', zScore: 0, series: [], currentEWMA: 0, historicalAvg: 0,
|
|
264
|
+
};
|
|
265
|
+
return {
|
|
266
|
+
repo,
|
|
267
|
+
totalScans: scanCount,
|
|
268
|
+
timeSpanDays: 0,
|
|
269
|
+
overallDirection: 'stable',
|
|
270
|
+
overallZScore: 0,
|
|
271
|
+
streams: {
|
|
272
|
+
overall: emptyStream,
|
|
273
|
+
aiDrift: { ...emptyStream },
|
|
274
|
+
structural: { ...emptyStream },
|
|
275
|
+
security: { ...emptyStream },
|
|
276
|
+
},
|
|
277
|
+
monthly: [],
|
|
278
|
+
weekly: [],
|
|
279
|
+
latestScanAnomaly: { isAnomaly: false, direction: 'normal', zScore: 0, message: 'Not enough data' },
|
|
280
|
+
narrative: `${repo}: Only ${scanCount} scans recorded. Need at least 3 for trend analysis.`,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Generate the human-readable narrative that answers:
|
|
285
|
+
* "3 mahine mein hum better hue ya worse?"
|
|
286
|
+
*/
|
|
287
|
+
function generateNarrative(repo, days, monthly, overall, aiDrift, structural, security) {
|
|
288
|
+
const parts = [];
|
|
289
|
+
// Time span
|
|
290
|
+
if (days > 30) {
|
|
291
|
+
parts.push(`Over the last ${Math.round(days / 30)} months`);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
parts.push(`Over the last ${days} days`);
|
|
295
|
+
}
|
|
296
|
+
// Overall direction
|
|
297
|
+
if (overall.direction === 'improving') {
|
|
298
|
+
parts.push(`${repo} quality is IMPROVING (score trending up, Z=${overall.zScore}).`);
|
|
299
|
+
}
|
|
300
|
+
else if (overall.direction === 'degrading') {
|
|
301
|
+
parts.push(`${repo} quality is DEGRADING (score trending down, Z=${overall.zScore}).`);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
parts.push(`${repo} quality is STABLE.`);
|
|
305
|
+
}
|
|
306
|
+
// Per-provenance diagnosis
|
|
307
|
+
const problems = [];
|
|
308
|
+
if (aiDrift.direction === 'degrading') {
|
|
309
|
+
problems.push(`AI-generated code is getting worse (Z=${aiDrift.zScore}) — agents are producing more drift`);
|
|
310
|
+
}
|
|
311
|
+
if (structural.direction === 'degrading') {
|
|
312
|
+
problems.push(`Structural code quality is declining (Z=${structural.zScore}) — team may be taking shortcuts`);
|
|
313
|
+
}
|
|
314
|
+
if (security.direction === 'degrading') {
|
|
315
|
+
problems.push(`Security posture is weakening (Z=${security.zScore}) — credential/vulnerability issues increasing`);
|
|
316
|
+
}
|
|
317
|
+
if (problems.length > 0) {
|
|
318
|
+
parts.push('Root causes: ' + problems.join('; ') + '.');
|
|
319
|
+
}
|
|
320
|
+
// Monthly progression
|
|
321
|
+
if (monthly.length >= 2) {
|
|
322
|
+
const first = monthly[0];
|
|
323
|
+
const last = monthly[monthly.length - 1];
|
|
324
|
+
const scoreDelta = last.avgScore - first.avgScore;
|
|
325
|
+
const failDelta = last.totalFailures / last.scanCount - first.totalFailures / first.scanCount;
|
|
326
|
+
if (Math.abs(scoreDelta) > 5) {
|
|
327
|
+
parts.push(`Monthly avg score: ${first.avgScore} (${first.month}) → ${last.avgScore} (${last.month}), ` +
|
|
328
|
+
`delta ${scoreDelta > 0 ? '+' : ''}${scoreDelta}.`);
|
|
329
|
+
}
|
|
330
|
+
if (Math.abs(failDelta) > 1) {
|
|
331
|
+
parts.push(`Avg failures/scan: ${(first.totalFailures / first.scanCount).toFixed(1)} → ${(last.totalFailures / last.scanCount).toFixed(1)}.`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Bright spots
|
|
335
|
+
const improvements = [];
|
|
336
|
+
if (aiDrift.direction === 'improving')
|
|
337
|
+
improvements.push('AI drift reducing');
|
|
338
|
+
if (structural.direction === 'improving')
|
|
339
|
+
improvements.push('structural quality improving');
|
|
340
|
+
if (security.direction === 'improving')
|
|
341
|
+
improvements.push('security posture strengthening');
|
|
342
|
+
if (improvements.length > 0) {
|
|
343
|
+
parts.push('Bright spots: ' + improvements.join(', ') + '.');
|
|
344
|
+
}
|
|
345
|
+
return parts.join(' ');
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Get a formatted summary string for CLI/MCP output.
|
|
349
|
+
*/
|
|
350
|
+
export function formatDriftSummary(report) {
|
|
351
|
+
const lines = [];
|
|
352
|
+
lines.push(`═══ Temporal Drift: ${report.repo} (${report.totalScans} scans, ${report.timeSpanDays} days) ═══`);
|
|
353
|
+
lines.push('');
|
|
354
|
+
// EWMA Streams
|
|
355
|
+
const bar = (direction) => {
|
|
356
|
+
switch (direction) {
|
|
357
|
+
case 'improving': return '████████░░ IMPROVING';
|
|
358
|
+
case 'stable': return '█████░░░░░ stable';
|
|
359
|
+
case 'degrading': return '██░░░░░░░░ DEGRADING';
|
|
360
|
+
default: return '░░░░░░░░░░ unknown';
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
lines.push('Per-Provenance Health:');
|
|
364
|
+
lines.push(` AI Drift: ${bar(report.streams.aiDrift.direction)} (Z=${report.streams.aiDrift.zScore})`);
|
|
365
|
+
lines.push(` Structural: ${bar(report.streams.structural.direction)} (Z=${report.streams.structural.zScore})`);
|
|
366
|
+
lines.push(` Security: ${bar(report.streams.security.direction)} (Z=${report.streams.security.zScore})`);
|
|
367
|
+
lines.push(` Overall: ${bar(report.streams.overall.direction)} (Z=${report.streams.overall.zScore})`);
|
|
368
|
+
lines.push('');
|
|
369
|
+
// Monthly trend
|
|
370
|
+
if (report.monthly.length > 0) {
|
|
371
|
+
lines.push('Monthly Trend:');
|
|
372
|
+
for (const m of report.monthly.slice(-6)) {
|
|
373
|
+
const failPerScan = (m.totalFailures / m.scanCount).toFixed(1);
|
|
374
|
+
lines.push(` ${m.month}: score=${m.avgScore}, failures/scan=${failPerScan}, scans=${m.scanCount}`);
|
|
375
|
+
}
|
|
376
|
+
lines.push('');
|
|
377
|
+
}
|
|
378
|
+
// Anomaly
|
|
379
|
+
if (report.latestScanAnomaly.isAnomaly) {
|
|
380
|
+
lines.push(`WARNING: ${report.latestScanAnomaly.message}`);
|
|
381
|
+
lines.push('');
|
|
382
|
+
}
|
|
383
|
+
// Narrative
|
|
384
|
+
lines.push(report.narrative);
|
|
385
|
+
return lines.join('\n');
|
|
386
|
+
}
|
|
@@ -18,6 +18,10 @@ export const UNIVERSAL_CONFIG = {
|
|
|
18
18
|
},
|
|
19
19
|
dependencies: {
|
|
20
20
|
forbid: [],
|
|
21
|
+
detect_unused: true,
|
|
22
|
+
detect_heavy_alternatives: true,
|
|
23
|
+
detect_duplicate_purpose: true,
|
|
24
|
+
unused_allowlist: [],
|
|
21
25
|
},
|
|
22
26
|
architecture: {
|
|
23
27
|
boundaries: [],
|
|
@@ -217,6 +221,19 @@ export const UNIVERSAL_CONFIG = {
|
|
|
217
221
|
ignore_patterns: [],
|
|
218
222
|
audit_log: true,
|
|
219
223
|
},
|
|
224
|
+
style_drift: {
|
|
225
|
+
enabled: true,
|
|
226
|
+
deviation_threshold: 0.25,
|
|
227
|
+
sample_size: 100,
|
|
228
|
+
baseline_path: '.rigour/style-baseline.json',
|
|
229
|
+
},
|
|
230
|
+
logic_drift: {
|
|
231
|
+
enabled: true,
|
|
232
|
+
baseline_path: '.rigour/logic-baseline.json',
|
|
233
|
+
track_operators: true,
|
|
234
|
+
track_branches: true,
|
|
235
|
+
track_returns: true,
|
|
236
|
+
},
|
|
220
237
|
side_effect_analysis: {
|
|
221
238
|
enabled: true,
|
|
222
239
|
check_unbounded_timers: true,
|