@rigour-labs/core 5.0.1 → 5.1.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 (141) hide show
  1. package/README.md +9 -1
  2. package/dist/gates/agent-team.d.ts +0 -1
  3. package/dist/gates/agent-team.js +0 -1
  4. package/dist/gates/checkpoint.d.ts +0 -2
  5. package/dist/gates/checkpoint.js +0 -2
  6. package/dist/gates/context-window-artifacts.d.ts +6 -2
  7. package/dist/gates/context-window-artifacts.js +107 -31
  8. package/dist/gates/deep-analysis.d.ts +2 -0
  9. package/dist/gates/deep-analysis.js +41 -11
  10. package/dist/gates/dependency.d.ts +0 -2
  11. package/dist/gates/dependency.js +23 -5
  12. package/dist/gates/deprecated-apis.d.ts +0 -2
  13. package/dist/gates/deprecated-apis.js +33 -20
  14. package/dist/gates/duplication-drift/index.d.ts +61 -0
  15. package/dist/gates/duplication-drift/index.js +240 -0
  16. package/dist/gates/duplication-drift/similarity.d.ts +68 -0
  17. package/dist/gates/duplication-drift/similarity.js +177 -0
  18. package/dist/gates/duplication-drift/tokenizer.d.ts +55 -0
  19. package/dist/gates/duplication-drift/tokenizer.js +195 -0
  20. package/dist/gates/frontend-secret-exposure.d.ts +0 -3
  21. package/dist/gates/frontend-secret-exposure.js +1 -114
  22. package/dist/gates/frontend-secret-patterns.d.ts +33 -0
  23. package/dist/gates/frontend-secret-patterns.js +119 -0
  24. package/dist/gates/{hallucinated-imports.d.ts → hallucinated-imports/index.d.ts} +2 -29
  25. package/dist/gates/hallucinated-imports/index.js +174 -0
  26. package/dist/gates/hallucinated-imports/js-resolver.d.ts +45 -0
  27. package/dist/gates/hallucinated-imports/js-resolver.js +320 -0
  28. package/dist/gates/hallucinated-imports/manifest-discovery.d.ts +28 -0
  29. package/dist/gates/hallucinated-imports/manifest-discovery.js +114 -0
  30. package/dist/gates/hallucinated-imports/python-resolver.d.ts +24 -0
  31. package/dist/gates/hallucinated-imports/python-resolver.js +306 -0
  32. package/dist/gates/hallucinated-imports-lang.d.ts +2 -2
  33. package/dist/gates/hallucinated-imports-lang.js +269 -34
  34. package/dist/gates/hallucinated-imports.test.js +1 -2
  35. package/dist/gates/inconsistent-error-handling.d.ts +0 -5
  36. package/dist/gates/inconsistent-error-handling.js +15 -144
  37. package/dist/gates/language-adapters/csharp-adapter.d.ts +16 -0
  38. package/dist/gates/language-adapters/csharp-adapter.js +211 -0
  39. package/dist/gates/language-adapters/go-adapter.d.ts +26 -0
  40. package/dist/gates/language-adapters/go-adapter.js +195 -0
  41. package/dist/gates/language-adapters/index.d.ts +15 -0
  42. package/dist/gates/language-adapters/index.js +16 -0
  43. package/dist/gates/language-adapters/java-adapter.d.ts +16 -0
  44. package/dist/gates/language-adapters/java-adapter.js +237 -0
  45. package/dist/gates/language-adapters/js-adapter.d.ts +26 -0
  46. package/dist/gates/language-adapters/js-adapter.js +279 -0
  47. package/dist/gates/language-adapters/python-adapter.d.ts +25 -0
  48. package/dist/gates/language-adapters/python-adapter.js +183 -0
  49. package/dist/gates/language-adapters/registry.d.ts +26 -0
  50. package/dist/gates/language-adapters/registry.js +65 -0
  51. package/dist/gates/language-adapters/ruby-adapter.d.ts +25 -0
  52. package/dist/gates/language-adapters/ruby-adapter.js +217 -0
  53. package/dist/gates/language-adapters/rust-adapter.d.ts +27 -0
  54. package/dist/gates/language-adapters/rust-adapter.js +235 -0
  55. package/dist/gates/language-adapters/types.d.ts +60 -0
  56. package/dist/gates/language-adapters/types.js +22 -0
  57. package/dist/gates/logic-drift-extractors.d.ts +15 -0
  58. package/dist/gates/logic-drift-extractors.js +34 -0
  59. package/dist/gates/logic-drift.d.ts +0 -30
  60. package/dist/gates/logic-drift.js +39 -129
  61. package/dist/gates/phantom-apis.d.ts +0 -2
  62. package/dist/gates/phantom-apis.js +49 -20
  63. package/dist/gates/promise-safety.d.ts +0 -1
  64. package/dist/gates/promise-safety.js +14 -2
  65. package/dist/gates/runner.js +52 -23
  66. package/dist/gates/runner.test.js +1 -1
  67. package/dist/gates/security-patterns-data.d.ts +14 -0
  68. package/dist/gates/security-patterns-data.js +235 -0
  69. package/dist/gates/security-patterns.d.ts +17 -3
  70. package/dist/gates/security-patterns.js +80 -211
  71. package/dist/gates/side-effect-analysis/categorizer.d.ts +32 -0
  72. package/dist/gates/side-effect-analysis/categorizer.js +83 -0
  73. package/dist/gates/{side-effect-analysis.d.ts → side-effect-analysis/index.d.ts} +3 -5
  74. package/dist/gates/{side-effect-analysis.js → side-effect-analysis/index.js} +33 -45
  75. package/dist/gates/side-effect-analysis/scope-tracker.d.ts +37 -0
  76. package/dist/gates/side-effect-analysis/scope-tracker.js +40 -0
  77. package/dist/gates/side-effect-helpers/index.d.ts +4 -0
  78. package/dist/gates/side-effect-helpers/index.js +4 -0
  79. package/dist/gates/side-effect-helpers/pattern-detection.d.ts +123 -0
  80. package/dist/gates/{side-effect-helpers.js → side-effect-helpers/pattern-detection.js} +22 -468
  81. package/dist/gates/side-effect-helpers/resource-tracking.d.ts +80 -0
  82. package/dist/gates/side-effect-helpers/resource-tracking.js +281 -0
  83. package/dist/gates/side-effect-helpers/scope-analysis.d.ts +21 -0
  84. package/dist/gates/side-effect-helpers/scope-analysis.js +146 -0
  85. package/dist/gates/side-effect-helpers/types.d.ts +38 -0
  86. package/dist/gates/side-effect-helpers/types.js +41 -0
  87. package/dist/gates/side-effect-rules.d.ts +0 -1
  88. package/dist/gates/side-effect-rules.js +0 -1
  89. package/dist/gates/style-drift-rules.d.ts +86 -0
  90. package/dist/gates/style-drift-rules.js +103 -0
  91. package/dist/gates/style-drift.d.ts +7 -16
  92. package/dist/gates/style-drift.js +101 -119
  93. package/dist/gates/test-quality-matchers.d.ts +53 -0
  94. package/dist/gates/test-quality-matchers.js +86 -0
  95. package/dist/gates/test-quality.d.ts +0 -3
  96. package/dist/gates/test-quality.js +47 -44
  97. package/dist/hooks/checker.d.ts +0 -1
  98. package/dist/hooks/checker.js +0 -2
  99. package/dist/hooks/dlp-templates.d.ts +0 -1
  100. package/dist/hooks/dlp-templates.js +0 -4
  101. package/dist/hooks/index.d.ts +0 -2
  102. package/dist/hooks/index.js +0 -2
  103. package/dist/hooks/input-validator.d.ts +0 -1
  104. package/dist/hooks/input-validator.js +0 -1
  105. package/dist/hooks/input-validator.test.js +0 -1
  106. package/dist/hooks/standalone-checker.d.ts +0 -1
  107. package/dist/hooks/standalone-checker.js +0 -1
  108. package/dist/hooks/standalone-dlp-checker.d.ts +0 -1
  109. package/dist/hooks/standalone-dlp-checker.js +0 -1
  110. package/dist/hooks/templates.d.ts +0 -1
  111. package/dist/hooks/templates.js +0 -1
  112. package/dist/hooks/types.d.ts +0 -1
  113. package/dist/hooks/types.js +0 -1
  114. package/dist/index.d.ts +1 -1
  115. package/dist/index.js +1 -1
  116. package/dist/inference/index.js +1 -1
  117. package/dist/services/adaptive-thresholds.d.ts +0 -2
  118. package/dist/services/adaptive-thresholds.js +0 -2
  119. package/dist/services/filesystem-cache.d.ts +0 -1
  120. package/dist/services/filesystem-cache.js +0 -1
  121. package/dist/services/score-history.d.ts +0 -1
  122. package/dist/services/score-history.js +0 -1
  123. package/dist/services/temporal-drift.d.ts +1 -2
  124. package/dist/services/temporal-drift.js +7 -8
  125. package/dist/storage/db.d.ts +23 -7
  126. package/dist/storage/db.js +116 -55
  127. package/dist/storage/findings.d.ts +4 -3
  128. package/dist/storage/findings.js +13 -20
  129. package/dist/storage/local-memory.d.ts +4 -4
  130. package/dist/storage/local-memory.js +20 -22
  131. package/dist/storage/patterns.d.ts +5 -5
  132. package/dist/storage/patterns.js +20 -26
  133. package/dist/storage/scans.d.ts +6 -6
  134. package/dist/storage/scans.js +12 -21
  135. package/dist/types/index.d.ts +1 -0
  136. package/dist/utils/scanner.js +1 -1
  137. package/package.json +7 -8
  138. package/dist/gates/duplication-drift.d.ts +0 -128
  139. package/dist/gates/duplication-drift.js +0 -585
  140. package/dist/gates/hallucinated-imports.js +0 -641
  141. package/dist/gates/side-effect-helpers.d.ts +0 -260
@@ -9,8 +9,8 @@ export function checkGoImports(content, file, cwd, projectFiles, hallucinated) {
9
9
  const lines = content.split('\n');
10
10
  let inImportBlock = false;
11
11
  // Find go.mod by walking up from the Go file's directory (monorepo support).
12
- // A monorepo may have go.mod in subdirectories like kubernetes/go.mod, server/go.mod, etc.
13
12
  let modulePath = null;
13
+ const replaceModules = new Set(); // Modules with local replace directives
14
14
  const fileAbsDir = path.dirname(path.resolve(cwd, file));
15
15
  const rootDir = path.resolve(cwd);
16
16
  let searchDir = fileAbsDir;
@@ -22,8 +22,26 @@ export function checkGoImports(content, file, cwd, projectFiles, hallucinated) {
22
22
  const moduleMatch = goMod.match(/^module\s+(\S+)/m);
23
23
  if (moduleMatch) {
24
24
  modulePath = moduleMatch[1];
25
- break;
26
25
  }
26
+ // Parse replace directives: replace github.com/foo/bar => ../local-bar
27
+ // These modules are valid even without being in go.sum
28
+ const replacePattern = /^replace\s+(\S+)\s+=>\s+/gm;
29
+ let rm;
30
+ while ((rm = replacePattern.exec(goMod)) !== null) {
31
+ replaceModules.add(rm[1]);
32
+ }
33
+ // Also handle replace blocks: replace ( ... )
34
+ const replaceBlockMatch = goMod.match(/^replace\s*\(([\s\S]*?)\)/m);
35
+ if (replaceBlockMatch) {
36
+ const blockLines = replaceBlockMatch[1].split('\n');
37
+ for (const bl of blockLines) {
38
+ const blMatch = bl.trim().match(/^(\S+)\s+=>/);
39
+ if (blMatch)
40
+ replaceModules.add(blMatch[1]);
41
+ }
42
+ }
43
+ if (modulePath)
44
+ break;
27
45
  }
28
46
  }
29
47
  catch { /* skip */ }
@@ -32,6 +50,30 @@ export function checkGoImports(content, file, cwd, projectFiles, hallucinated) {
32
50
  break;
33
51
  searchDir = parent;
34
52
  }
53
+ // Check for go.work file (Go workspace mode)
54
+ const goWorkModules = new Set();
55
+ try {
56
+ const goWorkPath = path.join(rootDir, 'go.work');
57
+ if (fs.pathExistsSync(goWorkPath)) {
58
+ const goWork = fs.readFileSync(goWorkPath, 'utf-8');
59
+ // use directives: use ./service-a or use ( ./service-a ./service-b )
60
+ const usePattern = /use\s+(\.\S+)/g;
61
+ let um;
62
+ while ((um = usePattern.exec(goWork)) !== null) {
63
+ // Read go.mod from each workspace member
64
+ const memberModPath = path.join(rootDir, um[1], 'go.mod');
65
+ if (fs.pathExistsSync(memberModPath)) {
66
+ const memberMod = fs.readFileSync(memberModPath, 'utf-8');
67
+ const memberModMatch = memberMod.match(/^module\s+(\S+)/m);
68
+ if (memberModMatch)
69
+ goWorkModules.add(memberModMatch[1]);
70
+ }
71
+ }
72
+ }
73
+ }
74
+ catch { /* skip */ }
75
+ // Check for vendor/ directory — if it exists, all vendored modules are valid
76
+ const hasVendor = fs.pathExistsSync(path.join(rootDir, 'vendor'));
35
77
  for (let i = 0; i < lines.length; i++) {
36
78
  const line = lines[i].trim();
37
79
  // Detect import block: import ( ... )
@@ -53,7 +95,6 @@ export function checkGoImports(content, file, cwd, projectFiles, hallucinated) {
53
95
  if (isGoStdlib(importPath))
54
96
  continue;
55
97
  // 2. If we have a module path, check project-relative imports FIRST
56
- // (project imports like github.com/myorg/project/pkg also have dots)
57
98
  if (modulePath && importPath.startsWith(modulePath + '/')) {
58
99
  const relPath = importPath.slice(modulePath.length + 1);
59
100
  const hasMatchingFile = [...projectFiles].some(f => f.endsWith('.go') && f.startsWith(relPath));
@@ -65,6 +106,29 @@ export function checkGoImports(content, file, cwd, projectFiles, hallucinated) {
65
106
  }
66
107
  continue;
67
108
  }
109
+ // 2b. Check if import matches a go.work workspace member module
110
+ let matchesWorkspace = false;
111
+ for (const wm of goWorkModules) {
112
+ if (importPath === wm || importPath.startsWith(wm + '/')) {
113
+ matchesWorkspace = true;
114
+ break;
115
+ }
116
+ }
117
+ if (matchesWorkspace)
118
+ continue;
119
+ // 2c. Check if import matches a replace directive (local replacement)
120
+ let matchesReplace = false;
121
+ for (const rm of replaceModules) {
122
+ if (importPath === rm || importPath.startsWith(rm + '/')) {
123
+ matchesReplace = true;
124
+ break;
125
+ }
126
+ }
127
+ if (matchesReplace)
128
+ continue;
129
+ // 2d. If vendor/ exists, all external imports are valid (vendored)
130
+ if (hasVendor && importPath.includes('.'))
131
+ continue;
68
132
  // 3. Skip external modules — any import containing a dot is a domain
69
133
  // e.g. github.com/*, google.golang.org/*, go.uber.org/*
70
134
  if (importPath.includes('.'))
@@ -128,36 +192,60 @@ export function checkRubyImports(content, file, cwd, projectFiles, hallucinated)
128
192
  }
129
193
  }
130
194
  }
131
- export function loadRubyGems(cwd) {
195
+ export function loadRubyGems(cwd, projectFiles) {
132
196
  const gems = new Set();
133
- try {
134
- const gemfilePath = path.join(cwd, 'Gemfile');
135
- if (fs.pathExistsSync(gemfilePath)) {
136
- const content = fs.readFileSync(gemfilePath, 'utf-8');
197
+ const parseGemfile = (filePath) => {
198
+ try {
199
+ if (!fs.pathExistsSync(filePath))
200
+ return;
201
+ const content = fs.readFileSync(filePath, 'utf-8');
137
202
  const gemPattern = /gem\s+['"]([^'"]+)['"]/g;
138
203
  let m;
139
204
  while ((m = gemPattern.exec(content)) !== null) {
140
205
  gems.add(m[1]);
141
206
  }
142
207
  }
143
- // Also check .gemspec
144
- const gemspecs = [...new Set()]; // placeholder
145
- const files = fs.readdirSync?.(cwd) || [];
146
- for (const f of files) {
147
- if (typeof f === 'string' && f.endsWith('.gemspec')) {
148
- try {
149
- const spec = fs.readFileSync(path.join(cwd, f), 'utf-8');
150
- const depPattern = /add_(?:runtime_)?dependency\s+['"]([^'"]+)['"]/g;
151
- let dm;
152
- while ((dm = depPattern.exec(spec)) !== null) {
153
- gems.add(dm[1]);
154
- }
208
+ catch { /* skip */ }
209
+ };
210
+ const parseGemspec = (filePath) => {
211
+ try {
212
+ if (!fs.pathExistsSync(filePath))
213
+ return;
214
+ const spec = fs.readFileSync(filePath, 'utf-8');
215
+ // add_runtime_dependency, add_dependency, add_development_dependency
216
+ const depPattern = /add_(?:runtime_|development_)?dependency\s+['"]([^'"]+)['"]/g;
217
+ let dm;
218
+ while ((dm = depPattern.exec(spec)) !== null) {
219
+ gems.add(dm[1]);
220
+ }
221
+ }
222
+ catch { /* skip */ }
223
+ };
224
+ // Parse root Gemfile
225
+ parseGemfile(path.join(cwd, 'Gemfile'));
226
+ // Discover ALL Gemfiles and .gemspec files in the project (monorepo support)
227
+ if (projectFiles) {
228
+ for (const f of projectFiles) {
229
+ if (f.endsWith('Gemfile') || f.endsWith('/Gemfile')) {
230
+ parseGemfile(path.join(cwd, f));
231
+ }
232
+ if (f.endsWith('.gemspec')) {
233
+ parseGemspec(path.join(cwd, f));
234
+ }
235
+ }
236
+ }
237
+ else {
238
+ // Fallback: check root directory only
239
+ try {
240
+ const files = fs.readdirSync?.(cwd) || [];
241
+ for (const f of files) {
242
+ if (typeof f === 'string' && f.endsWith('.gemspec')) {
243
+ parseGemspec(path.join(cwd, f));
155
244
  }
156
- catch { /* skip */ }
157
245
  }
158
246
  }
247
+ catch { /* skip */ }
159
248
  }
160
- catch { /* no Gemfile */ }
161
249
  return gems;
162
250
  }
163
251
  export function checkCSharpImports(content, file, cwd, projectFiles, hallucinated) {
@@ -218,16 +306,57 @@ export function loadNuGetPackages(cwd) {
218
306
  if (typeof f === 'string' && f.endsWith('.csproj')) {
219
307
  try {
220
308
  const content = fs.readFileSync(path.join(cwd, f), 'utf-8');
309
+ // PackageReference — NuGet packages
221
310
  const pkgPattern = /PackageReference\s+Include="([^"]+)"/g;
222
311
  let m;
223
312
  while ((m = pkgPattern.exec(content)) !== null) {
224
313
  packages.add(m[1]);
225
- // Also add top-level namespace (e.g. Newtonsoft.Json → Newtonsoft)
226
314
  packages.add(m[1].split('.')[0]);
227
315
  }
316
+ // ProjectReference — sibling projects (their namespaces become valid)
317
+ const projRefPattern = /ProjectReference\s+Include="([^"]+)"/g;
318
+ let pr;
319
+ while ((pr = projRefPattern.exec(content)) !== null) {
320
+ // Extract project name from path: "../MyLib/MyLib.csproj" → "MyLib"
321
+ const projName = path.basename(pr[1], '.csproj');
322
+ packages.add(projName);
323
+ }
324
+ // RootNamespace — project's own root namespace
325
+ const nsMatch = content.match(/<RootNamespace>([^<]+)<\/RootNamespace>/);
326
+ if (nsMatch) {
327
+ packages.add(nsMatch[1]);
328
+ packages.add(nsMatch[1].split('.')[0]);
329
+ }
228
330
  }
229
331
  catch { /* skip */ }
230
332
  }
333
+ // Legacy packages.config format
334
+ if (typeof f === 'string' && f === 'packages.config') {
335
+ try {
336
+ const content = fs.readFileSync(path.join(cwd, f), 'utf-8');
337
+ const pkgPattern = /id="([^"]+)"/g;
338
+ let m;
339
+ while ((m = pkgPattern.exec(content)) !== null) {
340
+ packages.add(m[1]);
341
+ packages.add(m[1].split('.')[0]);
342
+ }
343
+ }
344
+ catch { /* skip */ }
345
+ }
346
+ }
347
+ // Check Directory.Build.props for shared package references
348
+ const buildPropsPath = path.join(cwd, 'Directory.Build.props');
349
+ if (fs.pathExistsSync(buildPropsPath)) {
350
+ try {
351
+ const content = fs.readFileSync(buildPropsPath, 'utf-8');
352
+ const pkgPattern = /PackageReference\s+Include="([^"]+)"/g;
353
+ let m;
354
+ while ((m = pkgPattern.exec(content)) !== null) {
355
+ packages.add(m[1]);
356
+ packages.add(m[1].split('.')[0]);
357
+ }
358
+ }
359
+ catch { /* skip */ }
231
360
  }
232
361
  }
233
362
  catch { /* no .csproj */ }
@@ -294,31 +423,85 @@ export function checkRustImports(content, file, cwd, projectFiles, hallucinated)
294
423
  }
295
424
  export function loadCargoDeps(cwd) {
296
425
  const deps = new Set();
297
- try {
298
- const cargoPath = path.join(cwd, 'Cargo.toml');
299
- if (fs.pathExistsSync(cargoPath)) {
300
- const content = fs.readFileSync(cargoPath, 'utf-8');
301
- // Match [dependencies] section entries: name = "version" or name = { ... }
302
- const depPattern = /^\s*(\w[\w-]*)\s*=/gm;
426
+ const parseCargoToml = (filePath) => {
427
+ try {
428
+ if (!fs.pathExistsSync(filePath))
429
+ return;
430
+ const content = fs.readFileSync(filePath, 'utf-8');
431
+ // Parse ALL dependency sections: [dependencies], [dev-dependencies], [build-dependencies]
432
+ // Also handle [target.'cfg(...)'.dependencies]
303
433
  let inDeps = false;
304
434
  for (const line of content.split('\n')) {
305
- if (/^\[(?:.*-)?dependencies/.test(line.trim())) {
435
+ const trimmed = line.trim();
436
+ // Match any dependencies section header
437
+ if (/^\[(?:(?:dev|build)-)?dependencies(?:\.]|\])/.test(trimmed) ||
438
+ /^\[target\.[^\]]*\.(?:(?:dev|build)-)?dependencies\]/.test(trimmed) ||
439
+ /^\[workspace\.dependencies\]/.test(trimmed)) {
306
440
  inDeps = true;
307
441
  continue;
308
442
  }
309
- if (/^\[/.test(line.trim()) && inDeps) {
443
+ if (/^\[/.test(trimmed) && inDeps) {
310
444
  inDeps = false;
311
445
  continue;
312
446
  }
313
447
  if (inDeps) {
314
- const m = line.match(/^\s*([\w][\w-]*)\s*=/);
448
+ const m = trimmed.match(/^([\w][\w-]*)\s*=/);
315
449
  if (m)
316
450
  deps.add(m[1].replace(/-/g, '_')); // Rust uses _ in code for - in Cargo
317
451
  }
318
452
  }
453
+ // Handle [dependencies.foo] format (inline table)
454
+ const inlineDepPattern = /^\[(?:(?:dev|build)-)?dependencies\.([\w][\w-]*)\]/gm;
455
+ let dm;
456
+ while ((dm = inlineDepPattern.exec(content)) !== null) {
457
+ deps.add(dm[1].replace(/-/g, '_'));
458
+ }
459
+ // Handle [features] section — optional deps are valid when feature is enabled
460
+ let inFeatures = false;
461
+ for (const line of content.split('\n')) {
462
+ const trimmed = line.trim();
463
+ if (trimmed === '[features]') {
464
+ inFeatures = true;
465
+ continue;
466
+ }
467
+ if (/^\[/.test(trimmed) && inFeatures) {
468
+ inFeatures = false;
469
+ continue;
470
+ }
471
+ if (inFeatures) {
472
+ // Features reference dep names: feature = ["dep:foo", "bar/feature"]
473
+ const depRefs = trimmed.match(/"dep:(\w[\w-]*)"/g);
474
+ if (depRefs) {
475
+ for (const ref of depRefs) {
476
+ const name = ref.match(/"dep:(\w[\w-]*)"/)?.[1];
477
+ if (name)
478
+ deps.add(name.replace(/-/g, '_'));
479
+ }
480
+ }
481
+ }
482
+ }
319
483
  }
484
+ catch { /* skip */ }
485
+ };
486
+ // Parse the Cargo.toml in the working directory
487
+ parseCargoToml(path.join(cwd, 'Cargo.toml'));
488
+ // Walk up to find workspace Cargo.toml
489
+ let searchDir = cwd;
490
+ const rootDir = path.parse(cwd).root;
491
+ while (searchDir !== rootDir) {
492
+ const parentCargoPath = path.join(searchDir, 'Cargo.toml');
493
+ if (fs.pathExistsSync(parentCargoPath)) {
494
+ const content = fs.readFileSync(parentCargoPath, 'utf-8');
495
+ if (content.includes('[workspace]')) {
496
+ parseCargoToml(parentCargoPath);
497
+ break;
498
+ }
499
+ }
500
+ const parent = path.dirname(searchDir);
501
+ if (parent === searchDir)
502
+ break;
503
+ searchDir = parent;
320
504
  }
321
- catch { /* no Cargo.toml */ }
322
505
  return deps;
323
506
  }
324
507
  export function checkJavaKotlinImports(content, file, ext, cwd, projectFiles, hallucinated) {
@@ -375,11 +558,45 @@ export function loadJavaDeps(cwd) {
375
558
  if (fs.pathExistsSync(gradlePath)) {
376
559
  const content = fs.readFileSync(gradlePath, 'utf-8');
377
560
  // Match: implementation 'group:artifact:version' or "group:artifact:version"
378
- const depPattern = /(?:implementation|api|compile|testImplementation|runtimeOnly)\s*[('"]([^:'"]+)/g;
561
+ const depPattern = /(?:implementation|api|compile|testImplementation|testCompile|runtimeOnly|compileOnly|annotationProcessor)\s*[('"]([^:'"]+)/g;
379
562
  let m;
380
563
  while ((m = depPattern.exec(content)) !== null) {
381
564
  deps.add(m[1]); // group ID like "com.google.guava"
382
565
  }
566
+ // Also match Kotlin DSL format: implementation("group:artifact:version")
567
+ const kotlinDslPattern = /(?:implementation|api|testImplementation|runtimeOnly|compileOnly)\s*\(\s*"([^:"]+)/g;
568
+ while ((m = kotlinDslPattern.exec(content)) !== null) {
569
+ deps.add(m[1]);
570
+ }
571
+ }
572
+ }
573
+ // Gradle settings.gradle(.kts) — find included modules
574
+ for (const settingsFile of ['settings.gradle', 'settings.gradle.kts']) {
575
+ const settingsPath = path.join(cwd, settingsFile);
576
+ if (fs.pathExistsSync(settingsPath)) {
577
+ try {
578
+ const content = fs.readFileSync(settingsPath, 'utf-8');
579
+ // include ':module-a', ':module-b'
580
+ const includePattern = /include\s*[('"]([^'"]+)/g;
581
+ let im;
582
+ while ((im = includePattern.exec(content)) !== null) {
583
+ const moduleName = im[1].replace(/^:/, '');
584
+ deps.add(moduleName);
585
+ }
586
+ // includeBuild('../composite-project')
587
+ const compositePat = /includeBuild\s*\(\s*['"]([^'"]+)/g;
588
+ while ((im = compositePat.exec(content)) !== null) {
589
+ // Read the included build's group
590
+ const compositePath = path.resolve(cwd, im[1], 'build.gradle');
591
+ if (fs.pathExistsSync(compositePath)) {
592
+ const compositeContent = fs.readFileSync(compositePath, 'utf-8');
593
+ const groupMatch = compositeContent.match(/group\s*=\s*['"]([^'"]+)/);
594
+ if (groupMatch)
595
+ deps.add(groupMatch[1]);
596
+ }
597
+ }
598
+ }
599
+ catch { /* skip */ }
383
600
  }
384
601
  }
385
602
  // Maven: pom.xml
@@ -391,6 +608,24 @@ export function loadJavaDeps(cwd) {
391
608
  while ((m = groupPattern.exec(content)) !== null) {
392
609
  deps.add(m[1]);
393
610
  }
611
+ // Maven <modules> — multi-module projects
612
+ const modulePattern = /<module>([^<]+)<\/module>/g;
613
+ while ((m = modulePattern.exec(content)) !== null) {
614
+ deps.add(m[1]);
615
+ // Also load deps from submodule pom.xml
616
+ const subPomPath = path.join(cwd, m[1], 'pom.xml');
617
+ if (fs.pathExistsSync(subPomPath)) {
618
+ try {
619
+ const subContent = fs.readFileSync(subPomPath, 'utf-8');
620
+ const subGroupPattern = /<groupId>([^<]+)<\/groupId>/g;
621
+ let sm;
622
+ while ((sm = subGroupPattern.exec(subContent)) !== null) {
623
+ deps.add(sm[1]);
624
+ }
625
+ }
626
+ catch { /* skip */ }
627
+ }
628
+ }
394
629
  }
395
630
  }
396
631
  catch { /* no build files */ }
@@ -6,10 +6,9 @@
6
6
  * Tests the fix for Go stdlib false positives (PicoClaw regression)
7
7
  * and validates all 8 language checkers for false positive/negative accuracy.
8
8
  *
9
- * @since v3.0.1
10
9
  */
11
10
  import { describe, it, expect, vi, beforeEach } from 'vitest';
12
- import { HallucinatedImportsGate } from './hallucinated-imports.js';
11
+ import { HallucinatedImportsGate } from './hallucinated-imports/index.js';
13
12
  import path from 'path';
14
13
  // Mock fs-extra — vi.hoisted ensures these are available when vi.mock runs (hoisted)
15
14
  const { mockPathExists, mockPathExistsSync, mockReadFile, mockReadFileSync, mockReadJson, mockReaddirSync } = vi.hoisted(() => ({
@@ -17,7 +17,6 @@
17
17
  * - File C: catch(e) { return null }
18
18
  * - File D: catch(e) { [empty] }
19
19
  *
20
- * @since v2.16.0
21
20
  */
22
21
  import { Gate, GateContext } from './base.js';
23
22
  import { Failure, Provenance } from '../types/index.js';
@@ -32,9 +31,5 @@ export declare class InconsistentErrorHandlingGate extends Gate {
32
31
  constructor(config?: InconsistentErrorHandlingConfig);
33
32
  protected get provenance(): Provenance;
34
33
  run(context: GateContext): Promise<Failure[]>;
35
- private extractErrorHandlers;
36
- private classifyStrategy;
37
- private extractCatchBody;
38
- private extractCatchCallbackBody;
39
34
  private shouldSkipFile;
40
35
  }
@@ -17,11 +17,11 @@
17
17
  * - File C: catch(e) { return null }
18
18
  * - File D: catch(e) { [empty] }
19
19
  *
20
- * @since v2.16.0
21
20
  */
22
21
  import { Gate } from './base.js';
23
22
  import { FileScanner } from '../utils/scanner.js';
24
23
  import { Logger } from '../utils/logger.js';
24
+ import { languageAdapters } from './language-adapters/index.js';
25
25
  import fs from 'fs-extra';
26
26
  import path from 'path';
27
27
  export class InconsistentErrorHandlingGate extends Gate {
@@ -41,7 +41,7 @@ export class InconsistentErrorHandlingGate extends Gate {
41
41
  return [];
42
42
  const failures = [];
43
43
  const handlers = [];
44
- const scanPatterns = context.patterns || ['**/*.{ts,js,tsx,jsx}'];
44
+ const scanPatterns = context.patterns || languageAdapters.getScanPatterns();
45
45
  const files = await FileScanner.findFiles({
46
46
  cwd: context.cwd,
47
47
  patterns: scanPatterns,
@@ -53,7 +53,19 @@ export class InconsistentErrorHandlingGate extends Gate {
53
53
  continue;
54
54
  try {
55
55
  const content = await fs.readFile(path.join(context.cwd, file), 'utf-8');
56
- this.extractErrorHandlers(content, file, handlers);
56
+ const adapter = languageAdapters.getAdapter(file);
57
+ if (!adapter)
58
+ continue;
59
+ const errorHandlerFacts = adapter.extractErrorHandlers(content);
60
+ for (const fact of errorHandlerFacts) {
61
+ handlers.push({
62
+ file,
63
+ line: fact.startLine,
64
+ errorType: fact.type,
65
+ strategy: fact.strategy,
66
+ rawPattern: fact.body.split('\n')[0]?.trim() || '',
67
+ });
68
+ }
57
69
  }
58
70
  catch (e) { }
59
71
  }
@@ -96,147 +108,6 @@ export class InconsistentErrorHandlingGate extends Gate {
96
108
  }
97
109
  return failures;
98
110
  }
99
- extractErrorHandlers(content, file, handlers) {
100
- const lines = content.split('\n');
101
- for (let i = 0; i < lines.length; i++) {
102
- // Match catch clauses: catch (e), catch (error: Error), catch (e: TypeError)
103
- const catchMatch = lines[i].match(/\bcatch\s*\(\s*(\w+)(?:\s*:\s*(\w+))?\s*\)/);
104
- if (!catchMatch)
105
- continue;
106
- const varName = catchMatch[1];
107
- const explicitType = catchMatch[2] || 'any';
108
- // Extract catch body (up to next closing brace at same level)
109
- const body = this.extractCatchBody(lines, i);
110
- if (!body)
111
- continue;
112
- const strategy = this.classifyStrategy(body, varName);
113
- const rawPattern = body.split('\n')[0]?.trim() || '';
114
- handlers.push({
115
- file,
116
- line: i + 1,
117
- errorType: explicitType,
118
- strategy,
119
- rawPattern,
120
- });
121
- }
122
- // Also detect .catch() promise patterns
123
- for (let i = 0; i < lines.length; i++) {
124
- const catchMatch = lines[i].match(/\.catch\s*\(\s*(?:async\s+)?(?:\(\s*)?(\w+)/);
125
- if (!catchMatch)
126
- continue;
127
- const varName = catchMatch[1];
128
- // Extract the callback body
129
- const body = this.extractCatchCallbackBody(lines, i);
130
- if (!body)
131
- continue;
132
- const strategy = this.classifyStrategy(body, varName);
133
- handlers.push({
134
- file,
135
- line: i + 1,
136
- errorType: 'Promise',
137
- strategy,
138
- rawPattern: body.split('\n')[0]?.trim() || '',
139
- });
140
- }
141
- }
142
- classifyStrategy(body, varName) {
143
- const trimmed = body.trim();
144
- // Empty catch (swallow)
145
- if (!trimmed || trimmed === '{}' || trimmed === '') {
146
- return 'swallow';
147
- }
148
- // Re-throw
149
- if (/\bthrow\b/.test(trimmed)) {
150
- if (/\bthrow\s+new\b/.test(trimmed))
151
- return 'wrap-and-throw';
152
- if (new RegExp(`\\bthrow\\s+${varName}\\b`).test(trimmed))
153
- return 'rethrow';
154
- return 'throw-new';
155
- }
156
- // Return patterns
157
- if (/\breturn\s+null\b/.test(trimmed))
158
- return 'return-null';
159
- if (/\breturn\s+undefined\b/.test(trimmed) || /\breturn\s*;/.test(trimmed))
160
- return 'return-undefined';
161
- if (/\breturn\s+\[\s*\]/.test(trimmed))
162
- return 'return-empty-array';
163
- if (/\breturn\s+\{\s*\}/.test(trimmed))
164
- return 'return-empty-object';
165
- if (/\breturn\s+false\b/.test(trimmed))
166
- return 'return-false';
167
- if (/\breturn\s+/.test(trimmed))
168
- return 'return-value';
169
- // Logging patterns
170
- if (/console\.(error|warn)\b/.test(trimmed))
171
- return 'log-error';
172
- if (/console\.log\b/.test(trimmed))
173
- return 'log-info';
174
- if (/\blogger\b/i.test(trimmed) || /\blog\b/i.test(trimmed))
175
- return 'log-custom';
176
- // Process.exit
177
- if (/process\.exit\b/.test(trimmed))
178
- return 'exit';
179
- // Response patterns (Express/HTTP)
180
- if (/\bres\s*\.\s*status\b/.test(trimmed) || /\bres\s*\.\s*json\b/.test(trimmed))
181
- return 'http-response';
182
- // Notification patterns
183
- if (/\bnotif|toast|alert|modal\b/i.test(trimmed))
184
- return 'user-notification';
185
- return 'other';
186
- }
187
- extractCatchBody(lines, catchLine) {
188
- let braceDepth = 0;
189
- let started = false;
190
- const body = [];
191
- for (let i = catchLine; i < lines.length; i++) {
192
- for (const ch of lines[i]) {
193
- if (ch === '{') {
194
- braceDepth++;
195
- started = true;
196
- }
197
- if (ch === '}')
198
- braceDepth--;
199
- }
200
- if (started && i > catchLine)
201
- body.push(lines[i]);
202
- if (started && braceDepth === 0)
203
- break;
204
- }
205
- return body.length > 0 ? body.join('\n') : null;
206
- }
207
- extractCatchCallbackBody(lines, startLine) {
208
- // Detect if this is an arrow function (.catch(e => { ... }))
209
- const hasArrow = lines[startLine]?.includes('=>');
210
- let braceDepth = 0;
211
- let started = false;
212
- const body = [];
213
- for (let i = startLine; i < Math.min(startLine + 20, lines.length); i++) {
214
- for (const ch of lines[i]) {
215
- // For arrow functions, only track braces (not parens) for body extraction
216
- if (hasArrow) {
217
- if (ch === '{') {
218
- braceDepth++;
219
- started = true;
220
- }
221
- if (ch === '}')
222
- braceDepth--;
223
- }
224
- else {
225
- if (ch === '{' || ch === '(') {
226
- braceDepth++;
227
- started = true;
228
- }
229
- if (ch === '}' || ch === ')')
230
- braceDepth--;
231
- }
232
- }
233
- if (started && i > startLine)
234
- body.push(lines[i]);
235
- if (started && braceDepth <= 0)
236
- break;
237
- }
238
- return body.length > 0 ? body.join('\n') : null;
239
- }
240
111
  shouldSkipFile(file) {
241
112
  const normalized = file.replace(/\\/g, '/');
242
113
  return (normalized.includes('/examples/') ||
@@ -0,0 +1,16 @@
1
+ import type { LanguageAdapter, FunctionFact, ImportFact, ErrorHandlerFact, NamingPattern } from './types.js';
2
+ declare class CSharpAdapter implements LanguageAdapter {
3
+ readonly id = "csharp";
4
+ readonly name = "C#";
5
+ readonly extensions: string[];
6
+ extractFunctions(source: string): FunctionFact[];
7
+ extractImports(source: string): ImportFact[];
8
+ extractErrorHandlers(source: string): ErrorHandlerFact[];
9
+ extractNamingPatterns(source: string): NamingPattern[];
10
+ stripComments(source: string): string;
11
+ extractComparisonOps(source: string): string[];
12
+ countBranches(source: string): number;
13
+ countReturns(source: string): number;
14
+ }
15
+ export declare const csharpAdapter: CSharpAdapter;
16
+ export {};