@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
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Inconsistent Error Handling Gate
|
|
3
|
-
*
|
|
4
|
-
* Detects when the same error type is handled differently across the codebase.
|
|
5
|
-
* This is an AI-specific failure mode — each agent session writes error handling
|
|
6
|
-
* from scratch, leading to 4 different patterns for the same error type.
|
|
7
|
-
*
|
|
8
|
-
* Detection strategy:
|
|
9
|
-
* 1. Extract all try-catch blocks and error handling patterns
|
|
10
|
-
* 2. Cluster by caught error type (e.g. "Error", "TypeError", custom errors)
|
|
11
|
-
* 3. Compare handling strategies within each cluster
|
|
12
|
-
* 4. Flag types with >2 distinct handling patterns across files
|
|
13
|
-
*
|
|
14
|
-
* Examples of inconsistency:
|
|
15
|
-
* - File A: catch(e) { console.log(e) }
|
|
16
|
-
* - File B: catch(e) { throw new AppError(e) }
|
|
17
|
-
* - File C: catch(e) { return null }
|
|
18
|
-
* - File D: catch(e) { [empty] }
|
|
19
|
-
*
|
|
20
|
-
* @since v2.16.0
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { Gate, GateContext } from './base.js';
|
|
24
|
-
import { Failure } from '../types/index.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
|
-
|
|
30
|
-
interface ErrorHandler {
|
|
31
|
-
file: string;
|
|
32
|
-
line: number;
|
|
33
|
-
errorType: string; // What's caught: 'Error', 'TypeError', 'any', etc.
|
|
34
|
-
strategy: string; // Classified handling strategy
|
|
35
|
-
rawPattern: string; // First line of catch body for display
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface InconsistentErrorHandlingConfig {
|
|
39
|
-
enabled?: boolean;
|
|
40
|
-
max_strategies_per_type?: number; // Flag if more than N strategies, default 2
|
|
41
|
-
min_occurrences?: number; // Minimum catch blocks to analyze, default 3
|
|
42
|
-
ignore_empty_catches?: boolean; // Whether to count empty catches as a strategy
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export class InconsistentErrorHandlingGate extends Gate {
|
|
46
|
-
private config: Required<InconsistentErrorHandlingConfig>;
|
|
47
|
-
|
|
48
|
-
constructor(config: InconsistentErrorHandlingConfig = {}) {
|
|
49
|
-
super('inconsistent-error-handling', 'Inconsistent Error Handling Detection');
|
|
50
|
-
this.config = {
|
|
51
|
-
enabled: config.enabled ?? true,
|
|
52
|
-
max_strategies_per_type: config.max_strategies_per_type ?? 2,
|
|
53
|
-
min_occurrences: config.min_occurrences ?? 3,
|
|
54
|
-
ignore_empty_catches: config.ignore_empty_catches ?? false,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async run(context: GateContext): Promise<Failure[]> {
|
|
59
|
-
if (!this.config.enabled) return [];
|
|
60
|
-
|
|
61
|
-
const failures: Failure[] = [];
|
|
62
|
-
const handlers: ErrorHandler[] = [];
|
|
63
|
-
|
|
64
|
-
const files = await FileScanner.findFiles({
|
|
65
|
-
cwd: context.cwd,
|
|
66
|
-
patterns: ['**/*.{ts,js,tsx,jsx}'],
|
|
67
|
-
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*'],
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
Logger.info(`Inconsistent Error Handling: Scanning ${files.length} files`);
|
|
71
|
-
|
|
72
|
-
for (const file of files) {
|
|
73
|
-
try {
|
|
74
|
-
const content = await fs.readFile(path.join(context.cwd, file), 'utf-8');
|
|
75
|
-
this.extractErrorHandlers(content, file, handlers);
|
|
76
|
-
} catch (e) { }
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Group by error type
|
|
80
|
-
const byType = new Map<string, ErrorHandler[]>();
|
|
81
|
-
for (const handler of handlers) {
|
|
82
|
-
const existing = byType.get(handler.errorType) || [];
|
|
83
|
-
existing.push(handler);
|
|
84
|
-
byType.set(handler.errorType, existing);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Analyze each error type for inconsistent handling
|
|
88
|
-
for (const [errorType, typeHandlers] of byType) {
|
|
89
|
-
if (typeHandlers.length < this.config.min_occurrences) continue;
|
|
90
|
-
|
|
91
|
-
// Count unique strategies
|
|
92
|
-
const strategies = new Map<string, ErrorHandler[]>();
|
|
93
|
-
for (const handler of typeHandlers) {
|
|
94
|
-
const existing = strategies.get(handler.strategy) || [];
|
|
95
|
-
existing.push(handler);
|
|
96
|
-
strategies.set(handler.strategy, existing);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Filter out empty catches if configured
|
|
100
|
-
const activeStrategies = this.config.ignore_empty_catches
|
|
101
|
-
? new Map([...strategies].filter(([k]) => k !== 'swallow'))
|
|
102
|
-
: strategies;
|
|
103
|
-
|
|
104
|
-
if (activeStrategies.size > this.config.max_strategies_per_type) {
|
|
105
|
-
// Only flag if handlers span multiple files
|
|
106
|
-
const uniqueFiles = new Set(typeHandlers.map(h => h.file));
|
|
107
|
-
if (uniqueFiles.size < 2) continue;
|
|
108
|
-
|
|
109
|
-
const strategyBreakdown = [...activeStrategies.entries()]
|
|
110
|
-
.map(([strategy, handlers]) => {
|
|
111
|
-
const files = [...new Set(handlers.map(h => h.file))].slice(0, 3);
|
|
112
|
-
return ` • ${strategy} (${handlers.length}x): ${files.join(', ')}`;
|
|
113
|
-
})
|
|
114
|
-
.join('\n');
|
|
115
|
-
|
|
116
|
-
failures.push(this.createFailure(
|
|
117
|
-
`Inconsistent error handling for '${errorType}': ${activeStrategies.size} different strategies found across ${uniqueFiles.size} files:\n${strategyBreakdown}`,
|
|
118
|
-
[...uniqueFiles].slice(0, 5),
|
|
119
|
-
`Standardize error handling for '${errorType}'. Create a shared error handler or establish a project convention. AI agents often write error handling from scratch each session, leading to divergent patterns.`,
|
|
120
|
-
'Inconsistent Error Handling',
|
|
121
|
-
typeHandlers[0].line,
|
|
122
|
-
undefined,
|
|
123
|
-
'high'
|
|
124
|
-
));
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return failures;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
private extractErrorHandlers(content: string, file: string, handlers: ErrorHandler[]): void {
|
|
132
|
-
const lines = content.split('\n');
|
|
133
|
-
|
|
134
|
-
for (let i = 0; i < lines.length; i++) {
|
|
135
|
-
// Match catch clauses: catch (e), catch (error: Error), catch (e: TypeError)
|
|
136
|
-
const catchMatch = lines[i].match(/\bcatch\s*\(\s*(\w+)(?:\s*:\s*(\w+))?\s*\)/);
|
|
137
|
-
if (!catchMatch) continue;
|
|
138
|
-
|
|
139
|
-
const varName = catchMatch[1];
|
|
140
|
-
const explicitType = catchMatch[2] || 'any';
|
|
141
|
-
|
|
142
|
-
// Extract catch body (up to next closing brace at same level)
|
|
143
|
-
const body = this.extractCatchBody(lines, i);
|
|
144
|
-
if (!body) continue;
|
|
145
|
-
|
|
146
|
-
const strategy = this.classifyStrategy(body, varName);
|
|
147
|
-
const rawPattern = body.split('\n')[0]?.trim() || '';
|
|
148
|
-
|
|
149
|
-
handlers.push({
|
|
150
|
-
file,
|
|
151
|
-
line: i + 1,
|
|
152
|
-
errorType: explicitType,
|
|
153
|
-
strategy,
|
|
154
|
-
rawPattern,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Also detect .catch() promise patterns
|
|
159
|
-
for (let i = 0; i < lines.length; i++) {
|
|
160
|
-
const catchMatch = lines[i].match(/\.catch\s*\(\s*(?:async\s+)?(?:\(\s*)?(\w+)/);
|
|
161
|
-
if (!catchMatch) continue;
|
|
162
|
-
|
|
163
|
-
const varName = catchMatch[1];
|
|
164
|
-
|
|
165
|
-
// Extract the callback body
|
|
166
|
-
const body = this.extractCatchCallbackBody(lines, i);
|
|
167
|
-
if (!body) continue;
|
|
168
|
-
|
|
169
|
-
const strategy = this.classifyStrategy(body, varName);
|
|
170
|
-
|
|
171
|
-
handlers.push({
|
|
172
|
-
file,
|
|
173
|
-
line: i + 1,
|
|
174
|
-
errorType: 'Promise',
|
|
175
|
-
strategy,
|
|
176
|
-
rawPattern: body.split('\n')[0]?.trim() || '',
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
private classifyStrategy(body: string, varName: string): string {
|
|
182
|
-
const trimmed = body.trim();
|
|
183
|
-
|
|
184
|
-
// Empty catch (swallow)
|
|
185
|
-
if (!trimmed || trimmed === '{}' || trimmed === '') {
|
|
186
|
-
return 'swallow';
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Re-throw
|
|
190
|
-
if (/\bthrow\b/.test(trimmed)) {
|
|
191
|
-
if (/\bthrow\s+new\b/.test(trimmed)) return 'wrap-and-throw';
|
|
192
|
-
if (new RegExp(`\\bthrow\\s+${varName}\\b`).test(trimmed)) return 'rethrow';
|
|
193
|
-
return 'throw-new';
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Return patterns
|
|
197
|
-
if (/\breturn\s+null\b/.test(trimmed)) return 'return-null';
|
|
198
|
-
if (/\breturn\s+undefined\b/.test(trimmed) || /\breturn\s*;/.test(trimmed)) return 'return-undefined';
|
|
199
|
-
if (/\breturn\s+\[\s*\]/.test(trimmed)) return 'return-empty-array';
|
|
200
|
-
if (/\breturn\s+\{\s*\}/.test(trimmed)) return 'return-empty-object';
|
|
201
|
-
if (/\breturn\s+false\b/.test(trimmed)) return 'return-false';
|
|
202
|
-
if (/\breturn\s+/.test(trimmed)) return 'return-value';
|
|
203
|
-
|
|
204
|
-
// Logging patterns
|
|
205
|
-
if (/console\.(error|warn)\b/.test(trimmed)) return 'log-error';
|
|
206
|
-
if (/console\.log\b/.test(trimmed)) return 'log-info';
|
|
207
|
-
if (/\blogger\b/i.test(trimmed) || /\blog\b/i.test(trimmed)) return 'log-custom';
|
|
208
|
-
|
|
209
|
-
// Process.exit
|
|
210
|
-
if (/process\.exit\b/.test(trimmed)) return 'exit';
|
|
211
|
-
|
|
212
|
-
// Response patterns (Express/HTTP)
|
|
213
|
-
if (/\bres\s*\.\s*status\b/.test(trimmed) || /\bres\s*\.\s*json\b/.test(trimmed)) return 'http-response';
|
|
214
|
-
|
|
215
|
-
// Notification patterns
|
|
216
|
-
if (/\bnotif|toast|alert|modal\b/i.test(trimmed)) return 'user-notification';
|
|
217
|
-
|
|
218
|
-
return 'other';
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
private extractCatchBody(lines: string[], catchLine: number): string | null {
|
|
222
|
-
let braceDepth = 0;
|
|
223
|
-
let started = false;
|
|
224
|
-
const body: string[] = [];
|
|
225
|
-
|
|
226
|
-
for (let i = catchLine; i < lines.length; i++) {
|
|
227
|
-
for (const ch of lines[i]) {
|
|
228
|
-
if (ch === '{') { braceDepth++; started = true; }
|
|
229
|
-
if (ch === '}') braceDepth--;
|
|
230
|
-
}
|
|
231
|
-
if (started && i > catchLine) body.push(lines[i]);
|
|
232
|
-
if (started && braceDepth === 0) break;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return body.length > 0 ? body.join('\n') : null;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private extractCatchCallbackBody(lines: string[], startLine: number): string | null {
|
|
239
|
-
let depth = 0;
|
|
240
|
-
let started = false;
|
|
241
|
-
const body: string[] = [];
|
|
242
|
-
|
|
243
|
-
for (let i = startLine; i < Math.min(startLine + 20, lines.length); i++) {
|
|
244
|
-
for (const ch of lines[i]) {
|
|
245
|
-
if (ch === '{' || ch === '(') { depth++; started = true; }
|
|
246
|
-
if (ch === '}' || ch === ')') depth--;
|
|
247
|
-
}
|
|
248
|
-
if (started && i > startLine) body.push(lines[i]);
|
|
249
|
-
if (started && depth <= 0) break;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return body.length > 0 ? body.join('\n') : null;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { Gate, GateContext } from './base.js';
|
|
2
|
-
import { Failure, Gates } from '../types/index.js';
|
|
3
|
-
import fs from 'fs-extra';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
|
|
6
|
-
interface FailureRecord {
|
|
7
|
-
category: string;
|
|
8
|
-
count: number;
|
|
9
|
-
lastError: string;
|
|
10
|
-
lastTimestamp: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface RigourState {
|
|
14
|
-
failureHistory: Record<string, FailureRecord>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const ERROR_PATTERNS: [RegExp, string][] = [
|
|
18
|
-
[/ERR_REQUIRE_ESM|Cannot find module|MODULE_NOT_FOUND/i, 'module_resolution'],
|
|
19
|
-
[/FUNCTION_INVOCATION_FAILED|Build Failed|deploy.*fail/i, 'deployment'],
|
|
20
|
-
[/TypeError|SyntaxError|ReferenceError|compilation.*error/i, 'runtime_error'],
|
|
21
|
-
[/Connection refused|ECONNREFUSED|timeout|ETIMEDOUT/i, 'network'],
|
|
22
|
-
[/Permission denied|EACCES|EPERM/i, 'permissions'],
|
|
23
|
-
[/ENOMEM|heap out of memory|OOM/i, 'resources'],
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Retry Loop Breaker Gate
|
|
28
|
-
*
|
|
29
|
-
* Detects when an agent is stuck in a retry loop and forces them to consult
|
|
30
|
-
* official documentation before continuing. This gate is universal and works
|
|
31
|
-
* with any type of failure, not just specific tools or languages.
|
|
32
|
-
*/
|
|
33
|
-
export class RetryLoopBreakerGate extends Gate {
|
|
34
|
-
constructor(private options: Gates['retry_loop_breaker']) {
|
|
35
|
-
super('retry_loop_breaker', 'Retry Loop Breaker');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async run(context: GateContext): Promise<Failure[]> {
|
|
39
|
-
const state = await this.loadState(context.cwd);
|
|
40
|
-
const failures: Failure[] = [];
|
|
41
|
-
|
|
42
|
-
for (const [category, record] of Object.entries(state.failureHistory)) {
|
|
43
|
-
if (record.count >= (this.options?.max_retries ?? 3)) {
|
|
44
|
-
const docUrl = this.options?.doc_sources?.[category] || this.getDefaultDocUrl(category);
|
|
45
|
-
failures.push(this.createFailure(
|
|
46
|
-
`Operation '${category}' has failed ${record.count} times consecutively. Last error: ${record.lastError}`,
|
|
47
|
-
undefined,
|
|
48
|
-
`STOP RETRYING. You are in a loop. Consult the official documentation: ${docUrl}. Extract the canonical solution pattern and apply it.`,
|
|
49
|
-
`Retry Loop Detected: ${category}`
|
|
50
|
-
));
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return failures;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Classify an error message into a category based on patterns.
|
|
59
|
-
*/
|
|
60
|
-
static classifyError(errorMessage: string): string {
|
|
61
|
-
for (const [pattern, category] of ERROR_PATTERNS) {
|
|
62
|
-
if (pattern.test(errorMessage)) {
|
|
63
|
-
return category;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return 'general';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Record a failure for retry loop detection.
|
|
71
|
-
* Call this when an operation fails.
|
|
72
|
-
*/
|
|
73
|
-
static async recordFailure(cwd: string, errorMessage: string, category?: string): Promise<void> {
|
|
74
|
-
const resolvedCategory = category || this.classifyError(errorMessage);
|
|
75
|
-
const state = await this.loadStateStatic(cwd);
|
|
76
|
-
|
|
77
|
-
const existing = state.failureHistory[resolvedCategory] || {
|
|
78
|
-
category: resolvedCategory,
|
|
79
|
-
count: 0,
|
|
80
|
-
lastError: '',
|
|
81
|
-
lastTimestamp: ''
|
|
82
|
-
};
|
|
83
|
-
existing.count += 1;
|
|
84
|
-
existing.lastError = errorMessage.slice(0, 500); // Truncate for storage
|
|
85
|
-
existing.lastTimestamp = new Date().toISOString();
|
|
86
|
-
state.failureHistory[resolvedCategory] = existing;
|
|
87
|
-
|
|
88
|
-
await this.saveStateStatic(cwd, state);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Clear failure history for a specific category after successful resolution.
|
|
93
|
-
*/
|
|
94
|
-
static async clearFailure(cwd: string, category: string): Promise<void> {
|
|
95
|
-
const state = await this.loadStateStatic(cwd);
|
|
96
|
-
delete state.failureHistory[category];
|
|
97
|
-
await this.saveStateStatic(cwd, state);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Clear all failure history.
|
|
102
|
-
*/
|
|
103
|
-
static async clearAllFailures(cwd: string): Promise<void> {
|
|
104
|
-
const state = await this.loadStateStatic(cwd);
|
|
105
|
-
state.failureHistory = {};
|
|
106
|
-
await this.saveStateStatic(cwd, state);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Get the current failure state for inspection.
|
|
111
|
-
*/
|
|
112
|
-
static async getState(cwd: string): Promise<RigourState> {
|
|
113
|
-
return this.loadStateStatic(cwd);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
private getDefaultDocUrl(category: string): string {
|
|
117
|
-
const defaults: Record<string, string> = {
|
|
118
|
-
module_resolution: 'https://nodejs.org/api/esm.html',
|
|
119
|
-
deployment: 'Check the deployment platform\'s official documentation',
|
|
120
|
-
runtime_error: 'Check the language\'s official documentation',
|
|
121
|
-
network: 'Check network configuration and firewall rules',
|
|
122
|
-
permissions: 'Check file/directory permissions and ownership',
|
|
123
|
-
resources: 'Check system resource limits and memory allocation',
|
|
124
|
-
general: 'Consult the relevant official documentation',
|
|
125
|
-
};
|
|
126
|
-
return defaults[category] || defaults.general;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
private async loadState(cwd: string): Promise<RigourState> {
|
|
130
|
-
return RetryLoopBreakerGate.loadStateStatic(cwd);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
private static async loadStateStatic(cwd: string): Promise<RigourState> {
|
|
134
|
-
const statePath = path.join(cwd, '.rigour', 'state.json');
|
|
135
|
-
if (await fs.pathExists(statePath)) {
|
|
136
|
-
try {
|
|
137
|
-
const data = await fs.readJson(statePath);
|
|
138
|
-
return { failureHistory: data.failureHistory || {}, ...data };
|
|
139
|
-
} catch {
|
|
140
|
-
return { failureHistory: {} };
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return { failureHistory: {} };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
private static async saveStateStatic(cwd: string, state: RigourState): Promise<void> {
|
|
147
|
-
const statePath = path.join(cwd, '.rigour', 'state.json');
|
|
148
|
-
await fs.ensureDir(path.dirname(statePath));
|
|
149
|
-
await fs.writeJson(statePath, state, { spaces: 2 });
|
|
150
|
-
}
|
|
151
|
-
}
|
package/src/gates/runner.ts
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import { Gate } from './base.js';
|
|
2
|
-
import { Failure, Config, Report, Status, Severity, SEVERITY_WEIGHTS } from '../types/index.js';
|
|
3
|
-
import { FileGate } from './file.js';
|
|
4
|
-
import { ContentGate } from './content.js';
|
|
5
|
-
import { StructureGate } from './structure.js';
|
|
6
|
-
import { ASTGate } from './ast.js';
|
|
7
|
-
import { FileGuardGate } from './safety.js';
|
|
8
|
-
import { DependencyGate } from './dependency.js';
|
|
9
|
-
import { CoverageGate } from './coverage.js';
|
|
10
|
-
import { ContextGate } from './context.js';
|
|
11
|
-
import { ContextEngine } from '../services/context-engine.js';
|
|
12
|
-
import { EnvironmentGate } from './environment.js';
|
|
13
|
-
import { RetryLoopBreakerGate } from './retry-loop-breaker.js';
|
|
14
|
-
import { AgentTeamGate } from './agent-team.js';
|
|
15
|
-
import { CheckpointGate } from './checkpoint.js';
|
|
16
|
-
import { SecurityPatternsGate } from './security-patterns.js';
|
|
17
|
-
import { DuplicationDriftGate } from './duplication-drift.js';
|
|
18
|
-
import { HallucinatedImportsGate } from './hallucinated-imports.js';
|
|
19
|
-
import { InconsistentErrorHandlingGate } from './inconsistent-error-handling.js';
|
|
20
|
-
import { ContextWindowArtifactsGate } from './context-window-artifacts.js';
|
|
21
|
-
import { execa } from 'execa';
|
|
22
|
-
import { Logger } from '../utils/logger.js';
|
|
23
|
-
|
|
24
|
-
export class GateRunner {
|
|
25
|
-
private gates: Gate[] = [];
|
|
26
|
-
|
|
27
|
-
constructor(private config: Config) {
|
|
28
|
-
this.initializeGates();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
private initializeGates() {
|
|
32
|
-
// Retry Loop Breaker Gate - HIGHEST PRIORITY (runs first)
|
|
33
|
-
if (this.config.gates.retry_loop_breaker?.enabled !== false) {
|
|
34
|
-
this.gates.push(new RetryLoopBreakerGate(this.config.gates.retry_loop_breaker));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (this.config.gates.max_file_lines) {
|
|
38
|
-
this.gates.push(new FileGate({ maxLines: this.config.gates.max_file_lines }));
|
|
39
|
-
}
|
|
40
|
-
this.gates.push(
|
|
41
|
-
new ContentGate({
|
|
42
|
-
forbidTodos: !!this.config.gates.forbid_todos,
|
|
43
|
-
forbidFixme: !!this.config.gates.forbid_fixme,
|
|
44
|
-
})
|
|
45
|
-
);
|
|
46
|
-
if (this.config.gates.required_files) {
|
|
47
|
-
this.gates.push(new StructureGate({ requiredFiles: this.config.gates.required_files }));
|
|
48
|
-
}
|
|
49
|
-
this.gates.push(new ASTGate(this.config.gates));
|
|
50
|
-
this.gates.push(new DependencyGate(this.config));
|
|
51
|
-
this.gates.push(new FileGuardGate(this.config.gates));
|
|
52
|
-
this.gates.push(new CoverageGate(this.config.gates));
|
|
53
|
-
|
|
54
|
-
if (this.config.gates.context?.enabled) {
|
|
55
|
-
this.gates.push(new ContextGate(this.config.gates));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Agent Team Governance Gate (for Opus 4.6 / GPT-5.3 multi-agent workflows)
|
|
59
|
-
if (this.config.gates.agent_team?.enabled) {
|
|
60
|
-
this.gates.push(new AgentTeamGate(this.config.gates.agent_team));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Checkpoint Supervision Gate (for long-running GPT-5.3 coworking mode)
|
|
64
|
-
if (this.config.gates.checkpoint?.enabled) {
|
|
65
|
-
this.gates.push(new CheckpointGate(this.config.gates.checkpoint));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Security Patterns Gate (code-level vulnerability detection) — enabled by default since v2.15
|
|
69
|
-
if (this.config.gates.security?.enabled !== false) {
|
|
70
|
-
this.gates.push(new SecurityPatternsGate(this.config.gates.security));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// v2.16+ AI-Native Drift Detection Gates (enabled by default)
|
|
74
|
-
if (this.config.gates.duplication_drift?.enabled !== false) {
|
|
75
|
-
this.gates.push(new DuplicationDriftGate(this.config.gates.duplication_drift));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (this.config.gates.hallucinated_imports?.enabled !== false) {
|
|
79
|
-
this.gates.push(new HallucinatedImportsGate(this.config.gates.hallucinated_imports));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (this.config.gates.inconsistent_error_handling?.enabled !== false) {
|
|
83
|
-
this.gates.push(new InconsistentErrorHandlingGate(this.config.gates.inconsistent_error_handling));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (this.config.gates.context_window_artifacts?.enabled !== false) {
|
|
87
|
-
this.gates.push(new ContextWindowArtifactsGate(this.config.gates.context_window_artifacts));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Environment Alignment Gate (Should be prioritized)
|
|
91
|
-
if (this.config.gates.environment?.enabled) {
|
|
92
|
-
this.gates.unshift(new EnvironmentGate(this.config.gates));
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Allows adding custom gates dynamically (SOLID - Open/Closed Principle)
|
|
98
|
-
*/
|
|
99
|
-
addGate(gate: Gate) {
|
|
100
|
-
this.gates.push(gate);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async run(cwd: string, patterns?: string[]): Promise<Report> {
|
|
104
|
-
const start = Date.now();
|
|
105
|
-
const failures: Failure[] = [];
|
|
106
|
-
const summary: Record<string, Status> = {};
|
|
107
|
-
|
|
108
|
-
const ignore = this.config.ignore;
|
|
109
|
-
|
|
110
|
-
// 0. Run Context Discovery
|
|
111
|
-
let record;
|
|
112
|
-
if (this.config.gates.context?.enabled) {
|
|
113
|
-
const engine = new ContextEngine(this.config);
|
|
114
|
-
record = await engine.discover(cwd);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// 1. Run internal gates
|
|
118
|
-
for (const gate of this.gates) {
|
|
119
|
-
try {
|
|
120
|
-
const gateFailures = await gate.run({ cwd, record, ignore, patterns });
|
|
121
|
-
if (gateFailures.length > 0) {
|
|
122
|
-
failures.push(...gateFailures);
|
|
123
|
-
summary[gate.id] = 'FAIL';
|
|
124
|
-
} else {
|
|
125
|
-
summary[gate.id] = 'PASS';
|
|
126
|
-
}
|
|
127
|
-
} catch (error: any) {
|
|
128
|
-
Logger.error(`Gate ${gate.id} failed with error: ${error.message}`);
|
|
129
|
-
summary[gate.id] = 'ERROR';
|
|
130
|
-
failures.push({
|
|
131
|
-
id: gate.id,
|
|
132
|
-
title: `Gate Error: ${gate.title}`,
|
|
133
|
-
details: error.message,
|
|
134
|
-
hint: 'There was an internal error running this gate. Check the logs.',
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// 2. Run command gates (lint, test, etc.)
|
|
140
|
-
const commands = this.config.commands;
|
|
141
|
-
if (commands) {
|
|
142
|
-
for (const [key, cmd] of Object.entries(commands)) {
|
|
143
|
-
if (!cmd) {
|
|
144
|
-
summary[key] = 'SKIP';
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
Logger.info(`Running command gate: ${key} (${cmd})`);
|
|
150
|
-
await execa(cmd, { shell: true, cwd });
|
|
151
|
-
summary[key] = 'PASS';
|
|
152
|
-
} catch (error: any) {
|
|
153
|
-
summary[key] = 'FAIL';
|
|
154
|
-
failures.push({
|
|
155
|
-
id: key,
|
|
156
|
-
title: `${key.toUpperCase()} Check Failed`,
|
|
157
|
-
details: error.stderr || error.stdout || error.message,
|
|
158
|
-
hint: `Fix the issues reported by \`${cmd}\`. Use rigorous standards (SOLID, DRY) in your resolution.`,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const status: Status = failures.length > 0 ? 'FAIL' : 'PASS';
|
|
165
|
-
|
|
166
|
-
// Severity-weighted scoring: each failure deducts based on its severity
|
|
167
|
-
// critical=20, high=10, medium=5, low=2, info=0
|
|
168
|
-
const severityBreakdown: Record<string, number> = {};
|
|
169
|
-
let totalDeduction = 0;
|
|
170
|
-
for (const f of failures) {
|
|
171
|
-
const sev = (f.severity || 'medium') as Severity;
|
|
172
|
-
severityBreakdown[sev] = (severityBreakdown[sev] || 0) + 1;
|
|
173
|
-
totalDeduction += SEVERITY_WEIGHTS[sev] ?? 5;
|
|
174
|
-
}
|
|
175
|
-
const score = Math.max(0, 100 - totalDeduction);
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
status,
|
|
179
|
-
summary,
|
|
180
|
-
failures,
|
|
181
|
-
stats: {
|
|
182
|
-
duration_ms: Date.now() - start,
|
|
183
|
-
score,
|
|
184
|
-
severity_breakdown: severityBreakdown,
|
|
185
|
-
},
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
}
|
package/src/gates/safety.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { Gate, GateContext } from './base.js';
|
|
2
|
-
import { Failure, Gates } from '../types/index.js';
|
|
3
|
-
import { execa } from 'execa';
|
|
4
|
-
|
|
5
|
-
export class FileGuardGate extends Gate {
|
|
6
|
-
constructor(private config: Gates) {
|
|
7
|
-
super('file-guard', 'File Guard — Protected Paths');
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
async run(context: GateContext): Promise<Failure[]> {
|
|
11
|
-
const failures: Failure[] = [];
|
|
12
|
-
const safety = this.config.safety || {};
|
|
13
|
-
const protectedPaths = safety.protected_paths || [];
|
|
14
|
-
|
|
15
|
-
if (protectedPaths.length === 0) return [];
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
// Check for modified files in protected paths using git
|
|
19
|
-
// File Guard - if an agent touched protected files, we fail.
|
|
20
|
-
const { stdout } = await execa('git', ['status', '--porcelain'], { cwd: context.cwd });
|
|
21
|
-
const modifiedFiles = stdout.split('\n')
|
|
22
|
-
.filter(line => {
|
|
23
|
-
const status = line.slice(0, 2);
|
|
24
|
-
// M: Modified, A: Added (staged), D: Deleted, R: Renamed
|
|
25
|
-
// We ignore ?? (Untracked) to allow rigour init and new doc creation
|
|
26
|
-
return /M|A|D|R/.test(status);
|
|
27
|
-
})
|
|
28
|
-
.map(line => line.slice(3).trim());
|
|
29
|
-
|
|
30
|
-
for (const file of modifiedFiles) {
|
|
31
|
-
if (this.isProtected(file, protectedPaths)) {
|
|
32
|
-
const message = `Protected file '${file}' was modified.`;
|
|
33
|
-
failures.push(this.createFailure(
|
|
34
|
-
message,
|
|
35
|
-
[file],
|
|
36
|
-
`Agents are forbidden from modifying files in ${protectedPaths.join(', ')}.`,
|
|
37
|
-
message
|
|
38
|
-
));
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
} catch (error) {
|
|
42
|
-
// If not a git repo, skip safety for now
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return failures;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
private isProtected(file: string, patterns: string[]): boolean {
|
|
49
|
-
return patterns.some(p => {
|
|
50
|
-
const cleanP = p.replace('/**', '').replace('/*', '');
|
|
51
|
-
if (file === cleanP) return true;
|
|
52
|
-
if (cleanP.endsWith('/')) return file.startsWith(cleanP);
|
|
53
|
-
return file.startsWith(cleanP + '/');
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|