@rigour-labs/core 2.21.2 → 3.0.0

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 (101) 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 +4 -2
  7. package/dist/gates/base.js +5 -1
  8. package/dist/gates/checkpoint.d.ts +2 -1
  9. package/dist/gates/checkpoint.js +3 -2
  10. package/dist/gates/content.js +1 -1
  11. package/dist/gates/context-window-artifacts.d.ts +34 -0
  12. package/dist/gates/context-window-artifacts.js +214 -0
  13. package/dist/gates/context.d.ts +2 -1
  14. package/dist/gates/context.js +4 -3
  15. package/dist/gates/coverage.js +3 -1
  16. package/dist/gates/dependency.js +5 -5
  17. package/dist/gates/duplication-drift.d.ts +33 -0
  18. package/dist/gates/duplication-drift.js +190 -0
  19. package/dist/gates/environment.js +4 -4
  20. package/dist/gates/file.js +1 -1
  21. package/dist/gates/hallucinated-imports.d.ts +63 -0
  22. package/dist/gates/hallucinated-imports.js +406 -0
  23. package/dist/gates/inconsistent-error-handling.d.ts +39 -0
  24. package/dist/gates/inconsistent-error-handling.js +236 -0
  25. package/dist/gates/promise-safety.d.ts +68 -0
  26. package/dist/gates/promise-safety.js +509 -0
  27. package/dist/gates/retry-loop-breaker.d.ts +2 -1
  28. package/dist/gates/retry-loop-breaker.js +2 -1
  29. package/dist/gates/runner.js +62 -1
  30. package/dist/gates/safety.d.ts +2 -1
  31. package/dist/gates/safety.js +2 -1
  32. package/dist/gates/security-patterns.d.ts +2 -1
  33. package/dist/gates/security-patterns.js +2 -1
  34. package/dist/gates/structure.js +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +1 -0
  37. package/dist/services/fix-packet-service.d.ts +0 -1
  38. package/dist/services/fix-packet-service.js +9 -14
  39. package/dist/services/score-history.d.ts +54 -0
  40. package/dist/services/score-history.js +122 -0
  41. package/dist/templates/index.js +195 -0
  42. package/dist/types/fix-packet.d.ts +5 -5
  43. package/dist/types/fix-packet.js +1 -1
  44. package/dist/types/index.d.ts +430 -0
  45. package/dist/types/index.js +57 -0
  46. package/package.json +21 -1
  47. package/src/context.test.ts +0 -256
  48. package/src/discovery.test.ts +0 -88
  49. package/src/discovery.ts +0 -112
  50. package/src/environment.test.ts +0 -115
  51. package/src/gates/agent-team.test.ts +0 -134
  52. package/src/gates/agent-team.ts +0 -210
  53. package/src/gates/ast-handlers/base.ts +0 -13
  54. package/src/gates/ast-handlers/python.ts +0 -145
  55. package/src/gates/ast-handlers/python_parser.py +0 -181
  56. package/src/gates/ast-handlers/typescript.ts +0 -264
  57. package/src/gates/ast-handlers/universal.ts +0 -184
  58. package/src/gates/ast.ts +0 -54
  59. package/src/gates/base.ts +0 -27
  60. package/src/gates/checkpoint.test.ts +0 -135
  61. package/src/gates/checkpoint.ts +0 -311
  62. package/src/gates/content.ts +0 -50
  63. package/src/gates/context.ts +0 -267
  64. package/src/gates/coverage.ts +0 -74
  65. package/src/gates/dependency.ts +0 -108
  66. package/src/gates/environment.ts +0 -94
  67. package/src/gates/file.ts +0 -42
  68. package/src/gates/retry-loop-breaker.ts +0 -151
  69. package/src/gates/runner.ts +0 -156
  70. package/src/gates/safety.ts +0 -56
  71. package/src/gates/security-patterns.test.ts +0 -162
  72. package/src/gates/security-patterns.ts +0 -305
  73. package/src/gates/structure.ts +0 -36
  74. package/src/index.ts +0 -13
  75. package/src/pattern-index/embeddings.ts +0 -84
  76. package/src/pattern-index/index.ts +0 -59
  77. package/src/pattern-index/indexer.test.ts +0 -276
  78. package/src/pattern-index/indexer.ts +0 -1023
  79. package/src/pattern-index/matcher.test.ts +0 -293
  80. package/src/pattern-index/matcher.ts +0 -493
  81. package/src/pattern-index/overrides.ts +0 -235
  82. package/src/pattern-index/security.ts +0 -151
  83. package/src/pattern-index/staleness.test.ts +0 -313
  84. package/src/pattern-index/staleness.ts +0 -568
  85. package/src/pattern-index/types.ts +0 -339
  86. package/src/safety.test.ts +0 -53
  87. package/src/services/adaptive-thresholds.test.ts +0 -189
  88. package/src/services/adaptive-thresholds.ts +0 -275
  89. package/src/services/context-engine.ts +0 -104
  90. package/src/services/fix-packet-service.ts +0 -42
  91. package/src/services/state-service.ts +0 -138
  92. package/src/smoke.test.ts +0 -18
  93. package/src/templates/index.ts +0 -312
  94. package/src/types/fix-packet.ts +0 -32
  95. package/src/types/index.ts +0 -159
  96. package/src/utils/logger.ts +0 -43
  97. package/src/utils/scanner.test.ts +0 -37
  98. package/src/utils/scanner.ts +0 -43
  99. package/tsconfig.json +0 -10
  100. package/vitest.config.ts +0 -7
  101. package/vitest.setup.ts +0 -30
@@ -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;
@@ -1,3 +1,4 @@
1
+ import { SEVERITY_WEIGHTS } from '../types/index.js';
1
2
  import { FileGate } from './file.js';
2
3
  import { ContentGate } from './content.js';
3
4
  import { StructureGate } from './structure.js';
@@ -12,6 +13,11 @@ import { RetryLoopBreakerGate } from './retry-loop-breaker.js';
12
13
  import { AgentTeamGate } from './agent-team.js';
13
14
  import { CheckpointGate } from './checkpoint.js';
14
15
  import { SecurityPatternsGate } from './security-patterns.js';
16
+ import { DuplicationDriftGate } from './duplication-drift.js';
17
+ import { HallucinatedImportsGate } from './hallucinated-imports.js';
18
+ import { InconsistentErrorHandlingGate } from './inconsistent-error-handling.js';
19
+ import { ContextWindowArtifactsGate } from './context-window-artifacts.js';
20
+ import { PromiseSafetyGate } from './promise-safety.js';
15
21
  import { execa } from 'execa';
16
22
  import { Logger } from '../utils/logger.js';
17
23
  export class GateRunner {
@@ -55,6 +61,23 @@ export class GateRunner {
55
61
  if (this.config.gates.security?.enabled !== false) {
56
62
  this.gates.push(new SecurityPatternsGate(this.config.gates.security));
57
63
  }
64
+ // v2.16+ AI-Native Drift Detection Gates (enabled by default)
65
+ if (this.config.gates.duplication_drift?.enabled !== false) {
66
+ this.gates.push(new DuplicationDriftGate(this.config.gates.duplication_drift));
67
+ }
68
+ if (this.config.gates.hallucinated_imports?.enabled !== false) {
69
+ this.gates.push(new HallucinatedImportsGate(this.config.gates.hallucinated_imports));
70
+ }
71
+ if (this.config.gates.inconsistent_error_handling?.enabled !== false) {
72
+ this.gates.push(new InconsistentErrorHandlingGate(this.config.gates.inconsistent_error_handling));
73
+ }
74
+ if (this.config.gates.context_window_artifacts?.enabled !== false) {
75
+ this.gates.push(new ContextWindowArtifactsGate(this.config.gates.context_window_artifacts));
76
+ }
77
+ // v2.17+ Promise Safety Gate (async/promise AI failure modes)
78
+ if (this.config.gates.promise_safety?.enabled !== false) {
79
+ this.gates.push(new PromiseSafetyGate(this.config.gates.promise_safety));
80
+ }
58
81
  // Environment Alignment Gate (Should be prioritized)
59
82
  if (this.config.gates.environment?.enabled) {
60
83
  this.gates.unshift(new EnvironmentGate(this.config.gates));
@@ -96,6 +119,8 @@ export class GateRunner {
96
119
  id: gate.id,
97
120
  title: `Gate Error: ${gate.title}`,
98
121
  details: error.message,
122
+ severity: 'medium',
123
+ provenance: 'traditional',
99
124
  hint: 'There was an internal error running this gate. Check the logs.',
100
125
  });
101
126
  }
@@ -119,13 +144,40 @@ export class GateRunner {
119
144
  id: key,
120
145
  title: `${key.toUpperCase()} Check Failed`,
121
146
  details: error.stderr || error.stdout || error.message,
147
+ severity: 'medium',
148
+ provenance: 'traditional',
122
149
  hint: `Fix the issues reported by \`${cmd}\`. Use rigorous standards (SOLID, DRY) in your resolution.`,
123
150
  });
124
151
  }
125
152
  }
126
153
  }
127
154
  const status = failures.length > 0 ? 'FAIL' : 'PASS';
128
- const score = Math.max(0, 100 - (failures.length * 5)); // Basic SME scoring logic
155
+ // Severity-weighted scoring: each failure deducts based on its severity
156
+ const severityBreakdown = {};
157
+ let totalDeduction = 0;
158
+ for (const f of failures) {
159
+ const sev = (f.severity || 'medium');
160
+ severityBreakdown[sev] = (severityBreakdown[sev] || 0) + 1;
161
+ totalDeduction += SEVERITY_WEIGHTS[sev] ?? 5;
162
+ }
163
+ const score = Math.max(0, 100 - totalDeduction);
164
+ // Two-score system: separate AI health from structural quality
165
+ let aiDeduction = 0;
166
+ let aiCount = 0;
167
+ let structuralDeduction = 0;
168
+ let structuralCount = 0;
169
+ for (const f of failures) {
170
+ const sev = (f.severity || 'medium');
171
+ const weight = SEVERITY_WEIGHTS[sev] ?? 5;
172
+ if (f.provenance === 'ai-drift') {
173
+ aiDeduction += weight;
174
+ aiCount++;
175
+ }
176
+ else {
177
+ structuralDeduction += weight;
178
+ structuralCount++;
179
+ }
180
+ }
129
181
  return {
130
182
  status,
131
183
  summary,
@@ -133,6 +185,15 @@ export class GateRunner {
133
185
  stats: {
134
186
  duration_ms: Date.now() - start,
135
187
  score,
188
+ ai_health_score: Math.max(0, 100 - aiDeduction),
189
+ structural_score: Math.max(0, 100 - structuralDeduction),
190
+ severity_breakdown: severityBreakdown,
191
+ provenance_breakdown: {
192
+ 'ai-drift': aiCount,
193
+ traditional: structuralCount - failures.filter(f => f.provenance === 'security' || f.provenance === 'governance').length,
194
+ security: failures.filter(f => f.provenance === 'security').length,
195
+ governance: failures.filter(f => f.provenance === 'governance').length,
196
+ },
136
197
  },
137
198
  };
138
199
  }
@@ -1,8 +1,9 @@
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
  export declare class FileGuardGate extends Gate {
4
4
  private config;
5
5
  constructor(config: Gates);
6
+ protected get provenance(): Provenance;
6
7
  run(context: GateContext): Promise<Failure[]>;
7
8
  private isProtected;
8
9
  }