@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
@@ -0,0 +1,174 @@
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
+ import { Gate } from '../base.js';
20
+ import { FileScanner } from '../../utils/scanner.js';
21
+ import { Logger } from '../../utils/logger.js';
22
+ import { languageAdapters } from '../language-adapters/index.js';
23
+ import fs from 'fs-extra';
24
+ import path from 'path';
25
+ import { checkGoImports, checkRubyImports, checkCSharpImports, checkRustImports, checkJavaKotlinImports } from '../hallucinated-imports-lang.js';
26
+ import { checkJSImports } from './js-resolver.js';
27
+ import { checkPyImports } from './python-resolver.js';
28
+ import { discoverWorkspacePackages, loadPackageJson } from './manifest-discovery.js';
29
+ export class HallucinatedImportsGate extends Gate {
30
+ config;
31
+ constructor(config = {}) {
32
+ super('hallucinated-imports', 'Hallucinated Import Detection');
33
+ this.config = {
34
+ enabled: config.enabled ?? true,
35
+ check_relative: config.check_relative ?? true,
36
+ check_packages: config.check_packages ?? true,
37
+ ignore_patterns: config.ignore_patterns ?? [
38
+ '\\.css$', '\\.scss$', '\\.less$', '\\.svg$', '\\.png$', '\\.jpg$',
39
+ '\\.json$', '\\.wasm$', '\\.graphql$', '\\.gql$',
40
+ ],
41
+ };
42
+ }
43
+ get provenance() { return 'ai-drift'; }
44
+ async run(context) {
45
+ if (!this.config.enabled)
46
+ return [];
47
+ const failures = [];
48
+ const hallucinated = [];
49
+ const defaultPatterns = ['**/*.{ts,js,tsx,jsx,py,go,rb,cs,rs,java,kt}'];
50
+ const scanPatterns = context.patterns || defaultPatterns;
51
+ const files = await FileScanner.findFiles({
52
+ cwd: context.cwd,
53
+ patterns: scanPatterns,
54
+ ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
55
+ '**/examples/**',
56
+ '**/studio-dist/**', '**/.next/**', '**/coverage/**',
57
+ '**/*.test.*', '**/*.spec.*', '**/__tests__/**',
58
+ '**/.venv/**', '**/venv/**', '**/vendor/**', '**/bin/Debug/**', '**/bin/Release/**', '**/obj/**',
59
+ '**/target/debug/**', '**/target/release/**',
60
+ '**/out/**', '**/.gradle/**', '**/gradle/**'],
61
+ });
62
+ const analyzableFiles = files.filter(file => !this.shouldSkipFile(file));
63
+ Logger.info(`Hallucinated Imports: Scanning ${analyzableFiles.length} files`);
64
+ const projectFiles = new Set(analyzableFiles.map(f => f.replace(/\\/g, '/')));
65
+ const allProjectFiles = new Set(files.map(f => f.replace(/\\/g, '/')));
66
+ const packageJson = await loadPackageJson(context.cwd);
67
+ const rootDeps = new Set([
68
+ ...Object.keys(packageJson?.dependencies || {}),
69
+ ...Object.keys(packageJson?.devDependencies || {}),
70
+ ...Object.keys(packageJson?.peerDependencies || {}),
71
+ ...Object.keys(packageJson?.optionalDependencies || {}),
72
+ ]);
73
+ const workspacePackages = await discoverWorkspacePackages(context.cwd, allProjectFiles);
74
+ for (const wp of workspacePackages)
75
+ rootDeps.add(wp);
76
+ const depCacheByDir = new Map();
77
+ const tsPathCacheByDir = new Map();
78
+ const hasNodeModules = await fs.pathExists(path.join(context.cwd, 'node_modules'));
79
+ for (const file of analyzableFiles) {
80
+ try {
81
+ const fullPath = path.join(context.cwd, file);
82
+ const content = await fs.readFile(fullPath, 'utf-8');
83
+ const ext = path.extname(file);
84
+ const adapter = languageAdapters.getAdapter(file);
85
+ if (!adapter)
86
+ continue;
87
+ switch (adapter.id) {
88
+ case 'js':
89
+ await checkJSImports(content, file, context.cwd, projectFiles, rootDeps, depCacheByDir, hasNodeModules, hallucinated, tsPathCacheByDir, (importPath) => this.shouldIgnore(importPath), (resolvedPath) => this.buildImportCandidates(resolvedPath), (fromFile, importPath, files) => this.resolveRelativeImport(fromFile, importPath, files), (importPath) => this.extractPackageName(importPath));
90
+ break;
91
+ case 'python':
92
+ await checkPyImports(content, file, context.cwd, projectFiles, hallucinated);
93
+ break;
94
+ case 'go':
95
+ checkGoImports(content, file, context.cwd, projectFiles, hallucinated);
96
+ break;
97
+ case 'ruby':
98
+ checkRubyImports(content, file, context.cwd, projectFiles, hallucinated);
99
+ break;
100
+ case 'csharp':
101
+ checkCSharpImports(content, file, context.cwd, projectFiles, hallucinated);
102
+ break;
103
+ case 'rust':
104
+ checkRustImports(content, file, context.cwd, projectFiles, hallucinated);
105
+ break;
106
+ case 'java':
107
+ checkJavaKotlinImports(content, file, ext, context.cwd, projectFiles, hallucinated);
108
+ break;
109
+ }
110
+ }
111
+ catch (e) { }
112
+ }
113
+ const byFile = new Map();
114
+ for (const h of hallucinated) {
115
+ const existing = byFile.get(h.file) || [];
116
+ existing.push(h);
117
+ byFile.set(h.file, existing);
118
+ }
119
+ for (const [file, imports] of byFile) {
120
+ const details = imports.map(i => ` L${i.line}: import '${i.importPath}' — ${i.reason}`).join('\n');
121
+ 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'));
122
+ }
123
+ return failures;
124
+ }
125
+ resolveRelativeImport(fromFile, importPath, projectFiles) {
126
+ const dir = path.dirname(fromFile);
127
+ const resolved = path.join(dir, importPath).replace(/\\/g, '/');
128
+ const candidates = this.buildImportCandidates(resolved);
129
+ return candidates.some(c => projectFiles.has(c));
130
+ }
131
+ extractPackageName(importPath) {
132
+ if (importPath.startsWith('@')) {
133
+ const parts = importPath.split('/');
134
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : importPath;
135
+ }
136
+ return importPath.split('/')[0];
137
+ }
138
+ shouldIgnore(importPath) {
139
+ return this.config.ignore_patterns.some(pattern => new RegExp(pattern).test(importPath));
140
+ }
141
+ buildImportCandidates(resolvedPath) {
142
+ const extension = path.extname(resolvedPath).toLowerCase();
143
+ const sourceExtensions = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.d.ts'];
144
+ const runtimeExtensions = new Set(['.js', '.jsx', '.mjs', '.cjs']);
145
+ let candidates = [];
146
+ if (runtimeExtensions.has(extension)) {
147
+ const withoutExt = resolvedPath.slice(0, -extension.length);
148
+ candidates = [
149
+ ...sourceExtensions.map(ext => withoutExt + ext),
150
+ ...sourceExtensions.map(ext => `${withoutExt}/index${ext}`),
151
+ resolvedPath,
152
+ `${resolvedPath}/index`,
153
+ ];
154
+ }
155
+ else if (extension) {
156
+ candidates = [resolvedPath, `${resolvedPath}/index`];
157
+ }
158
+ else {
159
+ candidates = [
160
+ ...sourceExtensions.map(ext => resolvedPath + ext),
161
+ ...sourceExtensions.map(ext => `${resolvedPath}/index${ext}`),
162
+ ];
163
+ }
164
+ return [...new Set(candidates)];
165
+ }
166
+ shouldSkipFile(file) {
167
+ const normalized = file.replace(/\\/g, '/');
168
+ return (normalized.includes('/examples/') ||
169
+ normalized.includes('/studio-dist/') ||
170
+ normalized.includes('/__tests__/') ||
171
+ /\.test\.[^.]+$/i.test(normalized) ||
172
+ /\.spec\.[^.]+$/i.test(normalized));
173
+ }
174
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * JavaScript/TypeScript import resolution for hallucinated-imports gate.
3
+ *
4
+ * Handles JS/TS-specific import validation including:
5
+ * - Relative imports with file resolution
6
+ * - TypeScript path alias resolution (tsconfig.json)
7
+ * - Workspace package discovery (pnpm, npm, yarn, lerna)
8
+ * - Node.js builtin checking
9
+ * - node_modules dependency verification
10
+ */
11
+ import { HallucinatedImport } from './index.js';
12
+ interface TsPathRule {
13
+ key: string;
14
+ hasWildcard: boolean;
15
+ prefix: string;
16
+ suffix: string;
17
+ targets: string[];
18
+ }
19
+ interface TsPathConfig {
20
+ baseDir: string;
21
+ rules: TsPathRule[];
22
+ }
23
+ /**
24
+ * Check JavaScript/TypeScript imports in a source file and add hallucinated findings.
25
+ */
26
+ export declare function checkJSImports(content: string, file: string, cwd: string, projectFiles: Set<string>, rootDeps: Set<string>, depCacheByDir: Map<string, Set<string>>, hasNodeModules: boolean, hallucinated: HallucinatedImport[], tsPathCacheByDir: Map<string, TsPathConfig | null>, shouldIgnore: (importPath: string) => boolean, buildImportCandidates: (resolvedPath: string) => string[], resolveRelativeImport: (fromFile: string, importPath: string, projectFiles: Set<string>) => boolean, extractPackageName: (importPath: string) => string): Promise<void>;
27
+ /**
28
+ * Collect all import statements from JavaScript/TypeScript file content.
29
+ * Handles import, export, require(), and dynamic import() statements.
30
+ */
31
+ export declare function collectJSImportSpecs(content: string, file: string): Array<{
32
+ importPath: string;
33
+ line: number;
34
+ }>;
35
+ /**
36
+ * Resolve JavaScript dependencies for a file by walking up the directory tree
37
+ * to find package.json files and merging their dependencies.
38
+ */
39
+ export declare function resolveJSDepsForFile(file: string, cwd: string, rootDeps: Set<string>, depCacheByDir: Map<string, Set<string>>): Promise<Set<string>>;
40
+ /**
41
+ * Resolve TypeScript path aliases from tsconfig.json or jsconfig.json.
42
+ * Returns true if resolved, false if alias exists but doesn't resolve, null if no alias found.
43
+ */
44
+ export declare function resolveTsPathAlias(file: string, importPath: string, cwd: string, projectFiles: Set<string>, tsPathCacheByDir: Map<string, TsPathConfig | null>): Promise<boolean | null>;
45
+ export {};
@@ -0,0 +1,320 @@
1
+ /**
2
+ * JavaScript/TypeScript import resolution for hallucinated-imports gate.
3
+ *
4
+ * Handles JS/TS-specific import validation including:
5
+ * - Relative imports with file resolution
6
+ * - TypeScript path alias resolution (tsconfig.json)
7
+ * - Workspace package discovery (pnpm, npm, yarn, lerna)
8
+ * - Node.js builtin checking
9
+ * - node_modules dependency verification
10
+ */
11
+ import fs from 'fs-extra';
12
+ import path from 'path';
13
+ import ts from 'typescript';
14
+ import { isNodeBuiltin } from '../hallucinated-imports-stdlib.js';
15
+ /**
16
+ * Check JavaScript/TypeScript imports in a source file and add hallucinated findings.
17
+ */
18
+ export async function checkJSImports(content, file, cwd, projectFiles, rootDeps, depCacheByDir, hasNodeModules, hallucinated, tsPathCacheByDir, shouldIgnore, buildImportCandidates, resolveRelativeImport, extractPackageName) {
19
+ const depsForFile = await resolveJSDepsForFile(file, cwd, rootDeps, depCacheByDir);
20
+ for (const spec of collectJSImportSpecs(content, file)) {
21
+ const { importPath, line } = spec;
22
+ if (!importPath || shouldIgnore(importPath))
23
+ continue;
24
+ if (importPath.startsWith('.')) {
25
+ const resolved = resolveRelativeImport(file, importPath, projectFiles);
26
+ if (!resolved) {
27
+ hallucinated.push({
28
+ file, line, importPath, type: 'relative',
29
+ reason: `File not found: ${importPath}`,
30
+ });
31
+ }
32
+ }
33
+ else {
34
+ const aliasResolution = await resolveTsPathAlias(file, importPath, cwd, projectFiles, tsPathCacheByDir);
35
+ if (aliasResolution === true)
36
+ continue;
37
+ if (aliasResolution === false) {
38
+ hallucinated.push({
39
+ file, line, importPath, type: 'package',
40
+ reason: `Path alias '${importPath}' does not resolve to a project file`,
41
+ });
42
+ continue;
43
+ }
44
+ const pkgName = extractPackageName(importPath);
45
+ if (isNodeBuiltin(pkgName))
46
+ continue;
47
+ if (!depsForFile.has(pkgName)) {
48
+ if (hasNodeModules) {
49
+ const pkgPath = path.join(cwd, 'node_modules', pkgName);
50
+ if (await fs.pathExists(pkgPath))
51
+ continue;
52
+ }
53
+ hallucinated.push({
54
+ file, line, importPath, type: 'package',
55
+ reason: `Package '${pkgName}' not in package.json dependencies`,
56
+ });
57
+ }
58
+ }
59
+ }
60
+ }
61
+ /**
62
+ * Collect all import statements from JavaScript/TypeScript file content.
63
+ * Handles import, export, require(), and dynamic import() statements.
64
+ */
65
+ export function collectJSImportSpecs(content, file) {
66
+ const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
67
+ const specs = [];
68
+ const add = (node, value) => {
69
+ if (!value)
70
+ return;
71
+ const line = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
72
+ specs.push({ importPath: value, line });
73
+ };
74
+ const visit = (node) => {
75
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
76
+ add(node, node.moduleSpecifier.text);
77
+ }
78
+ else if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
79
+ add(node, node.moduleSpecifier.text);
80
+ }
81
+ else if (ts.isCallExpression(node)) {
82
+ if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
83
+ const firstArg = node.arguments[0];
84
+ if (firstArg && ts.isStringLiteral(firstArg)) {
85
+ add(node, firstArg.text);
86
+ }
87
+ }
88
+ if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
89
+ const firstArg = node.arguments[0];
90
+ if (firstArg && ts.isStringLiteral(firstArg)) {
91
+ add(node, firstArg.text);
92
+ }
93
+ }
94
+ }
95
+ ts.forEachChild(node, visit);
96
+ };
97
+ ts.forEachChild(sourceFile, visit);
98
+ return specs;
99
+ }
100
+ /**
101
+ * Resolve JavaScript dependencies for a file by walking up the directory tree
102
+ * to find package.json files and merging their dependencies.
103
+ */
104
+ export async function resolveJSDepsForFile(file, cwd, rootDeps, depCacheByDir) {
105
+ const rootDir = path.resolve(cwd);
106
+ let currentDir = path.dirname(path.resolve(cwd, file));
107
+ while (currentDir.startsWith(rootDir)) {
108
+ const cached = depCacheByDir.get(currentDir);
109
+ if (cached)
110
+ return cached;
111
+ const packageJsonPath = path.join(currentDir, 'package.json');
112
+ if (await fs.pathExists(packageJsonPath)) {
113
+ try {
114
+ const packageJson = await fs.readJson(packageJsonPath);
115
+ const deps = new Set([
116
+ ...rootDeps,
117
+ ...Object.keys(packageJson?.dependencies || {}),
118
+ ...Object.keys(packageJson?.devDependencies || {}),
119
+ ...Object.keys(packageJson?.peerDependencies || {}),
120
+ ...Object.keys(packageJson?.optionalDependencies || {}),
121
+ ]);
122
+ depCacheByDir.set(currentDir, deps);
123
+ return deps;
124
+ }
125
+ catch {
126
+ depCacheByDir.set(currentDir, rootDeps);
127
+ return rootDeps;
128
+ }
129
+ }
130
+ const parent = path.dirname(currentDir);
131
+ if (parent === currentDir)
132
+ break;
133
+ currentDir = parent;
134
+ }
135
+ return rootDeps;
136
+ }
137
+ /**
138
+ * Resolve TypeScript path aliases from tsconfig.json or jsconfig.json.
139
+ * Returns true if resolved, false if alias exists but doesn't resolve, null if no alias found.
140
+ */
141
+ export async function resolveTsPathAlias(file, importPath, cwd, projectFiles, tsPathCacheByDir) {
142
+ const config = await resolveTsPathConfigForFile(file, cwd, tsPathCacheByDir);
143
+ if (!config || config.rules.length === 0)
144
+ return null;
145
+ for (const rule of config.rules) {
146
+ const wildcard = matchTsPathRule(rule, importPath);
147
+ if (wildcard === null)
148
+ continue;
149
+ for (const target of rule.targets) {
150
+ const candidatePattern = rule.hasWildcard ? target.replace('*', wildcard) : target;
151
+ if (resolveTsPathTarget(config.baseDir, candidatePattern, cwd, projectFiles)) {
152
+ return true;
153
+ }
154
+ }
155
+ return false;
156
+ }
157
+ return null;
158
+ }
159
+ /**
160
+ * Match a TypeScript path rule against an import path.
161
+ * Returns the wildcard portion if matched, null otherwise.
162
+ */
163
+ function matchTsPathRule(rule, importPath) {
164
+ if (!rule.hasWildcard) {
165
+ return importPath === rule.key ? '' : null;
166
+ }
167
+ if (!importPath.startsWith(rule.prefix) || !importPath.endsWith(rule.suffix)) {
168
+ return null;
169
+ }
170
+ return importPath.slice(rule.prefix.length, importPath.length - rule.suffix.length);
171
+ }
172
+ /**
173
+ * Check if a TypeScript path target resolves to an actual project file.
174
+ */
175
+ function resolveTsPathTarget(baseDir, candidatePattern, cwd, projectFiles) {
176
+ const absolute = path.resolve(baseDir, candidatePattern);
177
+ const relative = path.relative(cwd, absolute).replace(/\\/g, '/');
178
+ const normalized = relative.replace(/\/$/, '');
179
+ const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.d.ts'];
180
+ const candidates = [
181
+ ...extensions.map(ext => normalized + ext),
182
+ ...extensions.map(ext => `${normalized}/index${ext}`),
183
+ ];
184
+ return candidates.some(c => projectFiles.has(c));
185
+ }
186
+ /**
187
+ * Find and load TypeScript/JavaScript config for a file.
188
+ * Walks up the directory tree to find tsconfig.json or jsconfig.json.
189
+ */
190
+ async function resolveTsPathConfigForFile(file, cwd, tsPathCacheByDir) {
191
+ const rootDir = path.resolve(cwd);
192
+ let currentDir = path.dirname(path.resolve(cwd, file));
193
+ while (currentDir.startsWith(rootDir)) {
194
+ if (tsPathCacheByDir.has(currentDir)) {
195
+ const cached = tsPathCacheByDir.get(currentDir) || null;
196
+ if (cached)
197
+ return cached;
198
+ }
199
+ else {
200
+ const config = await loadTsPathConfig(currentDir);
201
+ tsPathCacheByDir.set(currentDir, config);
202
+ if (config)
203
+ return config;
204
+ }
205
+ const parent = path.dirname(currentDir);
206
+ if (parent === currentDir)
207
+ break;
208
+ currentDir = parent;
209
+ }
210
+ return null;
211
+ }
212
+ /**
213
+ * Load TypeScript path aliases from tsconfig.json, jsconfig.json, or tsconfig.base.json.
214
+ * Follows the extends chain to merge path mappings.
215
+ */
216
+ async function loadTsPathConfig(searchDir) {
217
+ const candidates = ['tsconfig.json', 'jsconfig.json', 'tsconfig.base.json'];
218
+ for (const configName of candidates) {
219
+ const configPath = path.join(searchDir, configName);
220
+ if (!(await fs.pathExists(configPath)))
221
+ continue;
222
+ const mergedPaths = {};
223
+ let baseUrl = '.';
224
+ let resolvedBaseDir = searchDir;
225
+ await resolveExtendsChain(configPath, mergedPaths, (bu) => { baseUrl = bu; }, new Set());
226
+ const parsed = await readLooseJson(configPath);
227
+ const compilerOptions = parsed?.compilerOptions || {};
228
+ if (compilerOptions.paths) {
229
+ for (const [key, value] of Object.entries(compilerOptions.paths)) {
230
+ if (typeof key === 'string' && Array.isArray(value)) {
231
+ mergedPaths[key] = value;
232
+ }
233
+ }
234
+ }
235
+ if (compilerOptions.baseUrl)
236
+ baseUrl = compilerOptions.baseUrl;
237
+ if (Object.keys(mergedPaths).length === 0)
238
+ continue;
239
+ resolvedBaseDir = path.resolve(searchDir, baseUrl);
240
+ const rules = [];
241
+ for (const [key, value] of Object.entries(mergedPaths)) {
242
+ if (typeof key !== 'string' || !Array.isArray(value) || value.length === 0)
243
+ continue;
244
+ const hasWildcard = key.includes('*');
245
+ const [prefix, suffix = ''] = key.split('*');
246
+ const targets = value.filter(v => typeof v === 'string');
247
+ if (targets.length === 0)
248
+ continue;
249
+ rules.push({ key, hasWildcard, prefix, suffix, targets });
250
+ }
251
+ if (rules.length === 0)
252
+ continue;
253
+ return { baseDir: resolvedBaseDir, rules };
254
+ }
255
+ return null;
256
+ }
257
+ /**
258
+ * Recursively follow tsconfig `extends` chain to collect path mappings.
259
+ */
260
+ async function resolveExtendsChain(configPath, mergedPaths, setBaseUrl, visited) {
261
+ const resolved = path.resolve(configPath);
262
+ if (visited.has(resolved))
263
+ return;
264
+ visited.add(resolved);
265
+ const parsed = await readLooseJson(configPath);
266
+ if (!parsed)
267
+ return;
268
+ if (parsed.extends) {
269
+ const extendsPath = typeof parsed.extends === 'string' ? parsed.extends : null;
270
+ if (extendsPath) {
271
+ let resolvedExtends;
272
+ if (extendsPath.startsWith('.')) {
273
+ resolvedExtends = path.resolve(path.dirname(configPath), extendsPath);
274
+ }
275
+ else {
276
+ resolvedExtends = path.resolve(path.dirname(configPath), 'node_modules', extendsPath);
277
+ }
278
+ if (!resolvedExtends.endsWith('.json')) {
279
+ const withJson = resolvedExtends + '.json';
280
+ if (await fs.pathExists(withJson)) {
281
+ resolvedExtends = withJson;
282
+ }
283
+ }
284
+ if (await fs.pathExists(resolvedExtends)) {
285
+ await resolveExtendsChain(resolvedExtends, mergedPaths, setBaseUrl, visited);
286
+ }
287
+ }
288
+ }
289
+ const compilerOptions = parsed.compilerOptions || {};
290
+ if (compilerOptions.baseUrl) {
291
+ setBaseUrl(compilerOptions.baseUrl);
292
+ }
293
+ if (compilerOptions.paths) {
294
+ for (const [key, value] of Object.entries(compilerOptions.paths)) {
295
+ if (typeof key === 'string' && Array.isArray(value)) {
296
+ mergedPaths[key] = value;
297
+ }
298
+ }
299
+ }
300
+ }
301
+ /**
302
+ * Read JSON file with support for comments and trailing commas.
303
+ */
304
+ async function readLooseJson(filePath) {
305
+ try {
306
+ const text = await fs.readFile(filePath, 'utf-8');
307
+ try {
308
+ return JSON.parse(text);
309
+ }
310
+ catch {
311
+ const noBlockComments = text.replace(/\/\*[\s\S]*?\*\//g, '');
312
+ const noLineComments = noBlockComments.replace(/(^|\s)\/\/.*$/gm, '$1');
313
+ const noTrailingCommas = noLineComments.replace(/,\s*([}\]])/g, '$1');
314
+ return JSON.parse(noTrailingCommas);
315
+ }
316
+ }
317
+ catch {
318
+ return null;
319
+ }
320
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Manifest discovery and workspace package detection for hallucinated-imports gate.
3
+ *
4
+ * Handles:
5
+ * - Discovery of workspace packages from pnpm-workspace.yaml, package.json, lerna.json
6
+ * - Glob pattern matching for workspace packages
7
+ * - Dynamic scanning of project manifests for all supported languages
8
+ */
9
+ export interface LoadPackageJsonResult {
10
+ dependencies?: Record<string, string>;
11
+ devDependencies?: Record<string, string>;
12
+ peerDependencies?: Record<string, string>;
13
+ optionalDependencies?: Record<string, string>;
14
+ workspaces?: string[] | {
15
+ packages?: string[];
16
+ };
17
+ exports?: Record<string, any>;
18
+ name?: string;
19
+ }
20
+ /**
21
+ * Load package.json from the project root.
22
+ */
23
+ export declare function loadPackageJson(cwd: string): Promise<LoadPackageJsonResult>;
24
+ /**
25
+ * Discover workspace packages from pnpm-workspace.yaml, package.json workspaces, lerna.json.
26
+ * Returns a set of package names that are valid imports.
27
+ */
28
+ export declare function discoverWorkspacePackages(cwd: string, allProjectFiles: Set<string>): Promise<Set<string>>;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Manifest discovery and workspace package detection for hallucinated-imports gate.
3
+ *
4
+ * Handles:
5
+ * - Discovery of workspace packages from pnpm-workspace.yaml, package.json, lerna.json
6
+ * - Glob pattern matching for workspace packages
7
+ * - Dynamic scanning of project manifests for all supported languages
8
+ */
9
+ import fs from 'fs-extra';
10
+ import path from 'path';
11
+ /**
12
+ * Load package.json from the project root.
13
+ */
14
+ export async function loadPackageJson(cwd) {
15
+ try {
16
+ const packageJsonPath = path.join(cwd, 'package.json');
17
+ if (await fs.pathExists(packageJsonPath)) {
18
+ return await fs.readJson(packageJsonPath);
19
+ }
20
+ }
21
+ catch { /* skip */ }
22
+ return {};
23
+ }
24
+ /**
25
+ * Discover workspace packages from pnpm-workspace.yaml, package.json workspaces, lerna.json.
26
+ * Returns a set of package names that are valid imports.
27
+ */
28
+ export async function discoverWorkspacePackages(cwd, allProjectFiles) {
29
+ const packages = new Set();
30
+ // 1. Check pnpm-workspace.yaml
31
+ const pnpmWorkspacePath = path.join(cwd, 'pnpm-workspace.yaml');
32
+ if (await fs.pathExists(pnpmWorkspacePath)) {
33
+ try {
34
+ const content = await fs.readFile(pnpmWorkspacePath, 'utf-8');
35
+ const packagesMatch = content.match(/packages:\s*\n((?:\s+-\s+['"]?[^\n]+\n?)*)/);
36
+ if (packagesMatch) {
37
+ const globs = packagesMatch[1].match(/-\s+['"]?([^'"\n]+)/g);
38
+ if (globs) {
39
+ for (const g of globs) {
40
+ const pattern = g.replace(/-\s+['"]?/, '').replace(/['"]?\s*$/, '').trim();
41
+ await addWorkspacePackagesFromGlob(cwd, pattern, allProjectFiles, packages);
42
+ }
43
+ }
44
+ }
45
+ }
46
+ catch { /* skip */ }
47
+ }
48
+ // 2. Check root package.json workspaces field
49
+ try {
50
+ const rootPkg = await loadPackageJson(cwd);
51
+ const workspaces = rootPkg?.workspaces;
52
+ if (workspaces) {
53
+ const patterns = Array.isArray(workspaces) ? workspaces : workspaces.packages || [];
54
+ for (const pattern of patterns) {
55
+ await addWorkspacePackagesFromGlob(cwd, pattern, allProjectFiles, packages);
56
+ }
57
+ }
58
+ }
59
+ catch { /* skip */ }
60
+ // 3. Check lerna.json
61
+ const lernaPath = path.join(cwd, 'lerna.json');
62
+ if (await fs.pathExists(lernaPath)) {
63
+ try {
64
+ const lerna = await fs.readJson(lernaPath);
65
+ const patterns = lerna.packages || ['packages/*'];
66
+ for (const pattern of patterns) {
67
+ await addWorkspacePackagesFromGlob(cwd, pattern, allProjectFiles, packages);
68
+ }
69
+ }
70
+ catch { /* skip */ }
71
+ }
72
+ return packages;
73
+ }
74
+ /**
75
+ * Find package.json files matching a workspace glob pattern
76
+ * and add their names to the packages set.
77
+ */
78
+ async function addWorkspacePackagesFromGlob(cwd, pattern, allProjectFiles, packages) {
79
+ const normalizedPattern = pattern.replace(/\*\*?$/, '').replace(/\/$/, '');
80
+ for (const file of allProjectFiles) {
81
+ if (!file.endsWith('package.json'))
82
+ continue;
83
+ const dir = path.dirname(file);
84
+ if (dir.startsWith(normalizedPattern) || matchGlobPrefix(dir, pattern)) {
85
+ try {
86
+ const pkg = await fs.readJson(path.join(cwd, file));
87
+ if (pkg.name) {
88
+ packages.add(pkg.name);
89
+ if (pkg.exports) {
90
+ packages.add(pkg.name);
91
+ }
92
+ }
93
+ }
94
+ catch { /* skip */ }
95
+ }
96
+ }
97
+ }
98
+ /**
99
+ * Simple glob matching for workspace patterns.
100
+ * Handles "packages/*" and similar patterns.
101
+ */
102
+ function matchGlobPrefix(dirPath, pattern) {
103
+ const parts = pattern.split('/');
104
+ const dirParts = dirPath.split('/');
105
+ for (let i = 0; i < parts.length; i++) {
106
+ if (parts[i] === '*' || parts[i] === '**')
107
+ return true;
108
+ if (i >= dirParts.length)
109
+ return false;
110
+ if (parts[i] !== dirParts[i])
111
+ return false;
112
+ }
113
+ return true;
114
+ }