@rigour-labs/core 2.22.0 → 3.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 +58 -0
- package/dist/context.test.js +2 -3
- package/dist/environment.test.js +2 -1
- package/dist/gates/agent-team.d.ts +2 -1
- package/dist/gates/agent-team.js +1 -0
- package/dist/gates/base.d.ts +3 -1
- package/dist/gates/base.js +3 -0
- package/dist/gates/checkpoint.d.ts +2 -1
- package/dist/gates/checkpoint.js +3 -2
- package/dist/gates/context-window-artifacts.d.ts +2 -1
- package/dist/gates/context-window-artifacts.js +6 -3
- package/dist/gates/context.d.ts +2 -1
- package/dist/gates/context.js +1 -0
- package/dist/gates/coverage.js +3 -1
- package/dist/gates/dependency.js +5 -5
- package/dist/gates/duplication-drift.d.ts +2 -1
- package/dist/gates/duplication-drift.js +4 -1
- package/dist/gates/environment.js +4 -4
- package/dist/gates/hallucinated-imports.d.ts +21 -2
- package/dist/gates/hallucinated-imports.js +116 -2
- package/dist/gates/inconsistent-error-handling.d.ts +2 -1
- package/dist/gates/inconsistent-error-handling.js +21 -7
- package/dist/gates/promise-safety.d.ts +68 -0
- package/dist/gates/promise-safety.js +509 -0
- package/dist/gates/retry-loop-breaker.d.ts +2 -1
- package/dist/gates/retry-loop-breaker.js +2 -1
- package/dist/gates/runner.js +34 -1
- package/dist/gates/safety.d.ts +2 -1
- package/dist/gates/safety.js +2 -1
- package/dist/gates/security-patterns-owasp.test.d.ts +1 -0
- package/dist/gates/security-patterns-owasp.test.js +171 -0
- package/dist/gates/security-patterns.d.ts +6 -1
- package/dist/gates/security-patterns.js +101 -0
- package/dist/gates/structure.js +1 -1
- package/dist/hooks/checker.d.ts +23 -0
- package/dist/hooks/checker.js +222 -0
- package/dist/hooks/checker.test.d.ts +1 -0
- package/dist/hooks/checker.test.js +132 -0
- package/dist/hooks/index.d.ts +9 -0
- package/dist/hooks/index.js +8 -0
- package/dist/hooks/standalone-checker.d.ts +15 -0
- package/dist/hooks/standalone-checker.js +106 -0
- package/dist/hooks/templates.d.ts +22 -0
- package/dist/hooks/templates.js +232 -0
- package/dist/hooks/types.d.ts +34 -0
- package/dist/hooks/types.js +21 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/services/fix-packet-service.d.ts +0 -1
- package/dist/services/fix-packet-service.js +9 -14
- package/dist/services/score-history.d.ts +54 -0
- package/dist/services/score-history.js +122 -0
- package/dist/templates/index.js +176 -0
- package/dist/types/fix-packet.d.ts +5 -5
- package/dist/types/fix-packet.js +1 -1
- package/dist/types/index.d.ts +207 -0
- package/dist/types/index.js +32 -0
- package/package.json +21 -1
- package/src/context.test.ts +0 -256
- package/src/discovery.test.ts +0 -88
- package/src/discovery.ts +0 -112
- package/src/environment.test.ts +0 -115
- package/src/gates/agent-team.test.ts +0 -134
- package/src/gates/agent-team.ts +0 -210
- package/src/gates/ast-handlers/base.ts +0 -13
- package/src/gates/ast-handlers/python.ts +0 -145
- package/src/gates/ast-handlers/python_parser.py +0 -181
- package/src/gates/ast-handlers/typescript.ts +0 -264
- package/src/gates/ast-handlers/universal.ts +0 -184
- package/src/gates/ast.ts +0 -54
- package/src/gates/base.ts +0 -28
- package/src/gates/checkpoint.test.ts +0 -135
- package/src/gates/checkpoint.ts +0 -311
- package/src/gates/content.ts +0 -51
- package/src/gates/context-window-artifacts.ts +0 -277
- package/src/gates/context.ts +0 -270
- package/src/gates/coverage.ts +0 -74
- package/src/gates/dependency.ts +0 -108
- package/src/gates/duplication-drift.ts +0 -231
- package/src/gates/environment.ts +0 -94
- package/src/gates/file.ts +0 -46
- package/src/gates/hallucinated-imports.ts +0 -361
- package/src/gates/inconsistent-error-handling.ts +0 -254
- package/src/gates/retry-loop-breaker.ts +0 -151
- package/src/gates/runner.ts +0 -188
- package/src/gates/safety.ts +0 -56
- package/src/gates/security-patterns.test.ts +0 -162
- package/src/gates/security-patterns.ts +0 -306
- package/src/gates/structure.ts +0 -36
- package/src/index.ts +0 -13
- package/src/pattern-index/embeddings.ts +0 -84
- package/src/pattern-index/index.ts +0 -59
- package/src/pattern-index/indexer.test.ts +0 -276
- package/src/pattern-index/indexer.ts +0 -1023
- package/src/pattern-index/matcher.test.ts +0 -293
- package/src/pattern-index/matcher.ts +0 -493
- package/src/pattern-index/overrides.ts +0 -235
- package/src/pattern-index/security.ts +0 -151
- package/src/pattern-index/staleness.test.ts +0 -313
- package/src/pattern-index/staleness.ts +0 -568
- package/src/pattern-index/types.ts +0 -339
- package/src/safety.test.ts +0 -53
- package/src/services/adaptive-thresholds.test.ts +0 -189
- package/src/services/adaptive-thresholds.ts +0 -275
- package/src/services/context-engine.ts +0 -104
- package/src/services/fix-packet-service.ts +0 -42
- package/src/services/state-service.ts +0 -138
- package/src/smoke.test.ts +0 -18
- package/src/templates/index.ts +0 -338
- package/src/types/fix-packet.ts +0 -32
- package/src/types/index.ts +0 -200
- package/src/utils/logger.ts +0 -43
- package/src/utils/scanner.test.ts +0 -37
- package/src/utils/scanner.ts +0 -43
- package/tsconfig.json +0 -10
- package/vitest.config.ts +0 -7
- package/vitest.setup.ts +0 -30
package/src/gates/checkpoint.ts
DELETED
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Checkpoint Supervision Gate
|
|
3
|
-
*
|
|
4
|
-
* Monitors agent quality during extended execution for frontier models
|
|
5
|
-
* like GPT-5.3-Codex "coworking mode" that run autonomously for long periods.
|
|
6
|
-
*
|
|
7
|
-
* Features:
|
|
8
|
-
* - Time-based checkpoint triggers
|
|
9
|
-
* - Quality score tracking
|
|
10
|
-
* - Drift detection (quality degradation over time)
|
|
11
|
-
* - Auto-save on failure
|
|
12
|
-
*
|
|
13
|
-
* @since v2.14.0
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { Gate, GateContext } from './base.js';
|
|
17
|
-
import { Failure } from '../types/index.js';
|
|
18
|
-
import { Logger } from '../utils/logger.js';
|
|
19
|
-
import * as fs from 'fs';
|
|
20
|
-
import * as path from 'path';
|
|
21
|
-
|
|
22
|
-
export interface CheckpointEntry {
|
|
23
|
-
checkpointId: string;
|
|
24
|
-
timestamp: Date;
|
|
25
|
-
progressPct: number;
|
|
26
|
-
filesChanged: string[];
|
|
27
|
-
summary: string;
|
|
28
|
-
qualityScore: number;
|
|
29
|
-
warnings: string[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface CheckpointSession {
|
|
33
|
-
sessionId: string;
|
|
34
|
-
startedAt: Date;
|
|
35
|
-
lastCheckpoint?: Date;
|
|
36
|
-
checkpoints: CheckpointEntry[];
|
|
37
|
-
status: 'active' | 'completed' | 'aborted';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface CheckpointConfig {
|
|
41
|
-
enabled?: boolean;
|
|
42
|
-
interval_minutes?: number;
|
|
43
|
-
quality_threshold?: number;
|
|
44
|
-
drift_detection?: boolean;
|
|
45
|
-
auto_save_on_failure?: boolean;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// In-memory checkpoint store (persisted to .rigour/checkpoint-session.json)
|
|
49
|
-
let currentCheckpointSession: CheckpointSession | null = null;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Generate unique checkpoint ID
|
|
53
|
-
*/
|
|
54
|
-
function generateCheckpointId(): string {
|
|
55
|
-
return `cp-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Get or create checkpoint session
|
|
60
|
-
*/
|
|
61
|
-
export function getOrCreateCheckpointSession(cwd: string): CheckpointSession {
|
|
62
|
-
if (!currentCheckpointSession) {
|
|
63
|
-
loadCheckpointSession(cwd);
|
|
64
|
-
}
|
|
65
|
-
if (!currentCheckpointSession) {
|
|
66
|
-
currentCheckpointSession = {
|
|
67
|
-
sessionId: `chk-session-${Date.now()}`,
|
|
68
|
-
startedAt: new Date(),
|
|
69
|
-
checkpoints: [],
|
|
70
|
-
status: 'active',
|
|
71
|
-
};
|
|
72
|
-
persistCheckpointSession(cwd);
|
|
73
|
-
}
|
|
74
|
-
return currentCheckpointSession;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Record a checkpoint with quality evaluation
|
|
79
|
-
*/
|
|
80
|
-
export function recordCheckpoint(
|
|
81
|
-
cwd: string,
|
|
82
|
-
progressPct: number,
|
|
83
|
-
filesChanged: string[],
|
|
84
|
-
summary: string,
|
|
85
|
-
qualityScore: number
|
|
86
|
-
): { continue: boolean; warnings: string[]; checkpoint: CheckpointEntry } {
|
|
87
|
-
const session = getOrCreateCheckpointSession(cwd);
|
|
88
|
-
const warnings: string[] = [];
|
|
89
|
-
|
|
90
|
-
// Default threshold
|
|
91
|
-
const qualityThreshold = 80;
|
|
92
|
-
|
|
93
|
-
// Check if quality is below threshold
|
|
94
|
-
const shouldContinue = qualityScore >= qualityThreshold;
|
|
95
|
-
if (!shouldContinue) {
|
|
96
|
-
warnings.push(`Quality score ${qualityScore}% is below threshold ${qualityThreshold}%`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Detect drift (quality degradation over recent checkpoints)
|
|
100
|
-
if (session.checkpoints.length >= 2) {
|
|
101
|
-
const recentScores = session.checkpoints.slice(-3).map(cp => cp.qualityScore);
|
|
102
|
-
const avgRecent = recentScores.reduce((a, b) => a + b, 0) / recentScores.length;
|
|
103
|
-
|
|
104
|
-
if (qualityScore < avgRecent - 10) {
|
|
105
|
-
warnings.push(`Drift detected: quality dropped from avg ${avgRecent.toFixed(0)}% to ${qualityScore}%`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const checkpoint: CheckpointEntry = {
|
|
110
|
-
checkpointId: generateCheckpointId(),
|
|
111
|
-
timestamp: new Date(),
|
|
112
|
-
progressPct,
|
|
113
|
-
filesChanged,
|
|
114
|
-
summary,
|
|
115
|
-
qualityScore,
|
|
116
|
-
warnings,
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
session.checkpoints.push(checkpoint);
|
|
120
|
-
session.lastCheckpoint = new Date();
|
|
121
|
-
persistCheckpointSession(cwd);
|
|
122
|
-
|
|
123
|
-
return { continue: shouldContinue, warnings, checkpoint };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Get current checkpoint session
|
|
128
|
-
*/
|
|
129
|
-
export function getCheckpointSession(cwd: string): CheckpointSession | null {
|
|
130
|
-
if (!currentCheckpointSession) {
|
|
131
|
-
loadCheckpointSession(cwd);
|
|
132
|
-
}
|
|
133
|
-
return currentCheckpointSession;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Complete checkpoint session
|
|
138
|
-
*/
|
|
139
|
-
export function completeCheckpointSession(cwd: string): void {
|
|
140
|
-
if (currentCheckpointSession) {
|
|
141
|
-
currentCheckpointSession.status = 'completed';
|
|
142
|
-
persistCheckpointSession(cwd);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Abort checkpoint session (quality too low)
|
|
148
|
-
*/
|
|
149
|
-
export function abortCheckpointSession(cwd: string, reason: string): void {
|
|
150
|
-
if (currentCheckpointSession) {
|
|
151
|
-
currentCheckpointSession.status = 'aborted';
|
|
152
|
-
// Add final checkpoint with abort reason
|
|
153
|
-
currentCheckpointSession.checkpoints.push({
|
|
154
|
-
checkpointId: generateCheckpointId(),
|
|
155
|
-
timestamp: new Date(),
|
|
156
|
-
progressPct: currentCheckpointSession.checkpoints.length > 0
|
|
157
|
-
? currentCheckpointSession.checkpoints[currentCheckpointSession.checkpoints.length - 1].progressPct
|
|
158
|
-
: 0,
|
|
159
|
-
filesChanged: [],
|
|
160
|
-
summary: `Session aborted: ${reason}`,
|
|
161
|
-
qualityScore: 0,
|
|
162
|
-
warnings: [reason],
|
|
163
|
-
});
|
|
164
|
-
persistCheckpointSession(cwd);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Clear checkpoint session
|
|
170
|
-
*/
|
|
171
|
-
export function clearCheckpointSession(cwd: string): void {
|
|
172
|
-
currentCheckpointSession = null;
|
|
173
|
-
const sessionPath = path.join(cwd, '.rigour', 'checkpoint-session.json');
|
|
174
|
-
if (fs.existsSync(sessionPath)) {
|
|
175
|
-
fs.unlinkSync(sessionPath);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function persistCheckpointSession(cwd: string): void {
|
|
180
|
-
const rigourDir = path.join(cwd, '.rigour');
|
|
181
|
-
if (!fs.existsSync(rigourDir)) {
|
|
182
|
-
fs.mkdirSync(rigourDir, { recursive: true });
|
|
183
|
-
}
|
|
184
|
-
const sessionPath = path.join(rigourDir, 'checkpoint-session.json');
|
|
185
|
-
fs.writeFileSync(sessionPath, JSON.stringify(currentCheckpointSession, null, 2));
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function loadCheckpointSession(cwd: string): void {
|
|
189
|
-
const sessionPath = path.join(cwd, '.rigour', 'checkpoint-session.json');
|
|
190
|
-
if (fs.existsSync(sessionPath)) {
|
|
191
|
-
try {
|
|
192
|
-
const data = JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
|
|
193
|
-
currentCheckpointSession = {
|
|
194
|
-
...data,
|
|
195
|
-
startedAt: new Date(data.startedAt),
|
|
196
|
-
lastCheckpoint: data.lastCheckpoint ? new Date(data.lastCheckpoint) : undefined,
|
|
197
|
-
checkpoints: data.checkpoints.map((cp: any) => ({
|
|
198
|
-
...cp,
|
|
199
|
-
timestamp: new Date(cp.timestamp),
|
|
200
|
-
})),
|
|
201
|
-
};
|
|
202
|
-
} catch (err) {
|
|
203
|
-
Logger.warn('Failed to load checkpoint session, starting fresh');
|
|
204
|
-
currentCheckpointSession = null;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Calculate time since last checkpoint
|
|
211
|
-
*/
|
|
212
|
-
function timeSinceLastCheckpoint(session: CheckpointSession): number {
|
|
213
|
-
const lastTime = session.lastCheckpoint || session.startedAt;
|
|
214
|
-
return (Date.now() - lastTime.getTime()) / 1000 / 60; // minutes
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Detect quality drift pattern
|
|
219
|
-
*/
|
|
220
|
-
function detectDrift(checkpoints: CheckpointEntry[]): { hasDrift: boolean; trend: 'improving' | 'stable' | 'degrading' } {
|
|
221
|
-
if (checkpoints.length < 3) {
|
|
222
|
-
return { hasDrift: false, trend: 'stable' };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const recent = checkpoints.slice(-5);
|
|
226
|
-
const scores = recent.map(cp => cp.qualityScore);
|
|
227
|
-
|
|
228
|
-
// Calculate trend using simple linear regression
|
|
229
|
-
const n = scores.length;
|
|
230
|
-
const sumX = (n * (n - 1)) / 2;
|
|
231
|
-
const sumY = scores.reduce((a, b) => a + b, 0);
|
|
232
|
-
const sumXY = scores.reduce((sum, y, x) => sum + x * y, 0);
|
|
233
|
-
const sumX2 = (n * (n - 1) * (2 * n - 1)) / 6;
|
|
234
|
-
|
|
235
|
-
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
|
|
236
|
-
|
|
237
|
-
if (slope < -2) {
|
|
238
|
-
return { hasDrift: true, trend: 'degrading' };
|
|
239
|
-
} else if (slope > 2) {
|
|
240
|
-
return { hasDrift: false, trend: 'improving' };
|
|
241
|
-
}
|
|
242
|
-
return { hasDrift: false, trend: 'stable' };
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
export class CheckpointGate extends Gate {
|
|
246
|
-
private config: CheckpointConfig;
|
|
247
|
-
|
|
248
|
-
constructor(config: CheckpointConfig = {}) {
|
|
249
|
-
super('checkpoint', 'Checkpoint Supervision');
|
|
250
|
-
this.config = {
|
|
251
|
-
enabled: config.enabled ?? false,
|
|
252
|
-
interval_minutes: config.interval_minutes ?? 15,
|
|
253
|
-
quality_threshold: config.quality_threshold ?? 80,
|
|
254
|
-
drift_detection: config.drift_detection ?? true,
|
|
255
|
-
auto_save_on_failure: config.auto_save_on_failure ?? true,
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async run(context: GateContext): Promise<Failure[]> {
|
|
260
|
-
if (!this.config.enabled) {
|
|
261
|
-
return [];
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const failures: Failure[] = [];
|
|
265
|
-
const session = getCheckpointSession(context.cwd);
|
|
266
|
-
|
|
267
|
-
if (!session || session.checkpoints.length === 0) {
|
|
268
|
-
// No checkpoints yet, skip
|
|
269
|
-
return [];
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
Logger.info(`Checkpoint Gate: ${session.checkpoints.length} checkpoints in session`);
|
|
273
|
-
|
|
274
|
-
// Check 1: Time since last checkpoint
|
|
275
|
-
const minutesSinceLast = timeSinceLastCheckpoint(session);
|
|
276
|
-
if (minutesSinceLast > (this.config.interval_minutes ?? 15) * 2) {
|
|
277
|
-
failures.push(this.createFailure(
|
|
278
|
-
`No checkpoint in ${minutesSinceLast.toFixed(0)} minutes (expected every ${this.config.interval_minutes} min)`,
|
|
279
|
-
undefined,
|
|
280
|
-
'Ensure agent is reporting checkpoints via rigour_checkpoint MCP tool',
|
|
281
|
-
'Missing Checkpoint'
|
|
282
|
-
));
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Check 2: Quality threshold
|
|
286
|
-
const lastCheckpoint = session.checkpoints[session.checkpoints.length - 1];
|
|
287
|
-
if (lastCheckpoint.qualityScore < (this.config.quality_threshold ?? 80)) {
|
|
288
|
-
failures.push(this.createFailure(
|
|
289
|
-
`Quality score ${lastCheckpoint.qualityScore}% is below threshold ${this.config.quality_threshold}%`,
|
|
290
|
-
lastCheckpoint.filesChanged,
|
|
291
|
-
'Review recent changes and address quality issues before continuing',
|
|
292
|
-
'Quality Below Threshold'
|
|
293
|
-
));
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Check 3: Drift detection
|
|
297
|
-
if (this.config.drift_detection) {
|
|
298
|
-
const { hasDrift, trend } = detectDrift(session.checkpoints);
|
|
299
|
-
if (hasDrift && trend === 'degrading') {
|
|
300
|
-
failures.push(this.createFailure(
|
|
301
|
-
`Quality drift detected: scores are degrading over time`,
|
|
302
|
-
undefined,
|
|
303
|
-
'Agent performance is declining. Consider pausing and reviewing recent work.',
|
|
304
|
-
'Quality Drift Detected'
|
|
305
|
-
));
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return failures;
|
|
310
|
-
}
|
|
311
|
-
}
|
package/src/gates/content.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { Gate, GateContext } from './base.js';
|
|
2
|
-
import { Failure } from '../types/index.js';
|
|
3
|
-
import { FileScanner } from '../utils/scanner.js';
|
|
4
|
-
|
|
5
|
-
export interface ContentGateConfig {
|
|
6
|
-
forbidTodos: boolean;
|
|
7
|
-
forbidFixme: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export class ContentGate extends Gate {
|
|
11
|
-
constructor(private config: ContentGateConfig) {
|
|
12
|
-
super('content-check', 'Forbidden Content');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async run(context: GateContext): Promise<Failure[]> {
|
|
16
|
-
const patterns: RegExp[] = [];
|
|
17
|
-
if (this.config.forbidTodos) patterns.push(/TODO/i);
|
|
18
|
-
if (this.config.forbidFixme) patterns.push(/FIXME/i);
|
|
19
|
-
|
|
20
|
-
if (patterns.length === 0) return [];
|
|
21
|
-
|
|
22
|
-
const files = await FileScanner.findFiles({
|
|
23
|
-
cwd: context.cwd,
|
|
24
|
-
ignore: context.ignore,
|
|
25
|
-
patterns: context.patterns
|
|
26
|
-
});
|
|
27
|
-
const contents = await FileScanner.readFiles(context.cwd, files);
|
|
28
|
-
|
|
29
|
-
const failures: Failure[] = [];
|
|
30
|
-
for (const [file, content] of contents) {
|
|
31
|
-
const lines = content.split('\n');
|
|
32
|
-
lines.forEach((line, index) => {
|
|
33
|
-
for (const pattern of patterns) {
|
|
34
|
-
if (pattern.test(line)) {
|
|
35
|
-
failures.push(this.createFailure(
|
|
36
|
-
`Forbidden placeholder '${pattern.source}' found`,
|
|
37
|
-
[file],
|
|
38
|
-
'Remove forbidden comments. address the root cause or create a tracked issue.',
|
|
39
|
-
undefined,
|
|
40
|
-
index + 1,
|
|
41
|
-
index + 1,
|
|
42
|
-
'info'
|
|
43
|
-
));
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return failures;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Context Window Artifacts Gate
|
|
3
|
-
*
|
|
4
|
-
* Detects quality degradation patterns within a single file that emerge
|
|
5
|
-
* when AI loses context mid-generation. The telltale sign: clean,
|
|
6
|
-
* well-structured code at the top of a file that gradually degrades
|
|
7
|
-
* toward the bottom.
|
|
8
|
-
*
|
|
9
|
-
* Detection signals:
|
|
10
|
-
* 1. Comment density drops sharply (top half vs bottom half)
|
|
11
|
-
* 2. Function complexity increases toward end of file
|
|
12
|
-
* 3. Variable naming quality degrades (shorter names, more single-letter vars)
|
|
13
|
-
* 4. Error handling becomes sparser toward the bottom
|
|
14
|
-
* 5. Code style inconsistencies emerge (indentation, spacing)
|
|
15
|
-
*
|
|
16
|
-
* @since v2.16.0
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { Gate, GateContext } from './base.js';
|
|
20
|
-
import { Failure } from '../types/index.js';
|
|
21
|
-
import { FileScanner } from '../utils/scanner.js';
|
|
22
|
-
import { Logger } from '../utils/logger.js';
|
|
23
|
-
import fs from 'fs-extra';
|
|
24
|
-
import path from 'path';
|
|
25
|
-
|
|
26
|
-
interface FileQualityMetrics {
|
|
27
|
-
file: string;
|
|
28
|
-
totalLines: number;
|
|
29
|
-
topHalf: HalfMetrics;
|
|
30
|
-
bottomHalf: HalfMetrics;
|
|
31
|
-
degradationScore: number; // 0-1, higher = more degradation
|
|
32
|
-
signals: string[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface HalfMetrics {
|
|
36
|
-
commentDensity: number; // comments per code line
|
|
37
|
-
avgFunctionLength: number; // average lines per function
|
|
38
|
-
singleCharVarCount: number; // number of single-char variables
|
|
39
|
-
errorHandlingDensity: number; // try/catch per function
|
|
40
|
-
emptyBlockCount: number; // empty {} blocks
|
|
41
|
-
todoCount: number; // TODO/FIXME/HACK comments
|
|
42
|
-
avgIdentifierLength: number; // average variable/function name length
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface ContextWindowArtifactsConfig {
|
|
46
|
-
enabled?: boolean;
|
|
47
|
-
min_file_lines?: number; // Only analyze files with 100+ lines
|
|
48
|
-
degradation_threshold?: number; // 0-1, flag if degradation > this, default 0.4
|
|
49
|
-
signals_required?: number; // How many signals needed to flag, default 2
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export class ContextWindowArtifactsGate extends Gate {
|
|
53
|
-
private config: Required<ContextWindowArtifactsConfig>;
|
|
54
|
-
|
|
55
|
-
constructor(config: ContextWindowArtifactsConfig = {}) {
|
|
56
|
-
super('context-window-artifacts', 'Context Window Artifact Detection');
|
|
57
|
-
this.config = {
|
|
58
|
-
enabled: config.enabled ?? true,
|
|
59
|
-
min_file_lines: config.min_file_lines ?? 100,
|
|
60
|
-
degradation_threshold: config.degradation_threshold ?? 0.4,
|
|
61
|
-
signals_required: config.signals_required ?? 2,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async run(context: GateContext): Promise<Failure[]> {
|
|
66
|
-
if (!this.config.enabled) return [];
|
|
67
|
-
|
|
68
|
-
const failures: Failure[] = [];
|
|
69
|
-
|
|
70
|
-
const files = await FileScanner.findFiles({
|
|
71
|
-
cwd: context.cwd,
|
|
72
|
-
patterns: ['**/*.{ts,js,tsx,jsx,py}'],
|
|
73
|
-
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*', '**/*.min.*'],
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
Logger.info(`Context Window Artifacts: Scanning ${files.length} files`);
|
|
77
|
-
|
|
78
|
-
for (const file of files) {
|
|
79
|
-
try {
|
|
80
|
-
const content = await fs.readFile(path.join(context.cwd, file), 'utf-8');
|
|
81
|
-
const lines = content.split('\n');
|
|
82
|
-
|
|
83
|
-
if (lines.length < this.config.min_file_lines) continue;
|
|
84
|
-
|
|
85
|
-
const metrics = this.analyzeFile(content, file);
|
|
86
|
-
if (metrics && metrics.signals.length >= this.config.signals_required &&
|
|
87
|
-
metrics.degradationScore >= this.config.degradation_threshold) {
|
|
88
|
-
|
|
89
|
-
const signalList = metrics.signals.map(s => ` • ${s}`).join('\n');
|
|
90
|
-
const midpoint = Math.floor(metrics.totalLines / 2);
|
|
91
|
-
|
|
92
|
-
failures.push(this.createFailure(
|
|
93
|
-
`Context window artifact detected in ${file} (${metrics.totalLines} lines, degradation: ${(metrics.degradationScore * 100).toFixed(0)}%):\n${signalList}`,
|
|
94
|
-
[file],
|
|
95
|
-
`This file shows quality degradation from top to bottom, a pattern typical of AI context window exhaustion. Consider refactoring the bottom half or splitting the file. The quality drop begins around line ${midpoint}.`,
|
|
96
|
-
'Context Window Artifacts',
|
|
97
|
-
midpoint,
|
|
98
|
-
undefined,
|
|
99
|
-
'high'
|
|
100
|
-
));
|
|
101
|
-
}
|
|
102
|
-
} catch (e) { }
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return failures;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
private analyzeFile(content: string, file: string): FileQualityMetrics | null {
|
|
109
|
-
const lines = content.split('\n');
|
|
110
|
-
const midpoint = Math.floor(lines.length / 2);
|
|
111
|
-
|
|
112
|
-
const topContent = lines.slice(0, midpoint).join('\n');
|
|
113
|
-
const bottomContent = lines.slice(midpoint).join('\n');
|
|
114
|
-
|
|
115
|
-
const topMetrics = this.measureHalf(topContent);
|
|
116
|
-
const bottomMetrics = this.measureHalf(bottomContent);
|
|
117
|
-
|
|
118
|
-
const signals: string[] = [];
|
|
119
|
-
let degradationScore = 0;
|
|
120
|
-
|
|
121
|
-
// Signal 1: Comment density drops
|
|
122
|
-
if (topMetrics.commentDensity > 0) {
|
|
123
|
-
const commentRatio = bottomMetrics.commentDensity / topMetrics.commentDensity;
|
|
124
|
-
if (commentRatio < 0.5) {
|
|
125
|
-
signals.push(`Comment density drops ${((1 - commentRatio) * 100).toFixed(0)}% in bottom half`);
|
|
126
|
-
degradationScore += 0.25;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Signal 2: Function length increases
|
|
131
|
-
if (topMetrics.avgFunctionLength > 0 && bottomMetrics.avgFunctionLength > 0) {
|
|
132
|
-
const lengthRatio = bottomMetrics.avgFunctionLength / topMetrics.avgFunctionLength;
|
|
133
|
-
if (lengthRatio > 1.5) {
|
|
134
|
-
signals.push(`Average function length ${lengthRatio.toFixed(1)}x longer in bottom half`);
|
|
135
|
-
degradationScore += 0.2;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Signal 3: Variable naming quality degrades
|
|
140
|
-
if (bottomMetrics.singleCharVarCount > topMetrics.singleCharVarCount * 2 &&
|
|
141
|
-
bottomMetrics.singleCharVarCount >= 3) {
|
|
142
|
-
signals.push(`${bottomMetrics.singleCharVarCount} single-char variables in bottom half vs ${topMetrics.singleCharVarCount} in top`);
|
|
143
|
-
degradationScore += 0.2;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Signal 3b: Average identifier length shrinks
|
|
147
|
-
if (topMetrics.avgIdentifierLength > 0 && bottomMetrics.avgIdentifierLength > 0) {
|
|
148
|
-
const nameRatio = bottomMetrics.avgIdentifierLength / topMetrics.avgIdentifierLength;
|
|
149
|
-
if (nameRatio < 0.7) {
|
|
150
|
-
signals.push(`Identifier names ${((1 - nameRatio) * 100).toFixed(0)}% shorter in bottom half`);
|
|
151
|
-
degradationScore += 0.15;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Signal 4: Error handling becomes sparser
|
|
156
|
-
if (topMetrics.errorHandlingDensity > 0) {
|
|
157
|
-
const errorRatio = bottomMetrics.errorHandlingDensity / topMetrics.errorHandlingDensity;
|
|
158
|
-
if (errorRatio < 0.3) {
|
|
159
|
-
signals.push(`Error handling ${((1 - errorRatio) * 100).toFixed(0)}% less frequent in bottom half`);
|
|
160
|
-
degradationScore += 0.2;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Signal 5: Empty blocks increase
|
|
165
|
-
if (bottomMetrics.emptyBlockCount > topMetrics.emptyBlockCount + 2) {
|
|
166
|
-
signals.push(`${bottomMetrics.emptyBlockCount} empty blocks in bottom half vs ${topMetrics.emptyBlockCount} in top`);
|
|
167
|
-
degradationScore += 0.15;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Signal 6: TODO/FIXME/HACK density increases at bottom
|
|
171
|
-
if (bottomMetrics.todoCount > topMetrics.todoCount + 1) {
|
|
172
|
-
signals.push(`${bottomMetrics.todoCount} TODO/FIXME/HACK in bottom half vs ${topMetrics.todoCount} in top`);
|
|
173
|
-
degradationScore += 0.1;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Cap at 1.0
|
|
177
|
-
degradationScore = Math.min(1.0, degradationScore);
|
|
178
|
-
|
|
179
|
-
return {
|
|
180
|
-
file,
|
|
181
|
-
totalLines: lines.length,
|
|
182
|
-
topHalf: topMetrics,
|
|
183
|
-
bottomHalf: bottomMetrics,
|
|
184
|
-
degradationScore,
|
|
185
|
-
signals,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
private measureHalf(content: string): HalfMetrics {
|
|
190
|
-
const lines = content.split('\n');
|
|
191
|
-
const codeLines = lines.filter(l => l.trim() && !l.trim().startsWith('//') && !l.trim().startsWith('#') && !l.trim().startsWith('*'));
|
|
192
|
-
const commentLines = lines.filter(l => {
|
|
193
|
-
const trimmed = l.trim();
|
|
194
|
-
return trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*') || trimmed.startsWith('/*');
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
// Comment density
|
|
198
|
-
const commentDensity = codeLines.length > 0 ? commentLines.length / codeLines.length : 0;
|
|
199
|
-
|
|
200
|
-
// Function lengths
|
|
201
|
-
const funcLengths = this.measureFunctionLengths(content);
|
|
202
|
-
const avgFunctionLength = funcLengths.length > 0
|
|
203
|
-
? funcLengths.reduce((a, b) => a + b, 0) / funcLengths.length
|
|
204
|
-
: 0;
|
|
205
|
-
|
|
206
|
-
// Single-char variables (excluding common loop vars i, j, k in for loops)
|
|
207
|
-
const singleCharMatches = content.match(/\b(?:const|let|var)\s+([a-z])\b/g) || [];
|
|
208
|
-
const singleCharVarCount = singleCharMatches.length;
|
|
209
|
-
|
|
210
|
-
// Error handling density
|
|
211
|
-
const tryCount = (content.match(/\btry\s*\{/g) || []).length;
|
|
212
|
-
const funcCount = Math.max(1, funcLengths.length);
|
|
213
|
-
const errorHandlingDensity = tryCount / funcCount;
|
|
214
|
-
|
|
215
|
-
// Empty blocks
|
|
216
|
-
const emptyBlockCount = (content.match(/\{\s*\}/g) || []).length;
|
|
217
|
-
|
|
218
|
-
// TODO/FIXME/HACK count
|
|
219
|
-
const todoCount = (content.match(/\b(TODO|FIXME|HACK|XXX)\b/gi) || []).length;
|
|
220
|
-
|
|
221
|
-
// Average identifier length
|
|
222
|
-
const identifiers = content.match(/\b(?:const|let|var|function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g) || [];
|
|
223
|
-
const identNames = identifiers.map(m => {
|
|
224
|
-
const parts = m.split(/\s+/);
|
|
225
|
-
return parts[parts.length - 1];
|
|
226
|
-
});
|
|
227
|
-
const avgIdentifierLength = identNames.length > 0
|
|
228
|
-
? identNames.reduce((sum, n) => sum + n.length, 0) / identNames.length
|
|
229
|
-
: 0;
|
|
230
|
-
|
|
231
|
-
return {
|
|
232
|
-
commentDensity,
|
|
233
|
-
avgFunctionLength,
|
|
234
|
-
singleCharVarCount,
|
|
235
|
-
errorHandlingDensity,
|
|
236
|
-
emptyBlockCount,
|
|
237
|
-
todoCount,
|
|
238
|
-
avgIdentifierLength,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
private measureFunctionLengths(content: string): number[] {
|
|
243
|
-
const lines = content.split('\n');
|
|
244
|
-
const lengths: number[] = [];
|
|
245
|
-
|
|
246
|
-
const funcStarts = [
|
|
247
|
-
/^(?:export\s+)?(?:async\s+)?function\s+\w+/,
|
|
248
|
-
/^(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?(?:\([^)]*\)|\w+)\s*=>/,
|
|
249
|
-
/^\s+(?:async\s+)?\w+\s*\([^)]*\)\s*\{/,
|
|
250
|
-
];
|
|
251
|
-
|
|
252
|
-
for (let i = 0; i < lines.length; i++) {
|
|
253
|
-
for (const pattern of funcStarts) {
|
|
254
|
-
if (pattern.test(lines[i])) {
|
|
255
|
-
// Count function body length
|
|
256
|
-
let braceDepth = 0;
|
|
257
|
-
let started = false;
|
|
258
|
-
let bodyLines = 0;
|
|
259
|
-
|
|
260
|
-
for (let j = i; j < lines.length; j++) {
|
|
261
|
-
for (const ch of lines[j]) {
|
|
262
|
-
if (ch === '{') { braceDepth++; started = true; }
|
|
263
|
-
if (ch === '}') braceDepth--;
|
|
264
|
-
}
|
|
265
|
-
if (started) bodyLines++;
|
|
266
|
-
if (started && braceDepth === 0) break;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (bodyLines > 0) lengths.push(bodyLines);
|
|
270
|
-
break;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return lengths;
|
|
276
|
-
}
|
|
277
|
-
}
|