@rigour-labs/core 5.0.0 → 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.
- package/README.md +9 -1
- package/dist/gates/agent-team.d.ts +0 -1
- package/dist/gates/agent-team.js +0 -1
- package/dist/gates/checkpoint.d.ts +0 -2
- package/dist/gates/checkpoint.js +0 -2
- package/dist/gates/context-window-artifacts.d.ts +6 -2
- package/dist/gates/context-window-artifacts.js +107 -31
- package/dist/gates/deep-analysis.d.ts +2 -0
- package/dist/gates/deep-analysis.js +41 -11
- package/dist/gates/dependency.d.ts +0 -2
- package/dist/gates/dependency.js +23 -5
- package/dist/gates/deprecated-apis.d.ts +0 -2
- package/dist/gates/deprecated-apis.js +33 -20
- package/dist/gates/duplication-drift/index.d.ts +61 -0
- package/dist/gates/duplication-drift/index.js +240 -0
- package/dist/gates/duplication-drift/similarity.d.ts +68 -0
- package/dist/gates/duplication-drift/similarity.js +177 -0
- package/dist/gates/duplication-drift/tokenizer.d.ts +55 -0
- package/dist/gates/duplication-drift/tokenizer.js +195 -0
- package/dist/gates/frontend-secret-exposure.d.ts +0 -3
- package/dist/gates/frontend-secret-exposure.js +1 -114
- package/dist/gates/frontend-secret-patterns.d.ts +33 -0
- package/dist/gates/frontend-secret-patterns.js +119 -0
- package/dist/gates/{hallucinated-imports.d.ts → hallucinated-imports/index.d.ts} +2 -29
- package/dist/gates/hallucinated-imports/index.js +174 -0
- package/dist/gates/hallucinated-imports/js-resolver.d.ts +45 -0
- package/dist/gates/hallucinated-imports/js-resolver.js +320 -0
- package/dist/gates/hallucinated-imports/manifest-discovery.d.ts +28 -0
- package/dist/gates/hallucinated-imports/manifest-discovery.js +114 -0
- package/dist/gates/hallucinated-imports/python-resolver.d.ts +24 -0
- package/dist/gates/hallucinated-imports/python-resolver.js +306 -0
- package/dist/gates/hallucinated-imports-lang.d.ts +2 -2
- package/dist/gates/hallucinated-imports-lang.js +269 -34
- package/dist/gates/hallucinated-imports.test.js +1 -2
- package/dist/gates/inconsistent-error-handling.d.ts +0 -5
- package/dist/gates/inconsistent-error-handling.js +15 -144
- package/dist/gates/language-adapters/csharp-adapter.d.ts +16 -0
- package/dist/gates/language-adapters/csharp-adapter.js +211 -0
- package/dist/gates/language-adapters/go-adapter.d.ts +26 -0
- package/dist/gates/language-adapters/go-adapter.js +195 -0
- package/dist/gates/language-adapters/index.d.ts +15 -0
- package/dist/gates/language-adapters/index.js +16 -0
- package/dist/gates/language-adapters/java-adapter.d.ts +16 -0
- package/dist/gates/language-adapters/java-adapter.js +237 -0
- package/dist/gates/language-adapters/js-adapter.d.ts +26 -0
- package/dist/gates/language-adapters/js-adapter.js +279 -0
- package/dist/gates/language-adapters/python-adapter.d.ts +25 -0
- package/dist/gates/language-adapters/python-adapter.js +183 -0
- package/dist/gates/language-adapters/registry.d.ts +26 -0
- package/dist/gates/language-adapters/registry.js +65 -0
- package/dist/gates/language-adapters/ruby-adapter.d.ts +25 -0
- package/dist/gates/language-adapters/ruby-adapter.js +217 -0
- package/dist/gates/language-adapters/rust-adapter.d.ts +27 -0
- package/dist/gates/language-adapters/rust-adapter.js +235 -0
- package/dist/gates/language-adapters/types.d.ts +60 -0
- package/dist/gates/language-adapters/types.js +22 -0
- package/dist/gates/logic-drift-extractors.d.ts +15 -0
- package/dist/gates/logic-drift-extractors.js +34 -0
- package/dist/gates/logic-drift.d.ts +0 -30
- package/dist/gates/logic-drift.js +39 -129
- package/dist/gates/phantom-apis.d.ts +0 -2
- package/dist/gates/phantom-apis.js +49 -20
- package/dist/gates/promise-safety.d.ts +0 -1
- package/dist/gates/promise-safety.js +14 -2
- package/dist/gates/runner.js +51 -22
- package/dist/gates/security-patterns-data.d.ts +14 -0
- package/dist/gates/security-patterns-data.js +235 -0
- package/dist/gates/security-patterns.d.ts +17 -3
- package/dist/gates/security-patterns.js +80 -211
- package/dist/gates/side-effect-analysis/categorizer.d.ts +32 -0
- package/dist/gates/side-effect-analysis/categorizer.js +83 -0
- package/dist/gates/{side-effect-analysis.d.ts → side-effect-analysis/index.d.ts} +3 -5
- package/dist/gates/{side-effect-analysis.js → side-effect-analysis/index.js} +33 -45
- package/dist/gates/side-effect-analysis/scope-tracker.d.ts +37 -0
- package/dist/gates/side-effect-analysis/scope-tracker.js +40 -0
- package/dist/gates/side-effect-helpers/index.d.ts +4 -0
- package/dist/gates/side-effect-helpers/index.js +4 -0
- package/dist/gates/side-effect-helpers/pattern-detection.d.ts +123 -0
- package/dist/gates/{side-effect-helpers.js → side-effect-helpers/pattern-detection.js} +22 -468
- package/dist/gates/side-effect-helpers/resource-tracking.d.ts +80 -0
- package/dist/gates/side-effect-helpers/resource-tracking.js +281 -0
- package/dist/gates/side-effect-helpers/scope-analysis.d.ts +21 -0
- package/dist/gates/side-effect-helpers/scope-analysis.js +146 -0
- package/dist/gates/side-effect-helpers/types.d.ts +38 -0
- package/dist/gates/side-effect-helpers/types.js +41 -0
- package/dist/gates/side-effect-rules.d.ts +0 -1
- package/dist/gates/side-effect-rules.js +0 -1
- package/dist/gates/style-drift-rules.d.ts +86 -0
- package/dist/gates/style-drift-rules.js +103 -0
- package/dist/gates/style-drift.d.ts +7 -16
- package/dist/gates/style-drift.js +101 -119
- package/dist/gates/test-quality-matchers.d.ts +53 -0
- package/dist/gates/test-quality-matchers.js +86 -0
- package/dist/gates/test-quality.d.ts +0 -3
- package/dist/gates/test-quality.js +47 -44
- package/dist/hooks/checker.d.ts +0 -1
- package/dist/hooks/checker.js +1 -3
- package/dist/hooks/dlp-templates.d.ts +0 -1
- package/dist/hooks/dlp-templates.js +0 -4
- package/dist/hooks/index.d.ts +0 -2
- package/dist/hooks/index.js +0 -2
- package/dist/hooks/input-validator.d.ts +0 -1
- package/dist/hooks/input-validator.js +0 -1
- package/dist/hooks/input-validator.test.js +0 -1
- package/dist/hooks/standalone-checker.d.ts +0 -1
- package/dist/hooks/standalone-checker.js +0 -1
- package/dist/hooks/standalone-dlp-checker.d.ts +0 -1
- package/dist/hooks/standalone-dlp-checker.js +0 -1
- package/dist/hooks/templates.d.ts +6 -1
- package/dist/hooks/templates.js +6 -1
- package/dist/hooks/types.d.ts +1 -2
- package/dist/hooks/types.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/services/adaptive-thresholds.d.ts +0 -2
- package/dist/services/adaptive-thresholds.js +0 -2
- package/dist/services/filesystem-cache.d.ts +0 -1
- package/dist/services/filesystem-cache.js +0 -1
- package/dist/services/score-history.d.ts +0 -1
- package/dist/services/score-history.js +0 -1
- package/dist/services/temporal-drift.d.ts +1 -2
- package/dist/services/temporal-drift.js +7 -8
- package/dist/storage/db.d.ts +23 -7
- package/dist/storage/db.js +116 -55
- package/dist/storage/findings.d.ts +4 -3
- package/dist/storage/findings.js +13 -20
- package/dist/storage/local-memory.d.ts +4 -4
- package/dist/storage/local-memory.js +20 -22
- package/dist/storage/patterns.d.ts +5 -5
- package/dist/storage/patterns.js +20 -26
- package/dist/storage/scans.d.ts +6 -6
- package/dist/storage/scans.js +12 -21
- package/dist/types/index.d.ts +1 -0
- package/dist/utils/scanner.js +1 -1
- package/package.json +7 -8
- package/dist/gates/duplication-drift.d.ts +0 -128
- package/dist/gates/duplication-drift.js +0 -585
- package/dist/gates/hallucinated-imports.js +0 -641
- 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
|
-
}
|