@rigour-labs/core 5.0.1 → 5.1.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 (139) 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 +51 -22
  66. package/dist/gates/security-patterns-data.d.ts +14 -0
  67. package/dist/gates/security-patterns-data.js +235 -0
  68. package/dist/gates/security-patterns.d.ts +17 -3
  69. package/dist/gates/security-patterns.js +80 -211
  70. package/dist/gates/side-effect-analysis/categorizer.d.ts +32 -0
  71. package/dist/gates/side-effect-analysis/categorizer.js +83 -0
  72. package/dist/gates/{side-effect-analysis.d.ts → side-effect-analysis/index.d.ts} +3 -5
  73. package/dist/gates/{side-effect-analysis.js → side-effect-analysis/index.js} +33 -45
  74. package/dist/gates/side-effect-analysis/scope-tracker.d.ts +37 -0
  75. package/dist/gates/side-effect-analysis/scope-tracker.js +40 -0
  76. package/dist/gates/side-effect-helpers/index.d.ts +4 -0
  77. package/dist/gates/side-effect-helpers/index.js +4 -0
  78. package/dist/gates/side-effect-helpers/pattern-detection.d.ts +123 -0
  79. package/dist/gates/{side-effect-helpers.js → side-effect-helpers/pattern-detection.js} +22 -468
  80. package/dist/gates/side-effect-helpers/resource-tracking.d.ts +80 -0
  81. package/dist/gates/side-effect-helpers/resource-tracking.js +281 -0
  82. package/dist/gates/side-effect-helpers/scope-analysis.d.ts +21 -0
  83. package/dist/gates/side-effect-helpers/scope-analysis.js +146 -0
  84. package/dist/gates/side-effect-helpers/types.d.ts +38 -0
  85. package/dist/gates/side-effect-helpers/types.js +41 -0
  86. package/dist/gates/side-effect-rules.d.ts +0 -1
  87. package/dist/gates/side-effect-rules.js +0 -1
  88. package/dist/gates/style-drift-rules.d.ts +86 -0
  89. package/dist/gates/style-drift-rules.js +103 -0
  90. package/dist/gates/style-drift.d.ts +7 -16
  91. package/dist/gates/style-drift.js +101 -119
  92. package/dist/gates/test-quality-matchers.d.ts +53 -0
  93. package/dist/gates/test-quality-matchers.js +86 -0
  94. package/dist/gates/test-quality.d.ts +0 -3
  95. package/dist/gates/test-quality.js +47 -44
  96. package/dist/hooks/checker.d.ts +0 -1
  97. package/dist/hooks/checker.js +0 -2
  98. package/dist/hooks/dlp-templates.d.ts +0 -1
  99. package/dist/hooks/dlp-templates.js +0 -4
  100. package/dist/hooks/index.d.ts +0 -2
  101. package/dist/hooks/index.js +0 -2
  102. package/dist/hooks/input-validator.d.ts +0 -1
  103. package/dist/hooks/input-validator.js +0 -1
  104. package/dist/hooks/input-validator.test.js +0 -1
  105. package/dist/hooks/standalone-checker.d.ts +0 -1
  106. package/dist/hooks/standalone-checker.js +0 -1
  107. package/dist/hooks/standalone-dlp-checker.d.ts +0 -1
  108. package/dist/hooks/standalone-dlp-checker.js +0 -1
  109. package/dist/hooks/templates.d.ts +0 -1
  110. package/dist/hooks/templates.js +0 -1
  111. package/dist/hooks/types.d.ts +0 -1
  112. package/dist/hooks/types.js +0 -1
  113. package/dist/index.d.ts +1 -1
  114. package/dist/index.js +1 -1
  115. package/dist/services/adaptive-thresholds.d.ts +0 -2
  116. package/dist/services/adaptive-thresholds.js +0 -2
  117. package/dist/services/filesystem-cache.d.ts +0 -1
  118. package/dist/services/filesystem-cache.js +0 -1
  119. package/dist/services/score-history.d.ts +0 -1
  120. package/dist/services/score-history.js +0 -1
  121. package/dist/services/temporal-drift.d.ts +1 -2
  122. package/dist/services/temporal-drift.js +7 -8
  123. package/dist/storage/db.d.ts +23 -7
  124. package/dist/storage/db.js +116 -55
  125. package/dist/storage/findings.d.ts +4 -3
  126. package/dist/storage/findings.js +13 -20
  127. package/dist/storage/local-memory.d.ts +4 -4
  128. package/dist/storage/local-memory.js +20 -22
  129. package/dist/storage/patterns.d.ts +5 -5
  130. package/dist/storage/patterns.js +20 -26
  131. package/dist/storage/scans.d.ts +6 -6
  132. package/dist/storage/scans.js +12 -21
  133. package/dist/types/index.d.ts +1 -0
  134. package/dist/utils/scanner.js +1 -1
  135. package/package.json +7 -8
  136. package/dist/gates/duplication-drift.d.ts +0 -128
  137. package/dist/gates/duplication-drift.js +0 -585
  138. package/dist/gates/hallucinated-imports.js +0 -641
  139. package/dist/gates/side-effect-helpers.d.ts +0 -260
@@ -1,641 +0,0 @@
1
- /**
2
- * Hallucinated Imports Gate
3
- *
4
- * Detects imports that reference modules which don't exist in the project.
5
- * This is an AI-specific failure mode — LLMs confidently generate import
6
- * statements for packages, files, or modules that were never installed
7
- * or created.
8
- *
9
- * Supported languages (v3.0.1):
10
- * JS/TS — package.json deps, node_modules fallback, Node.js builtins (22.x)
11
- * Python — stdlib whitelist (3.12+), relative imports, local module resolution
12
- * Go — stdlib whitelist (1.22+), go.mod module path, aliased imports
13
- * Ruby — stdlib whitelist (3.3+), Gemfile parsing, require + require_relative
14
- * C# — .NET 8 framework namespaces, .csproj NuGet parsing, using directives
15
- * Rust — std/core/alloc crates, Cargo.toml deps, use/extern crate statements
16
- * Java — java/javax/jakarta stdlib, build.gradle + pom.xml deps, import statements
17
- * Kotlin — kotlin/kotlinx stdlib, Gradle deps, import statements
18
- *
19
- * @since v2.16.0
20
- * @since v3.0.1 — Go stdlib fix, Ruby/C# strengthened, Rust/Java/Kotlin added
21
- */
22
- import { Gate } from './base.js';
23
- import { FileScanner } from '../utils/scanner.js';
24
- import { Logger } from '../utils/logger.js';
25
- import fs from 'fs-extra';
26
- import path from 'path';
27
- import ts from 'typescript';
28
- import { isNodeBuiltin, isPythonStdlib } from './hallucinated-imports-stdlib.js';
29
- import { checkGoImports, checkRubyImports, checkCSharpImports, checkRustImports, checkJavaKotlinImports, loadPackageJson } from './hallucinated-imports-lang.js';
30
- export class HallucinatedImportsGate extends Gate {
31
- config;
32
- constructor(config = {}) {
33
- super('hallucinated-imports', 'Hallucinated Import Detection');
34
- this.config = {
35
- enabled: config.enabled ?? true,
36
- check_relative: config.check_relative ?? true,
37
- check_packages: config.check_packages ?? true,
38
- ignore_patterns: config.ignore_patterns ?? [
39
- '\\.css$', '\\.scss$', '\\.less$', '\\.svg$', '\\.png$', '\\.jpg$',
40
- '\\.json$', '\\.wasm$', '\\.graphql$', '\\.gql$',
41
- ],
42
- };
43
- }
44
- get provenance() { return 'ai-drift'; }
45
- async run(context) {
46
- if (!this.config.enabled)
47
- return [];
48
- const failures = [];
49
- const hallucinated = [];
50
- const defaultPatterns = ['**/*.{ts,js,tsx,jsx,py,go,rb,cs,rs,java,kt}'];
51
- const scanPatterns = context.patterns || defaultPatterns;
52
- const files = await FileScanner.findFiles({
53
- cwd: context.cwd,
54
- patterns: scanPatterns,
55
- ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
56
- '**/examples/**',
57
- '**/studio-dist/**', '**/.next/**', '**/coverage/**',
58
- '**/*.test.*', '**/*.spec.*', '**/__tests__/**',
59
- '**/.venv/**', '**/venv/**', '**/vendor/**', '**/bin/Debug/**', '**/bin/Release/**', '**/obj/**',
60
- '**/target/debug/**', '**/target/release/**', // Rust
61
- '**/out/**', '**/.gradle/**', '**/gradle/**'], // Java/Kotlin
62
- });
63
- const analyzableFiles = files.filter(file => !this.shouldSkipFile(file));
64
- Logger.info(`Hallucinated Imports: Scanning ${analyzableFiles.length} files`);
65
- // Build lookup sets for fast resolution
66
- const projectFiles = new Set(analyzableFiles.map(f => f.replace(/\\/g, '/')));
67
- const packageJson = await loadPackageJson(context.cwd);
68
- const rootDeps = new Set([
69
- ...Object.keys(packageJson?.dependencies || {}),
70
- ...Object.keys(packageJson?.devDependencies || {}),
71
- ...Object.keys(packageJson?.peerDependencies || {}),
72
- ...Object.keys(packageJson?.optionalDependencies || {}),
73
- ]);
74
- const depCacheByDir = new Map();
75
- const tsPathCacheByDir = new Map();
76
- // Check if node_modules exists (for package verification)
77
- const hasNodeModules = await fs.pathExists(path.join(context.cwd, 'node_modules'));
78
- for (const file of analyzableFiles) {
79
- try {
80
- const fullPath = path.join(context.cwd, file);
81
- const content = await fs.readFile(fullPath, 'utf-8');
82
- const ext = path.extname(file);
83
- if (['.ts', '.js', '.tsx', '.jsx'].includes(ext)) {
84
- await this.checkJSImports(content, file, context.cwd, projectFiles, rootDeps, depCacheByDir, hasNodeModules, hallucinated, tsPathCacheByDir);
85
- }
86
- else if (ext === '.py') {
87
- await this.checkPyImports(content, file, context.cwd, projectFiles, hallucinated);
88
- }
89
- else if (ext === '.go') {
90
- checkGoImports(content, file, context.cwd, projectFiles, hallucinated);
91
- }
92
- else if (ext === '.rb') {
93
- checkRubyImports(content, file, context.cwd, projectFiles, hallucinated);
94
- }
95
- else if (ext === '.cs') {
96
- checkCSharpImports(content, file, context.cwd, projectFiles, hallucinated);
97
- }
98
- else if (ext === '.rs') {
99
- checkRustImports(content, file, context.cwd, projectFiles, hallucinated);
100
- }
101
- else if (ext === '.java' || ext === '.kt') {
102
- checkJavaKotlinImports(content, file, ext, context.cwd, projectFiles, hallucinated);
103
- }
104
- }
105
- catch (e) { }
106
- }
107
- // Group hallucinated imports by file for cleaner output
108
- const byFile = new Map();
109
- for (const h of hallucinated) {
110
- const existing = byFile.get(h.file) || [];
111
- existing.push(h);
112
- byFile.set(h.file, existing);
113
- }
114
- for (const [file, imports] of byFile) {
115
- const details = imports.map(i => ` L${i.line}: import '${i.importPath}' — ${i.reason}`).join('\n');
116
- failures.push(this.createFailure(`Hallucinated imports in ${file}:\n${details}`, [file], `These imports reference modules that don't exist. Remove or replace with real modules. AI models often "hallucinate" package names or file paths.`, 'Hallucinated Imports', imports[0].line, undefined, 'critical'));
117
- }
118
- return failures;
119
- }
120
- async checkJSImports(content, file, cwd, projectFiles, rootDeps, depCacheByDir, hasNodeModules, hallucinated, tsPathCacheByDir) {
121
- const depsForFile = await this.resolveJSDepsForFile(file, cwd, rootDeps, depCacheByDir);
122
- for (const spec of this.collectJSImportSpecs(content, file)) {
123
- const { importPath, line } = spec;
124
- if (!importPath || this.shouldIgnore(importPath))
125
- continue;
126
- if (importPath.startsWith('.')) {
127
- if (this.config.check_relative) {
128
- const resolved = this.resolveRelativeImport(file, importPath, projectFiles);
129
- if (!resolved) {
130
- hallucinated.push({
131
- file, line, importPath, type: 'relative',
132
- reason: `File not found: ${importPath}`,
133
- });
134
- }
135
- }
136
- }
137
- else {
138
- const aliasResolution = await this.resolveTsPathAlias(file, importPath, cwd, projectFiles, tsPathCacheByDir);
139
- if (aliasResolution === true)
140
- continue;
141
- if (aliasResolution === false) {
142
- hallucinated.push({
143
- file, line, importPath, type: 'package',
144
- reason: `Path alias '${importPath}' does not resolve to a project file`,
145
- });
146
- continue;
147
- }
148
- if (this.config.check_packages) {
149
- const pkgName = this.extractPackageName(importPath);
150
- if (isNodeBuiltin(pkgName))
151
- continue;
152
- if (!depsForFile.has(pkgName)) {
153
- if (hasNodeModules) {
154
- const pkgPath = path.join(cwd, 'node_modules', pkgName);
155
- if (await fs.pathExists(pkgPath))
156
- continue;
157
- }
158
- hallucinated.push({
159
- file, line, importPath, type: 'package',
160
- reason: `Package '${pkgName}' not in package.json dependencies`,
161
- });
162
- }
163
- }
164
- }
165
- }
166
- }
167
- collectJSImportSpecs(content, file) {
168
- const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
169
- const specs = [];
170
- const add = (node, value) => {
171
- if (!value)
172
- return;
173
- const line = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
174
- specs.push({ importPath: value, line });
175
- };
176
- const visit = (node) => {
177
- if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
178
- add(node, node.moduleSpecifier.text);
179
- }
180
- else if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
181
- add(node, node.moduleSpecifier.text);
182
- }
183
- else if (ts.isCallExpression(node)) {
184
- // require('x')
185
- if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
186
- const firstArg = node.arguments[0];
187
- if (firstArg && ts.isStringLiteral(firstArg)) {
188
- add(node, firstArg.text);
189
- }
190
- }
191
- // import('x')
192
- if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
193
- const firstArg = node.arguments[0];
194
- if (firstArg && ts.isStringLiteral(firstArg)) {
195
- add(node, firstArg.text);
196
- }
197
- }
198
- }
199
- ts.forEachChild(node, visit);
200
- };
201
- ts.forEachChild(sourceFile, visit);
202
- return specs;
203
- }
204
- async resolveJSDepsForFile(file, cwd, rootDeps, depCacheByDir) {
205
- const rootDir = path.resolve(cwd);
206
- let currentDir = path.dirname(path.resolve(cwd, file));
207
- while (currentDir.startsWith(rootDir)) {
208
- const cached = depCacheByDir.get(currentDir);
209
- if (cached)
210
- return cached;
211
- const packageJsonPath = path.join(currentDir, 'package.json');
212
- if (await fs.pathExists(packageJsonPath)) {
213
- try {
214
- const packageJson = await fs.readJson(packageJsonPath);
215
- const deps = new Set([
216
- ...rootDeps,
217
- ...Object.keys(packageJson?.dependencies || {}),
218
- ...Object.keys(packageJson?.devDependencies || {}),
219
- ...Object.keys(packageJson?.peerDependencies || {}),
220
- ...Object.keys(packageJson?.optionalDependencies || {}),
221
- ]);
222
- depCacheByDir.set(currentDir, deps);
223
- return deps;
224
- }
225
- catch {
226
- depCacheByDir.set(currentDir, rootDeps);
227
- return rootDeps;
228
- }
229
- }
230
- const parent = path.dirname(currentDir);
231
- if (parent === currentDir)
232
- break;
233
- currentDir = parent;
234
- }
235
- return rootDeps;
236
- }
237
- async resolveTsPathAlias(file, importPath, cwd, projectFiles, tsPathCacheByDir) {
238
- const config = await this.resolveTsPathConfigForFile(file, cwd, tsPathCacheByDir);
239
- if (!config || config.rules.length === 0)
240
- return null;
241
- for (const rule of config.rules) {
242
- const wildcard = this.matchTsPathRule(rule, importPath);
243
- if (wildcard === null)
244
- continue;
245
- for (const target of rule.targets) {
246
- const candidatePattern = rule.hasWildcard ? target.replace('*', wildcard) : target;
247
- if (this.resolveTsPathTarget(config.baseDir, candidatePattern, cwd, projectFiles)) {
248
- return true;
249
- }
250
- }
251
- return false;
252
- }
253
- return null;
254
- }
255
- matchTsPathRule(rule, importPath) {
256
- if (!rule.hasWildcard) {
257
- return importPath === rule.key ? '' : null;
258
- }
259
- if (!importPath.startsWith(rule.prefix) || !importPath.endsWith(rule.suffix)) {
260
- return null;
261
- }
262
- return importPath.slice(rule.prefix.length, importPath.length - rule.suffix.length);
263
- }
264
- resolveTsPathTarget(baseDir, candidatePattern, cwd, projectFiles) {
265
- const absolute = path.resolve(baseDir, candidatePattern);
266
- const relative = path.relative(cwd, absolute).replace(/\\/g, '/');
267
- const normalized = relative.replace(/\/$/, '');
268
- const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.d.ts'];
269
- const candidates = [
270
- ...extensions.map(ext => normalized + ext),
271
- ...extensions.map(ext => `${normalized}/index${ext}`),
272
- ];
273
- return candidates.some(c => projectFiles.has(c));
274
- }
275
- async resolveTsPathConfigForFile(file, cwd, tsPathCacheByDir) {
276
- const rootDir = path.resolve(cwd);
277
- let currentDir = path.dirname(path.resolve(cwd, file));
278
- while (currentDir.startsWith(rootDir)) {
279
- if (tsPathCacheByDir.has(currentDir)) {
280
- const cached = tsPathCacheByDir.get(currentDir) || null;
281
- if (cached)
282
- return cached;
283
- }
284
- else {
285
- const config = await this.loadTsPathConfig(currentDir);
286
- tsPathCacheByDir.set(currentDir, config);
287
- if (config)
288
- return config;
289
- }
290
- const parent = path.dirname(currentDir);
291
- if (parent === currentDir)
292
- break;
293
- currentDir = parent;
294
- }
295
- return null;
296
- }
297
- async loadTsPathConfig(searchDir) {
298
- const candidates = ['tsconfig.json', 'jsconfig.json', 'tsconfig.base.json'];
299
- for (const configName of candidates) {
300
- const configPath = path.join(searchDir, configName);
301
- if (!(await fs.pathExists(configPath)))
302
- continue;
303
- const parsed = await this.readLooseJson(configPath);
304
- const compilerOptions = parsed?.compilerOptions || {};
305
- const paths = compilerOptions.paths;
306
- if (!paths || typeof paths !== 'object')
307
- continue;
308
- const baseUrl = typeof compilerOptions.baseUrl === 'string' ? compilerOptions.baseUrl : '.';
309
- const baseDir = path.resolve(searchDir, baseUrl);
310
- const rules = [];
311
- for (const [key, value] of Object.entries(paths)) {
312
- if (typeof key !== 'string' || !Array.isArray(value) || value.length === 0)
313
- continue;
314
- const hasWildcard = key.includes('*');
315
- const [prefix, suffix = ''] = key.split('*');
316
- const targets = value.filter(v => typeof v === 'string');
317
- if (targets.length === 0)
318
- continue;
319
- rules.push({ key, hasWildcard, prefix, suffix, targets });
320
- }
321
- if (rules.length === 0)
322
- continue;
323
- return { baseDir, rules };
324
- }
325
- return null;
326
- }
327
- async readLooseJson(filePath) {
328
- try {
329
- const text = await fs.readFile(filePath, 'utf-8');
330
- try {
331
- return JSON.parse(text);
332
- }
333
- catch {
334
- const noBlockComments = text.replace(/\/\*[\s\S]*?\*\//g, '');
335
- const noLineComments = noBlockComments.replace(/(^|\s)\/\/.*$/gm, '$1');
336
- const noTrailingCommas = noLineComments.replace(/,\s*([}\]])/g, '$1');
337
- return JSON.parse(noTrailingCommas);
338
- }
339
- }
340
- catch {
341
- return null;
342
- }
343
- }
344
- async checkPyImports(content, file, cwd, projectFiles, hallucinated) {
345
- const lines = content.split('\n');
346
- // Lazily resolve Python source roots and installed packages for this project
347
- const pySourceRoots = await this.findPythonSourceRoots(cwd, projectFiles);
348
- const pyInstalledPkgs = await this.loadPythonInstalledPackages(cwd);
349
- for (let i = 0; i < lines.length; i++) {
350
- const line = lines[i].trim();
351
- // Match: from X import Y, import X
352
- const fromMatch = line.match(/^from\s+([\w.]+)\s+import/);
353
- const importMatch = line.match(/^import\s+([\w.]+)/);
354
- const modulePath = fromMatch?.[1] || importMatch?.[1];
355
- if (!modulePath)
356
- continue;
357
- // Skip standard library modules
358
- if (isPythonStdlib(modulePath))
359
- continue;
360
- // Check if it's a relative project import
361
- if (modulePath.startsWith('.')) {
362
- // Python relative import: count leading dots to determine traversal depth.
363
- // N dots = go up (N-1) package levels from the file's directory.
364
- // from .types import X → 1 dot = current package (0 levels up)
365
- // from ..types import X → 2 dots = parent package (1 level up)
366
- // from ...client import X → 3 dots = grandparent package (2 levels up)
367
- const dotMatch = modulePath.match(/^(\.+)/);
368
- const dotCount = dotMatch ? dotMatch[1].length : 0;
369
- const moduleRest = modulePath.slice(dotCount); // e.g. 'client', 'types', 'models.permission', or '' for bare dots
370
- // Walk up (dotCount - 1) directories from the file's directory
371
- let baseDir = path.dirname(file);
372
- for (let level = 1; level < dotCount; level++) {
373
- const parent = path.dirname(baseDir);
374
- if (parent === baseDir)
375
- break; // at root
376
- baseDir = parent;
377
- }
378
- // If moduleRest is empty (e.g. `from . import X`), we're just referencing the package directory.
379
- // The imported names come from the `import` clause, not the module path — skip validation
380
- // since bare-dot package references are almost never hallucinated.
381
- if (!moduleRest)
382
- continue;
383
- // Resolve remaining module path within the target directory
384
- const moduleParts = moduleRest.replace(/\./g, '/');
385
- const candidateBase = path.join(baseDir, moduleParts).replace(/\\/g, '/');
386
- const candidates = [
387
- candidateBase + '.py',
388
- candidateBase + '/__init__.py',
389
- ];
390
- if (!candidates.some(c => projectFiles.has(c))) {
391
- hallucinated.push({
392
- file, line: i + 1, importPath: modulePath, type: 'python',
393
- reason: `Relative module '${modulePath}' not found in project`,
394
- });
395
- }
396
- }
397
- else {
398
- // Absolute import — check if it's a project module or installed package
399
- const topLevel = modulePath.split('.')[0];
400
- // Check if this is an installed package (from pyproject.toml, requirements.txt, etc.)
401
- if (pyInstalledPkgs.has(topLevel) || pyInstalledPkgs.has(topLevel.replace(/_/g, '-'))) {
402
- continue; // Known installed package — skip
403
- }
404
- // Check if it's a local module (searching across all Python source roots)
405
- let foundLocal = false;
406
- const searchRoots = ['', ...pySourceRoots]; // '' = project root
407
- for (const root of searchRoots) {
408
- const prefix = root ? root + '/' : '';
409
- const pyFile = prefix + topLevel + '.py';
410
- const pyInit = prefix + topLevel + '/__init__.py';
411
- const dirPrefix = prefix + topLevel + '/';
412
- const isLocalModule = projectFiles.has(pyFile) || projectFiles.has(pyInit) ||
413
- [...projectFiles].some(f => f.startsWith(dirPrefix));
414
- if (!isLocalModule)
415
- continue;
416
- foundLocal = true;
417
- // It's referencing a local module — verify the full path
418
- const fullModulePath = prefix + modulePath.replace(/\./g, '/');
419
- const candidates = [
420
- fullModulePath + '.py',
421
- fullModulePath + '/__init__.py',
422
- ];
423
- const exists = candidates.some(c => projectFiles.has(c));
424
- if (exists)
425
- break; // Found it — no issue
426
- if (modulePath.includes('.')) {
427
- // Only flag deep module paths that partially resolve
428
- hallucinated.push({
429
- file, line: i + 1, importPath: modulePath, type: 'python',
430
- reason: `Module '${modulePath}' partially resolves but target not found`,
431
- });
432
- }
433
- break;
434
- }
435
- // If not local and not stdlib and not installed, we can't easily verify
436
- // — skip silently rather than risk false positives
437
- }
438
- }
439
- }
440
- /**
441
- * Find Python source roots (directories that are on sys.path) by looking at
442
- * pyproject.toml, setup.cfg, or common patterns like src/ layouts.
443
- */
444
- async findPythonSourceRoots(cwd, projectFiles) {
445
- const roots = [];
446
- // Check pyproject.toml for package-dir or src layout hints
447
- const pyprojectPath = path.join(cwd, 'pyproject.toml');
448
- if (await fs.pathExists(pyprojectPath)) {
449
- try {
450
- const content = await fs.readFile(pyprojectPath, 'utf-8');
451
- // Match [tool.setuptools.packages.find] where = ["src"] or similar
452
- const whereMatch = content.match(/where\s*=\s*\[\s*"([^"]+)"\s*\]/);
453
- if (whereMatch) {
454
- roots.push(whereMatch[1]);
455
- }
456
- // Match package-dir patterns
457
- const pkgDirMatch = content.match(/package-dir\s*=\s*\{\s*""\s*:\s*"([^"]+)"\s*\}/);
458
- if (pkgDirMatch) {
459
- roots.push(pkgDirMatch[1]);
460
- }
461
- }
462
- catch { /* skip */ }
463
- }
464
- // Common source root patterns
465
- const commonSrcDirs = ['src', 'lib', 'app'];
466
- for (const dir of commonSrcDirs) {
467
- if ([...projectFiles].some(f => f.startsWith(dir + '/') && f.endsWith('.py'))) {
468
- if (!roots.includes(dir)) {
469
- roots.push(dir);
470
- }
471
- }
472
- }
473
- // Also scan for directories containing __init__.py that aren't at root level
474
- // (e.g. sdks/sandbox/python/src/ as a source root)
475
- const pyprojectFiles = [...projectFiles].filter(f => f.endsWith('/pyproject.toml') || f === 'pyproject.toml');
476
- for (const pf of pyprojectFiles) {
477
- const pfDir = path.dirname(pf);
478
- if (pfDir === '.')
479
- continue;
480
- // Check if this pyproject.toml has a src/ dir
481
- const srcDir = pfDir + '/src';
482
- if ([...projectFiles].some(f => f.startsWith(srcDir + '/') && f.endsWith('.py'))) {
483
- if (!roots.includes(srcDir)) {
484
- roots.push(srcDir);
485
- }
486
- }
487
- }
488
- return roots;
489
- }
490
- /**
491
- * Load installed Python package names from pyproject.toml dependencies,
492
- * requirements.txt, setup.cfg, or Pipfile.
493
- */
494
- async loadPythonInstalledPackages(cwd) {
495
- const packages = new Set();
496
- // Helper to normalize package names (PEP 503: lowercase, replace [-_.] with -)
497
- const normalize = (name) => name.toLowerCase().replace(/[-_.]+/g, '-');
498
- // Check pyproject.toml
499
- const pyprojectPath = path.join(cwd, 'pyproject.toml');
500
- if (await fs.pathExists(pyprojectPath)) {
501
- try {
502
- const content = await fs.readFile(pyprojectPath, 'utf-8');
503
- // Match dependencies = ["fastapi>=0.100", "kubernetes", ...]
504
- const depsMatch = content.match(/dependencies\s*=\s*\[([\s\S]*?)\]/g);
505
- if (depsMatch) {
506
- for (const block of depsMatch) {
507
- const pkgs = block.match(/"([^">=<!\s\[]+)/g);
508
- if (pkgs) {
509
- for (const pkg of pkgs) {
510
- const name = pkg.replace(/^"/, '').split(/[>=<!\[]/)[0].trim();
511
- if (name && name !== 'dependencies') {
512
- packages.add(normalize(name));
513
- // Also add the import name (replace - with _)
514
- packages.add(name.replace(/-/g, '_').toLowerCase());
515
- }
516
- }
517
- }
518
- }
519
- }
520
- // optional-dependencies
521
- const optDepsMatch = content.match(/optional-dependencies\s*\]([\s\S]*?)(?:\n\[|\n$)/);
522
- if (optDepsMatch) {
523
- const pkgs = optDepsMatch[1].match(/"([^">=<!\s\[]+)/g);
524
- if (pkgs) {
525
- for (const pkg of pkgs) {
526
- const name = pkg.replace(/^"/, '').split(/[>=<!\[]/)[0].trim();
527
- if (name) {
528
- packages.add(normalize(name));
529
- packages.add(name.replace(/-/g, '_').toLowerCase());
530
- }
531
- }
532
- }
533
- }
534
- }
535
- catch { /* skip */ }
536
- }
537
- // Check requirements*.txt files
538
- const reqFiles = ['requirements.txt', 'requirements-dev.txt', 'requirements_dev.txt'];
539
- for (const reqFile of reqFiles) {
540
- const reqPath = path.join(cwd, reqFile);
541
- if (await fs.pathExists(reqPath)) {
542
- try {
543
- const content = await fs.readFile(reqPath, 'utf-8');
544
- for (const line of content.split('\n')) {
545
- const trimmed = line.trim();
546
- if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-'))
547
- continue;
548
- const name = trimmed.split(/[>=<!\[;@\s]/)[0].trim();
549
- if (name) {
550
- packages.add(normalize(name));
551
- packages.add(name.replace(/-/g, '_').toLowerCase());
552
- }
553
- }
554
- }
555
- catch { /* skip */ }
556
- }
557
- }
558
- // Also scan subdirectories for pyproject.toml (monorepo support)
559
- const subPyprojects = ['server/pyproject.toml', 'api/pyproject.toml', 'backend/pyproject.toml'];
560
- for (const sub of subPyprojects) {
561
- const subPath = path.join(cwd, sub);
562
- if (await fs.pathExists(subPath)) {
563
- try {
564
- const content = await fs.readFile(subPath, 'utf-8');
565
- const depsMatch = content.match(/dependencies\s*=\s*\[([\s\S]*?)\]/g);
566
- if (depsMatch) {
567
- for (const block of depsMatch) {
568
- const pkgs = block.match(/"([^">=<!\s\[]+)/g);
569
- if (pkgs) {
570
- for (const pkg of pkgs) {
571
- const name = pkg.replace(/^"/, '').split(/[>=<!\[]/)[0].trim();
572
- if (name && name !== 'dependencies') {
573
- packages.add(normalize(name));
574
- packages.add(name.replace(/-/g, '_').toLowerCase());
575
- }
576
- }
577
- }
578
- }
579
- }
580
- }
581
- catch { /* skip */ }
582
- }
583
- }
584
- return packages;
585
- }
586
- resolveRelativeImport(fromFile, importPath, projectFiles) {
587
- const dir = path.dirname(fromFile);
588
- const resolved = path.join(dir, importPath).replace(/\\/g, '/');
589
- const candidates = this.buildImportCandidates(resolved);
590
- return candidates.some(c => projectFiles.has(c));
591
- }
592
- extractPackageName(importPath) {
593
- // Scoped packages: @scope/package/... → @scope/package
594
- if (importPath.startsWith('@')) {
595
- const parts = importPath.split('/');
596
- return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : importPath;
597
- }
598
- // Regular packages: package/... → package
599
- return importPath.split('/')[0];
600
- }
601
- shouldIgnore(importPath) {
602
- return this.config.ignore_patterns.some(pattern => new RegExp(pattern).test(importPath));
603
- }
604
- /**
605
- * Build candidate source paths for an import.
606
- * Handles ESM-style TS source imports like "./foo.js" that map to "./foo.ts" pre-build.
607
- */
608
- buildImportCandidates(resolvedPath) {
609
- const extension = path.extname(resolvedPath).toLowerCase();
610
- const sourceExtensions = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.d.ts'];
611
- const runtimeExtensions = new Set(['.js', '.jsx', '.mjs', '.cjs']);
612
- let candidates = [];
613
- if (runtimeExtensions.has(extension)) {
614
- const withoutExt = resolvedPath.slice(0, -extension.length);
615
- candidates = [
616
- ...sourceExtensions.map(ext => withoutExt + ext),
617
- ...sourceExtensions.map(ext => `${withoutExt}/index${ext}`),
618
- resolvedPath,
619
- `${resolvedPath}/index`,
620
- ];
621
- }
622
- else if (extension) {
623
- candidates = [resolvedPath, `${resolvedPath}/index`];
624
- }
625
- else {
626
- candidates = [
627
- ...sourceExtensions.map(ext => resolvedPath + ext),
628
- ...sourceExtensions.map(ext => `${resolvedPath}/index${ext}`),
629
- ];
630
- }
631
- return [...new Set(candidates)];
632
- }
633
- shouldSkipFile(file) {
634
- const normalized = file.replace(/\\/g, '/');
635
- return (normalized.includes('/examples/') ||
636
- normalized.includes('/studio-dist/') ||
637
- normalized.includes('/__tests__/') ||
638
- /\.test\.[^.]+$/i.test(normalized) ||
639
- /\.spec\.[^.]+$/i.test(normalized));
640
- }
641
- }