@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.
Files changed (117) hide show
  1. package/README.md +58 -0
  2. package/dist/context.test.js +2 -3
  3. package/dist/environment.test.js +2 -1
  4. package/dist/gates/agent-team.d.ts +2 -1
  5. package/dist/gates/agent-team.js +1 -0
  6. package/dist/gates/base.d.ts +3 -1
  7. package/dist/gates/base.js +3 -0
  8. package/dist/gates/checkpoint.d.ts +2 -1
  9. package/dist/gates/checkpoint.js +3 -2
  10. package/dist/gates/context-window-artifacts.d.ts +2 -1
  11. package/dist/gates/context-window-artifacts.js +6 -3
  12. package/dist/gates/context.d.ts +2 -1
  13. package/dist/gates/context.js +1 -0
  14. package/dist/gates/coverage.js +3 -1
  15. package/dist/gates/dependency.js +5 -5
  16. package/dist/gates/duplication-drift.d.ts +2 -1
  17. package/dist/gates/duplication-drift.js +4 -1
  18. package/dist/gates/environment.js +4 -4
  19. package/dist/gates/hallucinated-imports.d.ts +21 -2
  20. package/dist/gates/hallucinated-imports.js +116 -2
  21. package/dist/gates/inconsistent-error-handling.d.ts +2 -1
  22. package/dist/gates/inconsistent-error-handling.js +21 -7
  23. package/dist/gates/promise-safety.d.ts +68 -0
  24. package/dist/gates/promise-safety.js +509 -0
  25. package/dist/gates/retry-loop-breaker.d.ts +2 -1
  26. package/dist/gates/retry-loop-breaker.js +2 -1
  27. package/dist/gates/runner.js +34 -1
  28. package/dist/gates/safety.d.ts +2 -1
  29. package/dist/gates/safety.js +2 -1
  30. package/dist/gates/security-patterns-owasp.test.d.ts +1 -0
  31. package/dist/gates/security-patterns-owasp.test.js +171 -0
  32. package/dist/gates/security-patterns.d.ts +6 -1
  33. package/dist/gates/security-patterns.js +101 -0
  34. package/dist/gates/structure.js +1 -1
  35. package/dist/hooks/checker.d.ts +23 -0
  36. package/dist/hooks/checker.js +222 -0
  37. package/dist/hooks/checker.test.d.ts +1 -0
  38. package/dist/hooks/checker.test.js +132 -0
  39. package/dist/hooks/index.d.ts +9 -0
  40. package/dist/hooks/index.js +8 -0
  41. package/dist/hooks/standalone-checker.d.ts +15 -0
  42. package/dist/hooks/standalone-checker.js +106 -0
  43. package/dist/hooks/templates.d.ts +22 -0
  44. package/dist/hooks/templates.js +232 -0
  45. package/dist/hooks/types.d.ts +34 -0
  46. package/dist/hooks/types.js +21 -0
  47. package/dist/index.d.ts +2 -0
  48. package/dist/index.js +2 -0
  49. package/dist/services/fix-packet-service.d.ts +0 -1
  50. package/dist/services/fix-packet-service.js +9 -14
  51. package/dist/services/score-history.d.ts +54 -0
  52. package/dist/services/score-history.js +122 -0
  53. package/dist/templates/index.js +176 -0
  54. package/dist/types/fix-packet.d.ts +5 -5
  55. package/dist/types/fix-packet.js +1 -1
  56. package/dist/types/index.d.ts +207 -0
  57. package/dist/types/index.js +32 -0
  58. package/package.json +21 -1
  59. package/src/context.test.ts +0 -256
  60. package/src/discovery.test.ts +0 -88
  61. package/src/discovery.ts +0 -112
  62. package/src/environment.test.ts +0 -115
  63. package/src/gates/agent-team.test.ts +0 -134
  64. package/src/gates/agent-team.ts +0 -210
  65. package/src/gates/ast-handlers/base.ts +0 -13
  66. package/src/gates/ast-handlers/python.ts +0 -145
  67. package/src/gates/ast-handlers/python_parser.py +0 -181
  68. package/src/gates/ast-handlers/typescript.ts +0 -264
  69. package/src/gates/ast-handlers/universal.ts +0 -184
  70. package/src/gates/ast.ts +0 -54
  71. package/src/gates/base.ts +0 -28
  72. package/src/gates/checkpoint.test.ts +0 -135
  73. package/src/gates/checkpoint.ts +0 -311
  74. package/src/gates/content.ts +0 -51
  75. package/src/gates/context-window-artifacts.ts +0 -277
  76. package/src/gates/context.ts +0 -270
  77. package/src/gates/coverage.ts +0 -74
  78. package/src/gates/dependency.ts +0 -108
  79. package/src/gates/duplication-drift.ts +0 -231
  80. package/src/gates/environment.ts +0 -94
  81. package/src/gates/file.ts +0 -46
  82. package/src/gates/hallucinated-imports.ts +0 -361
  83. package/src/gates/inconsistent-error-handling.ts +0 -254
  84. package/src/gates/retry-loop-breaker.ts +0 -151
  85. package/src/gates/runner.ts +0 -188
  86. package/src/gates/safety.ts +0 -56
  87. package/src/gates/security-patterns.test.ts +0 -162
  88. package/src/gates/security-patterns.ts +0 -306
  89. package/src/gates/structure.ts +0 -36
  90. package/src/index.ts +0 -13
  91. package/src/pattern-index/embeddings.ts +0 -84
  92. package/src/pattern-index/index.ts +0 -59
  93. package/src/pattern-index/indexer.test.ts +0 -276
  94. package/src/pattern-index/indexer.ts +0 -1023
  95. package/src/pattern-index/matcher.test.ts +0 -293
  96. package/src/pattern-index/matcher.ts +0 -493
  97. package/src/pattern-index/overrides.ts +0 -235
  98. package/src/pattern-index/security.ts +0 -151
  99. package/src/pattern-index/staleness.test.ts +0 -313
  100. package/src/pattern-index/staleness.ts +0 -568
  101. package/src/pattern-index/types.ts +0 -339
  102. package/src/safety.test.ts +0 -53
  103. package/src/services/adaptive-thresholds.test.ts +0 -189
  104. package/src/services/adaptive-thresholds.ts +0 -275
  105. package/src/services/context-engine.ts +0 -104
  106. package/src/services/fix-packet-service.ts +0 -42
  107. package/src/services/state-service.ts +0 -138
  108. package/src/smoke.test.ts +0 -18
  109. package/src/templates/index.ts +0 -338
  110. package/src/types/fix-packet.ts +0 -32
  111. package/src/types/index.ts +0 -200
  112. package/src/utils/logger.ts +0 -43
  113. package/src/utils/scanner.test.ts +0 -37
  114. package/src/utils/scanner.ts +0 -43
  115. package/tsconfig.json +0 -10
  116. package/vitest.config.ts +0 -7
  117. 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
- let depth = 0;
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
- if (ch === '{' || ch === '(') {
209
- depth++;
210
- started = true;
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 && depth <= 0)
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;