@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
|
@@ -35,6 +35,7 @@ export class InconsistentErrorHandlingGate extends Gate {
|
|
|
35
35
|
ignore_empty_catches: config.ignore_empty_catches ?? false,
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
+
get provenance() { return 'ai-drift'; }
|
|
38
39
|
async run(context) {
|
|
39
40
|
if (!this.config.enabled)
|
|
40
41
|
return [];
|
|
@@ -200,21 +201,34 @@ export class InconsistentErrorHandlingGate extends Gate {
|
|
|
200
201
|
return body.length > 0 ? body.join('\n') : null;
|
|
201
202
|
}
|
|
202
203
|
extractCatchCallbackBody(lines, startLine) {
|
|
203
|
-
|
|
204
|
+
// Detect if this is an arrow function (.catch(e => { ... }))
|
|
205
|
+
const hasArrow = lines[startLine]?.includes('=>');
|
|
206
|
+
let braceDepth = 0;
|
|
204
207
|
let started = false;
|
|
205
208
|
const body = [];
|
|
206
209
|
for (let i = startLine; i < Math.min(startLine + 20, lines.length); i++) {
|
|
207
210
|
for (const ch of lines[i]) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
+
// For arrow functions, only track braces (not parens) for body extraction
|
|
212
|
+
if (hasArrow) {
|
|
213
|
+
if (ch === '{') {
|
|
214
|
+
braceDepth++;
|
|
215
|
+
started = true;
|
|
216
|
+
}
|
|
217
|
+
if (ch === '}')
|
|
218
|
+
braceDepth--;
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
if (ch === '{' || ch === '(') {
|
|
222
|
+
braceDepth++;
|
|
223
|
+
started = true;
|
|
224
|
+
}
|
|
225
|
+
if (ch === '}' || ch === ')')
|
|
226
|
+
braceDepth--;
|
|
211
227
|
}
|
|
212
|
-
if (ch === '}' || ch === ')')
|
|
213
|
-
depth--;
|
|
214
228
|
}
|
|
215
229
|
if (started && i > startLine)
|
|
216
230
|
body.push(lines[i]);
|
|
217
|
-
if (started &&
|
|
231
|
+
if (started && braceDepth <= 0)
|
|
218
232
|
break;
|
|
219
233
|
}
|
|
220
234
|
return body.length > 0 ? body.join('\n') : null;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async & Error Safety Gate (Multi-Language)
|
|
3
|
+
*
|
|
4
|
+
* Detects unsafe async/promise/error patterns that AI code generators commonly produce.
|
|
5
|
+
* LLMs understand synchronous control flow well but frequently produce incomplete
|
|
6
|
+
* async and error-handling patterns across all languages.
|
|
7
|
+
*
|
|
8
|
+
* Supported languages:
|
|
9
|
+
* - JS/TS: .then() without .catch(), JSON.parse without try/catch, async without await, fetch without error handling
|
|
10
|
+
* - Python: json.loads without try/except, async def without await, requests/httpx without error handling, bare except
|
|
11
|
+
* - Go: ignored error returns (_, err pattern), json.Unmarshal without error check, http calls without error check
|
|
12
|
+
* - Ruby: JSON.parse without begin/rescue, Net::HTTP without begin/rescue
|
|
13
|
+
* - C#/.NET: JsonSerializer without try/catch, HttpClient without try/catch, async without await, .Result/.Wait() deadlocks
|
|
14
|
+
*
|
|
15
|
+
* @since v2.17.0
|
|
16
|
+
*/
|
|
17
|
+
import { Gate, GateContext } from './base.js';
|
|
18
|
+
import { Failure, Provenance } from '../types/index.js';
|
|
19
|
+
export interface PromiseSafetyConfig {
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
check_unhandled_then?: boolean;
|
|
22
|
+
check_unsafe_parse?: boolean;
|
|
23
|
+
check_async_without_await?: boolean;
|
|
24
|
+
check_unsafe_fetch?: boolean;
|
|
25
|
+
ignore_patterns?: string[];
|
|
26
|
+
}
|
|
27
|
+
export declare class PromiseSafetyGate extends Gate {
|
|
28
|
+
private config;
|
|
29
|
+
constructor(config?: PromiseSafetyConfig);
|
|
30
|
+
protected get provenance(): Provenance;
|
|
31
|
+
run(context: GateContext): Promise<Failure[]>;
|
|
32
|
+
private scanFile;
|
|
33
|
+
private scanJS;
|
|
34
|
+
private detectUnhandledThen;
|
|
35
|
+
private detectUnsafeParseJS;
|
|
36
|
+
private detectAsyncWithoutAwaitJS;
|
|
37
|
+
private detectUnsafeFetchJS;
|
|
38
|
+
private scanPython;
|
|
39
|
+
private detectUnsafeParsePython;
|
|
40
|
+
private detectAsyncWithoutAwaitPython;
|
|
41
|
+
private detectUnsafeFetchPython;
|
|
42
|
+
private detectBareExceptPython;
|
|
43
|
+
private scanGo;
|
|
44
|
+
private detectUnsafeParseGo;
|
|
45
|
+
private detectUnsafeFetchGo;
|
|
46
|
+
private detectIgnoredErrorsGo;
|
|
47
|
+
private scanRuby;
|
|
48
|
+
private detectUnsafeParseRuby;
|
|
49
|
+
private detectUnsafeFetchRuby;
|
|
50
|
+
private scanCSharp;
|
|
51
|
+
private detectUnsafeParseCSharp;
|
|
52
|
+
private detectUnsafeFetchCSharp;
|
|
53
|
+
private detectAsyncWithoutAwaitCSharp;
|
|
54
|
+
private detectDeadlockRiskCSharp;
|
|
55
|
+
private extractBraceBody;
|
|
56
|
+
/** Extract Python indented body after a colon */
|
|
57
|
+
private extractIndentedBody;
|
|
58
|
+
/** Check if line is inside try block (JS/TS/C# — brace-based) */
|
|
59
|
+
private isInsideTryBlock;
|
|
60
|
+
/** Check if line is inside Python try block (indent-based) */
|
|
61
|
+
private isInsidePythonTry;
|
|
62
|
+
/** Check if line is inside Ruby begin/rescue block */
|
|
63
|
+
private isInsideRubyRescue;
|
|
64
|
+
private hasCatchAhead;
|
|
65
|
+
private hasStatusCheckAhead;
|
|
66
|
+
private stripStrings;
|
|
67
|
+
private buildFailures;
|
|
68
|
+
}
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async & Error Safety Gate (Multi-Language)
|
|
3
|
+
*
|
|
4
|
+
* Detects unsafe async/promise/error patterns that AI code generators commonly produce.
|
|
5
|
+
* LLMs understand synchronous control flow well but frequently produce incomplete
|
|
6
|
+
* async and error-handling patterns across all languages.
|
|
7
|
+
*
|
|
8
|
+
* Supported languages:
|
|
9
|
+
* - JS/TS: .then() without .catch(), JSON.parse without try/catch, async without await, fetch without error handling
|
|
10
|
+
* - Python: json.loads without try/except, async def without await, requests/httpx without error handling, bare except
|
|
11
|
+
* - Go: ignored error returns (_, err pattern), json.Unmarshal without error check, http calls without error check
|
|
12
|
+
* - Ruby: JSON.parse without begin/rescue, Net::HTTP without begin/rescue
|
|
13
|
+
* - C#/.NET: JsonSerializer without try/catch, HttpClient without try/catch, async without await, .Result/.Wait() deadlocks
|
|
14
|
+
*
|
|
15
|
+
* @since v2.17.0
|
|
16
|
+
*/
|
|
17
|
+
import { Gate } from './base.js';
|
|
18
|
+
import { FileScanner } from '../utils/scanner.js';
|
|
19
|
+
import { Logger } from '../utils/logger.js';
|
|
20
|
+
import fs from 'fs-extra';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
// ─── Language Detection ───────────────────────────────────────────
|
|
23
|
+
const LANG_EXTENSIONS = {
|
|
24
|
+
'.ts': 'js', '.tsx': 'js', '.js': 'js', '.jsx': 'js', '.mjs': 'js', '.cjs': 'js',
|
|
25
|
+
'.py': 'python', '.pyw': 'python',
|
|
26
|
+
'.go': 'go',
|
|
27
|
+
'.rb': 'ruby', '.rake': 'ruby',
|
|
28
|
+
'.cs': 'csharp',
|
|
29
|
+
};
|
|
30
|
+
const LANG_GLOBS = {
|
|
31
|
+
js: ['**/*.{ts,js,tsx,jsx,mjs,cjs}'],
|
|
32
|
+
python: ['**/*.py'],
|
|
33
|
+
go: ['**/*.go'],
|
|
34
|
+
ruby: ['**/*.rb'],
|
|
35
|
+
csharp: ['**/*.cs'],
|
|
36
|
+
unknown: [],
|
|
37
|
+
};
|
|
38
|
+
function detectLang(filePath) {
|
|
39
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
40
|
+
return LANG_EXTENSIONS[ext] || 'unknown';
|
|
41
|
+
}
|
|
42
|
+
export class PromiseSafetyGate extends Gate {
|
|
43
|
+
config;
|
|
44
|
+
constructor(config = {}) {
|
|
45
|
+
super('promise-safety', 'Async & Error Safety');
|
|
46
|
+
this.config = {
|
|
47
|
+
enabled: config.enabled ?? true,
|
|
48
|
+
check_unhandled_then: config.check_unhandled_then ?? true,
|
|
49
|
+
check_unsafe_parse: config.check_unsafe_parse ?? true,
|
|
50
|
+
check_async_without_await: config.check_async_without_await ?? true,
|
|
51
|
+
check_unsafe_fetch: config.check_unsafe_fetch ?? true,
|
|
52
|
+
ignore_patterns: config.ignore_patterns ?? [],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
get provenance() { return 'ai-drift'; }
|
|
56
|
+
async run(context) {
|
|
57
|
+
if (!this.config.enabled)
|
|
58
|
+
return [];
|
|
59
|
+
const violations = [];
|
|
60
|
+
// Scan all supported languages
|
|
61
|
+
const allPatterns = Object.values(LANG_GLOBS).flat();
|
|
62
|
+
const files = await FileScanner.findFiles({
|
|
63
|
+
cwd: context.cwd,
|
|
64
|
+
patterns: allPatterns,
|
|
65
|
+
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
|
|
66
|
+
'**/*.test.*', '**/*.spec.*', '**/vendor/**', '**/__pycache__/**',
|
|
67
|
+
'**/bin/Debug/**', '**/bin/Release/**', '**/obj/**', '**/venv/**', '**/.venv/**'],
|
|
68
|
+
});
|
|
69
|
+
Logger.info(`Async Safety: Scanning ${files.length} files across all languages`);
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
if (this.config.ignore_patterns.some(p => new RegExp(p).test(file)))
|
|
72
|
+
continue;
|
|
73
|
+
const lang = detectLang(file);
|
|
74
|
+
if (lang === 'unknown')
|
|
75
|
+
continue;
|
|
76
|
+
try {
|
|
77
|
+
const fullPath = path.join(context.cwd, file);
|
|
78
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
79
|
+
const lines = content.split('\n');
|
|
80
|
+
this.scanFile(lang, lines, content, file, violations);
|
|
81
|
+
}
|
|
82
|
+
catch { /* skip unreadable files */ }
|
|
83
|
+
}
|
|
84
|
+
return this.buildFailures(violations);
|
|
85
|
+
}
|
|
86
|
+
// ─── Multi-Language Dispatcher ────────────────────────
|
|
87
|
+
scanFile(lang, lines, content, file, violations) {
|
|
88
|
+
switch (lang) {
|
|
89
|
+
case 'js':
|
|
90
|
+
this.scanJS(lines, content, file, violations);
|
|
91
|
+
break;
|
|
92
|
+
case 'python':
|
|
93
|
+
this.scanPython(lines, content, file, violations);
|
|
94
|
+
break;
|
|
95
|
+
case 'go':
|
|
96
|
+
this.scanGo(lines, content, file, violations);
|
|
97
|
+
break;
|
|
98
|
+
case 'ruby':
|
|
99
|
+
this.scanRuby(lines, content, file, violations);
|
|
100
|
+
break;
|
|
101
|
+
case 'csharp':
|
|
102
|
+
this.scanCSharp(lines, content, file, violations);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// ═══════════════════════════════════════════════════════
|
|
107
|
+
// JS/TS Checks
|
|
108
|
+
// ═══════════════════════════════════════════════════════
|
|
109
|
+
scanJS(lines, content, file, violations) {
|
|
110
|
+
if (this.config.check_unhandled_then)
|
|
111
|
+
this.detectUnhandledThen(lines, file, violations);
|
|
112
|
+
if (this.config.check_unsafe_parse)
|
|
113
|
+
this.detectUnsafeParseJS(lines, file, violations);
|
|
114
|
+
if (this.config.check_async_without_await)
|
|
115
|
+
this.detectAsyncWithoutAwaitJS(content, file, violations);
|
|
116
|
+
if (this.config.check_unsafe_fetch)
|
|
117
|
+
this.detectUnsafeFetchJS(lines, file, violations);
|
|
118
|
+
}
|
|
119
|
+
detectUnhandledThen(lines, file, violations) {
|
|
120
|
+
for (let i = 0; i < lines.length; i++) {
|
|
121
|
+
if (!/\.then\s*\(/.test(lines[i]))
|
|
122
|
+
continue;
|
|
123
|
+
let hasCatch = false;
|
|
124
|
+
for (let j = i; j < Math.min(i + 10, lines.length); j++) {
|
|
125
|
+
if (/\.catch\s*\(/.test(lines[j])) {
|
|
126
|
+
hasCatch = true;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
if (j > i && /^(?:const|let|var|function|class|export|import|if|for|while|return)\b/.test(lines[j].trim()))
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
if (!hasCatch)
|
|
133
|
+
hasCatch = this.isInsideTryBlock(lines, i);
|
|
134
|
+
const isStored = /(?:const|let|var)\s+\w+\s*=/.test(lines[i]);
|
|
135
|
+
if (!hasCatch && !isStored) {
|
|
136
|
+
violations.push({ file, line: i + 1, type: 'unhandled-then', code: lines[i].trim().substring(0, 80), reason: `.then() chain without .catch() — unhandled promise rejection` });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
detectUnsafeParseJS(lines, file, violations) {
|
|
141
|
+
for (let i = 0; i < lines.length; i++) {
|
|
142
|
+
if (/JSON\.parse\s*\(/.test(lines[i]) && !this.isInsideTryBlock(lines, i)) {
|
|
143
|
+
violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `JSON.parse() without try/catch — crashes on malformed input` });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
detectAsyncWithoutAwaitJS(content, file, violations) {
|
|
148
|
+
const patterns = [
|
|
149
|
+
/async\s+function\s+(\w+)\s*\([^)]*\)\s*\{/g,
|
|
150
|
+
/(?:const|let|var)\s+(\w+)\s*=\s*async\s+(?:\([^)]*\)|[a-zA-Z_$]\w*)\s*=>\s*\{/g,
|
|
151
|
+
/async\s+(\w+)\s*\([^)]*\)\s*(?::\s*[^{]+)?\s*\{/g,
|
|
152
|
+
];
|
|
153
|
+
for (const pattern of patterns) {
|
|
154
|
+
pattern.lastIndex = 0;
|
|
155
|
+
let match;
|
|
156
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
157
|
+
const funcName = match[1];
|
|
158
|
+
const body = this.extractBraceBody(content, match.index + match[0].length);
|
|
159
|
+
if (body && !/\bawait\b/.test(body) && body.trim().split('\n').length > 2) {
|
|
160
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
161
|
+
violations.push({ file, line: lineNum, type: 'async-no-await', code: `async ${funcName}()`, reason: `async function '${funcName}' never uses await — unnecessary async or missing await` });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
detectUnsafeFetchJS(lines, file, violations) {
|
|
167
|
+
for (let i = 0; i < lines.length; i++) {
|
|
168
|
+
if (!/\bfetch\s*\(/.test(lines[i]) && !/\baxios\.\w+\s*\(/.test(lines[i]))
|
|
169
|
+
continue;
|
|
170
|
+
if (this.isInsideTryBlock(lines, i))
|
|
171
|
+
continue;
|
|
172
|
+
if (this.hasCatchAhead(lines, i) || this.hasStatusCheckAhead(lines, i))
|
|
173
|
+
continue;
|
|
174
|
+
violations.push({ file, line: i + 1, type: 'unsafe-fetch', code: lines[i].trim().substring(0, 80), reason: `HTTP call without error handling (no try/catch, no .catch(), no .ok check)` });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// ═══════════════════════════════════════════════════════
|
|
178
|
+
// Python Checks
|
|
179
|
+
// ═══════════════════════════════════════════════════════
|
|
180
|
+
scanPython(lines, content, file, violations) {
|
|
181
|
+
if (this.config.check_unsafe_parse)
|
|
182
|
+
this.detectUnsafeParsePython(lines, file, violations);
|
|
183
|
+
if (this.config.check_async_without_await)
|
|
184
|
+
this.detectAsyncWithoutAwaitPython(content, file, violations);
|
|
185
|
+
if (this.config.check_unsafe_fetch)
|
|
186
|
+
this.detectUnsafeFetchPython(lines, file, violations);
|
|
187
|
+
this.detectBareExceptPython(lines, file, violations);
|
|
188
|
+
}
|
|
189
|
+
detectUnsafeParsePython(lines, file, violations) {
|
|
190
|
+
for (let i = 0; i < lines.length; i++) {
|
|
191
|
+
if (/json\.loads?\s*\(/.test(lines[i]) && !this.isInsidePythonTry(lines, i)) {
|
|
192
|
+
violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `json.loads() without try/except — crashes on malformed input` });
|
|
193
|
+
}
|
|
194
|
+
if (/yaml\.safe_load\s*\(/.test(lines[i]) && !this.isInsidePythonTry(lines, i)) {
|
|
195
|
+
violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `yaml.safe_load() without try/except — crashes on malformed input` });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
detectAsyncWithoutAwaitPython(content, file, violations) {
|
|
200
|
+
const pattern = /async\s+def\s+(\w+)\s*\([^)]*\)\s*(?:->[^:]+)?:/g;
|
|
201
|
+
let match;
|
|
202
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
203
|
+
const funcName = match[1];
|
|
204
|
+
const body = this.extractIndentedBody(content, match.index + match[0].length);
|
|
205
|
+
if (body && !/\bawait\b/.test(body) && body.trim().split('\n').length > 2) {
|
|
206
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
207
|
+
violations.push({ file, line: lineNum, type: 'async-no-await', code: `async def ${funcName}()`, reason: `async def '${funcName}' never uses await — unnecessary async or missing await` });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
detectUnsafeFetchPython(lines, file, violations) {
|
|
212
|
+
const httpPatterns = /\b(?:requests\.(?:get|post|put|patch|delete)|httpx\.(?:get|post|put|patch|delete)|aiohttp\.ClientSession|urllib\.request\.urlopen)\s*\(/;
|
|
213
|
+
for (let i = 0; i < lines.length; i++) {
|
|
214
|
+
if (!httpPatterns.test(lines[i]))
|
|
215
|
+
continue;
|
|
216
|
+
if (this.isInsidePythonTry(lines, i))
|
|
217
|
+
continue;
|
|
218
|
+
// Check for raise_for_status() within 10 lines
|
|
219
|
+
let hasCheck = false;
|
|
220
|
+
for (let j = i; j < Math.min(i + 10, lines.length); j++) {
|
|
221
|
+
if (/raise_for_status\s*\(/.test(lines[j]) || /\.status_code\b/.test(lines[j])) {
|
|
222
|
+
hasCheck = true;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (!hasCheck) {
|
|
227
|
+
violations.push({ file, line: i + 1, type: 'unsafe-fetch', code: lines[i].trim().substring(0, 80), reason: `HTTP call without error handling (no try/except, no raise_for_status)` });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
detectBareExceptPython(lines, file, violations) {
|
|
232
|
+
for (let i = 0; i < lines.length; i++) {
|
|
233
|
+
const trimmed = lines[i].trim();
|
|
234
|
+
if (/^except\s*:/.test(trimmed) || /^except\s+Exception\s*:/.test(trimmed)) {
|
|
235
|
+
// Check if the except block just passes (swallows errors silently)
|
|
236
|
+
const nextLine = i + 1 < lines.length ? lines[i + 1].trim() : '';
|
|
237
|
+
if (nextLine === 'pass' || nextLine === '...') {
|
|
238
|
+
violations.push({ file, line: i + 1, type: 'bare-except', code: trimmed, reason: `Bare except with pass — silently swallows all errors` });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// ═══════════════════════════════════════════════════════
|
|
244
|
+
// Go Checks
|
|
245
|
+
// ═══════════════════════════════════════════════════════
|
|
246
|
+
scanGo(lines, content, file, violations) {
|
|
247
|
+
if (this.config.check_unsafe_parse)
|
|
248
|
+
this.detectUnsafeParseGo(lines, file, violations);
|
|
249
|
+
if (this.config.check_unsafe_fetch)
|
|
250
|
+
this.detectUnsafeFetchGo(lines, file, violations);
|
|
251
|
+
this.detectIgnoredErrorsGo(lines, file, violations);
|
|
252
|
+
}
|
|
253
|
+
detectUnsafeParseGo(lines, file, violations) {
|
|
254
|
+
for (let i = 0; i < lines.length; i++) {
|
|
255
|
+
// json.Unmarshal returns error — check if error is ignored
|
|
256
|
+
if (/json\.(?:Unmarshal|NewDecoder)/.test(lines[i])) {
|
|
257
|
+
if (/\b_\s*(?:=|:=)/.test(lines[i]) || !/\berr\b/.test(lines[i])) {
|
|
258
|
+
violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `JSON decode with ignored error return — crashes on malformed input` });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
detectUnsafeFetchGo(lines, file, violations) {
|
|
264
|
+
for (let i = 0; i < lines.length; i++) {
|
|
265
|
+
if (/http\.(?:Get|Post|Do|Head)\s*\(/.test(lines[i])) {
|
|
266
|
+
if (/\b_\s*(?:=|:=)/.test(lines[i]) || !/\berr\b/.test(lines[i])) {
|
|
267
|
+
violations.push({ file, line: i + 1, type: 'unsafe-fetch', code: lines[i].trim().substring(0, 80), reason: `HTTP call with ignored error return — unhandled network errors` });
|
|
268
|
+
}
|
|
269
|
+
// Also check if resp.Body is closed (defer resp.Body.Close())
|
|
270
|
+
let hasClose = false;
|
|
271
|
+
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
272
|
+
if (/defer\s+.*\.Body\.Close\(\)/.test(lines[j]) || /\.Body\.Close\(\)/.test(lines[j])) {
|
|
273
|
+
hasClose = true;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (!hasClose && /\berr\b/.test(lines[i])) {
|
|
278
|
+
// Don't flag if error IS checked — only flag missing Body.Close
|
|
279
|
+
// This is a softer check, skip for now to reduce noise
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
detectIgnoredErrorsGo(lines, file, violations) {
|
|
285
|
+
for (let i = 0; i < lines.length; i++) {
|
|
286
|
+
// Detect: result, _ := someFunc() or _ = someFunc()
|
|
287
|
+
// Only flag when the ignored return is likely an error
|
|
288
|
+
const match = lines[i].match(/(\w+)\s*,\s*_\s*(?::=|=)\s*(\w+)\./);
|
|
289
|
+
if (match) {
|
|
290
|
+
const funcCall = lines[i].trim();
|
|
291
|
+
// Common error-returning functions
|
|
292
|
+
if (/\b(?:os\.|io\.|ioutil\.|bufio\.|sql\.|net\.|http\.|json\.|xml\.|yaml\.|strconv\.)/.test(funcCall)) {
|
|
293
|
+
violations.push({ file, line: i + 1, type: 'ignored-error', code: funcCall.substring(0, 80), reason: `Error return ignored with _ — unhandled error can cause silent failures` });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// ═══════════════════════════════════════════════════════
|
|
299
|
+
// Ruby Checks
|
|
300
|
+
// ═══════════════════════════════════════════════════════
|
|
301
|
+
scanRuby(lines, content, file, violations) {
|
|
302
|
+
if (this.config.check_unsafe_parse)
|
|
303
|
+
this.detectUnsafeParseRuby(lines, file, violations);
|
|
304
|
+
if (this.config.check_unsafe_fetch)
|
|
305
|
+
this.detectUnsafeFetchRuby(lines, file, violations);
|
|
306
|
+
}
|
|
307
|
+
detectUnsafeParseRuby(lines, file, violations) {
|
|
308
|
+
for (let i = 0; i < lines.length; i++) {
|
|
309
|
+
if (/JSON\.parse\s*\(/.test(lines[i]) && !this.isInsideRubyRescue(lines, i)) {
|
|
310
|
+
violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `JSON.parse without begin/rescue — crashes on malformed input` });
|
|
311
|
+
}
|
|
312
|
+
if (/YAML\.(?:safe_)?load\s*\(/.test(lines[i]) && !this.isInsideRubyRescue(lines, i)) {
|
|
313
|
+
violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `YAML.load without begin/rescue — crashes on malformed input` });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
detectUnsafeFetchRuby(lines, file, violations) {
|
|
318
|
+
for (let i = 0; i < lines.length; i++) {
|
|
319
|
+
if (/Net::HTTP\.(?:get|post|start)\s*\(/.test(lines[i]) || /HTTParty\.(?:get|post)\s*\(/.test(lines[i]) || /Faraday\.(?:get|post)\s*\(/.test(lines[i])) {
|
|
320
|
+
if (!this.isInsideRubyRescue(lines, i)) {
|
|
321
|
+
violations.push({ file, line: i + 1, type: 'unsafe-fetch', code: lines[i].trim().substring(0, 80), reason: `HTTP call without begin/rescue — unhandled network errors` });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// ═══════════════════════════════════════════════════════
|
|
327
|
+
// C# / .NET Checks
|
|
328
|
+
// ═══════════════════════════════════════════════════════
|
|
329
|
+
scanCSharp(lines, content, file, violations) {
|
|
330
|
+
if (this.config.check_unsafe_parse)
|
|
331
|
+
this.detectUnsafeParseCSharp(lines, file, violations);
|
|
332
|
+
if (this.config.check_unsafe_fetch)
|
|
333
|
+
this.detectUnsafeFetchCSharp(lines, file, violations);
|
|
334
|
+
if (this.config.check_async_without_await)
|
|
335
|
+
this.detectAsyncWithoutAwaitCSharp(content, file, violations);
|
|
336
|
+
this.detectDeadlockRiskCSharp(lines, file, violations);
|
|
337
|
+
}
|
|
338
|
+
detectUnsafeParseCSharp(lines, file, violations) {
|
|
339
|
+
for (let i = 0; i < lines.length; i++) {
|
|
340
|
+
if (/JsonSerializer\.Deserialize/.test(lines[i]) || /JsonConvert\.DeserializeObject/.test(lines[i]) || /JObject\.Parse/.test(lines[i])) {
|
|
341
|
+
if (!this.isInsideTryBlock(lines, i)) {
|
|
342
|
+
violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `JSON deserialization without try/catch — crashes on malformed input` });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
detectUnsafeFetchCSharp(lines, file, violations) {
|
|
348
|
+
for (let i = 0; i < lines.length; i++) {
|
|
349
|
+
if (/\.GetAsync\s*\(/.test(lines[i]) || /\.PostAsync\s*\(/.test(lines[i]) || /\.SendAsync\s*\(/.test(lines[i]) || /HttpClient\.\w+Async/.test(lines[i])) {
|
|
350
|
+
if (!this.isInsideTryBlock(lines, i)) {
|
|
351
|
+
let hasCheck = false;
|
|
352
|
+
for (let j = i; j < Math.min(i + 10, lines.length); j++) {
|
|
353
|
+
if (/EnsureSuccessStatusCode/.test(lines[j]) || /\.IsSuccessStatusCode/.test(lines[j]) || /\.StatusCode/.test(lines[j])) {
|
|
354
|
+
hasCheck = true;
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (!hasCheck) {
|
|
359
|
+
violations.push({ file, line: i + 1, type: 'unsafe-fetch', code: lines[i].trim().substring(0, 80), reason: `HTTP call without error handling (no try/catch, no status check)` });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
detectAsyncWithoutAwaitCSharp(content, file, violations) {
|
|
366
|
+
const pattern = /async\s+Task(?:<[^>]+>)?\s+(\w+)\s*\([^)]*\)\s*\{/g;
|
|
367
|
+
let match;
|
|
368
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
369
|
+
const funcName = match[1];
|
|
370
|
+
const body = this.extractBraceBody(content, match.index + match[0].length);
|
|
371
|
+
if (body && !/\bawait\b/.test(body) && body.trim().split('\n').length > 2) {
|
|
372
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
373
|
+
violations.push({ file, line: lineNum, type: 'async-no-await', code: `async Task ${funcName}()`, reason: `async method '${funcName}' never uses await — unnecessary async or missing await` });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
detectDeadlockRiskCSharp(lines, file, violations) {
|
|
378
|
+
for (let i = 0; i < lines.length; i++) {
|
|
379
|
+
if (/\.Result\b/.test(lines[i]) || /\.Wait\(\)/.test(lines[i]) || /\.GetAwaiter\(\)\.GetResult\(\)/.test(lines[i])) {
|
|
380
|
+
// Common AI mistake: using .Result or .Wait() on async tasks causes deadlocks
|
|
381
|
+
violations.push({ file, line: i + 1, type: 'deadlock-risk', code: lines[i].trim().substring(0, 80), reason: `.Result/.Wait() on async task — deadlock risk in synchronous context` });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// ═══════════════════════════════════════════════════════
|
|
386
|
+
// Shared Helpers
|
|
387
|
+
// ═══════════════════════════════════════════════════════
|
|
388
|
+
extractBraceBody(content, startIdx) {
|
|
389
|
+
let depth = 1;
|
|
390
|
+
let idx = startIdx;
|
|
391
|
+
while (depth > 0 && idx < content.length) {
|
|
392
|
+
if (content[idx] === '{')
|
|
393
|
+
depth++;
|
|
394
|
+
if (content[idx] === '}')
|
|
395
|
+
depth--;
|
|
396
|
+
idx++;
|
|
397
|
+
}
|
|
398
|
+
return depth === 0 ? content.substring(startIdx, idx - 1) : null;
|
|
399
|
+
}
|
|
400
|
+
/** Extract Python indented body after a colon */
|
|
401
|
+
extractIndentedBody(content, startIdx) {
|
|
402
|
+
const rest = content.substring(startIdx);
|
|
403
|
+
const lines = rest.split('\n');
|
|
404
|
+
if (lines.length < 2)
|
|
405
|
+
return null;
|
|
406
|
+
// Find indent level of first non-empty line after the def
|
|
407
|
+
let bodyIndent = -1;
|
|
408
|
+
const bodyLines = [];
|
|
409
|
+
for (let i = 1; i < lines.length; i++) {
|
|
410
|
+
const line = lines[i];
|
|
411
|
+
if (line.trim() === '' || line.trim().startsWith('#')) {
|
|
412
|
+
bodyLines.push(line);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const indent = line.length - line.trimStart().length;
|
|
416
|
+
if (bodyIndent === -1) {
|
|
417
|
+
bodyIndent = indent;
|
|
418
|
+
}
|
|
419
|
+
if (indent < bodyIndent)
|
|
420
|
+
break;
|
|
421
|
+
bodyLines.push(line);
|
|
422
|
+
}
|
|
423
|
+
return bodyLines.join('\n');
|
|
424
|
+
}
|
|
425
|
+
/** Check if line is inside try block (JS/TS/C# — brace-based) */
|
|
426
|
+
isInsideTryBlock(lines, lineIdx) {
|
|
427
|
+
let braceDepth = 0;
|
|
428
|
+
for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - 30); j--) {
|
|
429
|
+
const prevLine = this.stripStrings(lines[j]);
|
|
430
|
+
for (const ch of prevLine) {
|
|
431
|
+
if (ch === '}')
|
|
432
|
+
braceDepth++;
|
|
433
|
+
if (ch === '{')
|
|
434
|
+
braceDepth--;
|
|
435
|
+
}
|
|
436
|
+
if (/\btry\s*\{/.test(prevLine) && braceDepth <= 0)
|
|
437
|
+
return true;
|
|
438
|
+
if (/\}\s*catch\s*\(/.test(prevLine))
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
/** Check if line is inside Python try block (indent-based) */
|
|
444
|
+
isInsidePythonTry(lines, lineIdx) {
|
|
445
|
+
const lineIndent = lines[lineIdx].length - lines[lineIdx].trimStart().length;
|
|
446
|
+
for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - 30); j--) {
|
|
447
|
+
const trimmed = lines[j].trim();
|
|
448
|
+
if (trimmed === '')
|
|
449
|
+
continue;
|
|
450
|
+
const indent = lines[j].length - lines[j].trimStart().length;
|
|
451
|
+
if (indent < lineIndent && /^\s*try\s*:/.test(lines[j]))
|
|
452
|
+
return true;
|
|
453
|
+
if (indent < lineIndent && /^\s*(?:except|finally)\s*/.test(lines[j]))
|
|
454
|
+
return false;
|
|
455
|
+
if (indent === 0 && /^(?:def|class|async\s+def)\s/.test(trimmed))
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
/** Check if line is inside Ruby begin/rescue block */
|
|
461
|
+
isInsideRubyRescue(lines, lineIdx) {
|
|
462
|
+
for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - 30); j--) {
|
|
463
|
+
const trimmed = lines[j].trim();
|
|
464
|
+
if (trimmed === 'begin')
|
|
465
|
+
return true;
|
|
466
|
+
if (/^rescue\b/.test(trimmed))
|
|
467
|
+
return false;
|
|
468
|
+
if (/^(?:def|class|module)\s/.test(trimmed))
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
hasCatchAhead(lines, idx) {
|
|
474
|
+
for (let j = idx; j < Math.min(idx + 10, lines.length); j++) {
|
|
475
|
+
if (/\.catch\s*\(/.test(lines[j]))
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
hasStatusCheckAhead(lines, idx) {
|
|
481
|
+
for (let j = idx; j < Math.min(idx + 10, lines.length); j++) {
|
|
482
|
+
if (/\.\s*ok\b/.test(lines[j]) || /\.status(?:Text)?\b/.test(lines[j]))
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
stripStrings(line) {
|
|
488
|
+
return line.replace(/`[^`]*`/g, '""').replace(/"[^"]*"/g, '""').replace(/'[^']*'/g, '""');
|
|
489
|
+
}
|
|
490
|
+
// ─── Failure Aggregation ──────────────────────────────
|
|
491
|
+
buildFailures(violations) {
|
|
492
|
+
const byFile = new Map();
|
|
493
|
+
for (const v of violations) {
|
|
494
|
+
const existing = byFile.get(v.file) || [];
|
|
495
|
+
existing.push(v);
|
|
496
|
+
byFile.set(v.file, existing);
|
|
497
|
+
}
|
|
498
|
+
const failures = [];
|
|
499
|
+
for (const [file, fileViolations] of byFile) {
|
|
500
|
+
const details = fileViolations.map(v => ` L${v.line}: [${v.type}] ${v.reason}`).join('\n');
|
|
501
|
+
const hasHighSev = fileViolations.some(v => v.type !== 'async-no-await');
|
|
502
|
+
const severity = hasHighSev ? 'high' : 'medium';
|
|
503
|
+
const lang = detectLang(file);
|
|
504
|
+
const langLabel = lang === 'js' ? 'JS/TS' : lang === 'csharp' ? 'C#' : lang.charAt(0).toUpperCase() + lang.slice(1);
|
|
505
|
+
failures.push(this.createFailure(`Unsafe async/error patterns in ${file}:\n${details}`, [file], `AI code generators often produce incomplete error handling in ${langLabel}. Ensure all parse operations are wrapped in error handling, async functions use await, and HTTP calls check for errors.`, 'Async & Error Safety Violation', fileViolations[0].line, undefined, severity));
|
|
506
|
+
}
|
|
507
|
+
return failures;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Gate, GateContext } from './base.js';
|
|
2
|
-
import { Failure, Gates } from '../types/index.js';
|
|
2
|
+
import { Failure, Gates, Provenance } from '../types/index.js';
|
|
3
3
|
interface FailureRecord {
|
|
4
4
|
category: string;
|
|
5
5
|
count: number;
|
|
@@ -19,6 +19,7 @@ interface RigourState {
|
|
|
19
19
|
export declare class RetryLoopBreakerGate extends Gate {
|
|
20
20
|
private options;
|
|
21
21
|
constructor(options: Gates['retry_loop_breaker']);
|
|
22
|
+
protected get provenance(): Provenance;
|
|
22
23
|
run(context: GateContext): Promise<Failure[]>;
|
|
23
24
|
/**
|
|
24
25
|
* Classify an error message into a category based on patterns.
|
|
@@ -22,13 +22,14 @@ export class RetryLoopBreakerGate extends Gate {
|
|
|
22
22
|
super('retry_loop_breaker', 'Retry Loop Breaker');
|
|
23
23
|
this.options = options;
|
|
24
24
|
}
|
|
25
|
+
get provenance() { return 'governance'; }
|
|
25
26
|
async run(context) {
|
|
26
27
|
const state = await this.loadState(context.cwd);
|
|
27
28
|
const failures = [];
|
|
28
29
|
for (const [category, record] of Object.entries(state.failureHistory)) {
|
|
29
30
|
if (record.count >= (this.options?.max_retries ?? 3)) {
|
|
30
31
|
const docUrl = this.options?.doc_sources?.[category] || this.getDefaultDocUrl(category);
|
|
31
|
-
failures.push(this.createFailure(`Operation '${category}' has failed ${record.count} times consecutively. Last error: ${record.lastError}`, undefined, `STOP RETRYING. You are in a loop. Consult the official documentation: ${docUrl}. Extract the canonical solution pattern and apply it.`, `Retry Loop Detected: ${category}
|
|
32
|
+
failures.push(this.createFailure(`Operation '${category}' has failed ${record.count} times consecutively. Last error: ${record.lastError}`, undefined, `STOP RETRYING. You are in a loop. Consult the official documentation: ${docUrl}. Extract the canonical solution pattern and apply it.`, `Retry Loop Detected: ${category}`, undefined, undefined, 'critical'));
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
return failures;
|