@rigour-labs/core 6.2.4 → 6.3.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 (180) hide show
  1. package/README.md +2 -0
  2. package/dist/deep/code-context.d.ts +28 -0
  3. package/dist/deep/code-context.js +93 -0
  4. package/dist/deep/code-context.test.d.ts +1 -0
  5. package/dist/deep/code-context.test.js +48 -0
  6. package/dist/deep/code-pass.d.ts +15 -0
  7. package/dist/deep/code-pass.js +44 -0
  8. package/dist/deep/code-review-prompt.d.ts +17 -0
  9. package/dist/deep/code-review-prompt.js +30 -0
  10. package/dist/deep/code-review-prompt.test.d.ts +1 -0
  11. package/dist/deep/code-review-prompt.test.js +20 -0
  12. package/dist/deep/code-verifier.d.ts +19 -0
  13. package/dist/deep/code-verifier.js +48 -0
  14. package/dist/deep/code-verifier.test.d.ts +1 -0
  15. package/dist/deep/code-verifier.test.js +46 -0
  16. package/dist/deep/fact-extractor.d.ts +7 -1
  17. package/dist/deep/fact-extractor.js +16 -8
  18. package/dist/deep/fact-extractor.test.js +34 -1
  19. package/dist/deep/facts-pass.d.ts +24 -0
  20. package/dist/deep/facts-pass.js +69 -0
  21. package/dist/deep/index.d.ts +6 -1
  22. package/dist/deep/index.js +5 -1
  23. package/dist/deep/parse-findings.d.ts +12 -0
  24. package/dist/deep/parse-findings.js +73 -0
  25. package/dist/deep/parse-findings.test.d.ts +1 -0
  26. package/dist/deep/parse-findings.test.js +25 -0
  27. package/dist/deep/prompts.d.ts +1 -1
  28. package/dist/deep/prompts.js +7 -8
  29. package/dist/deep/prompts.test.js +15 -0
  30. package/dist/discovery-signals.d.ts +27 -0
  31. package/dist/discovery-signals.js +96 -0
  32. package/dist/discovery.d.ts +1 -2
  33. package/dist/discovery.js +36 -49
  34. package/dist/discovery.test.js +84 -71
  35. package/dist/gates/context.js +7 -0
  36. package/dist/gates/deep-analysis.d.ts +33 -8
  37. package/dist/gates/deep-analysis.js +169 -306
  38. package/dist/gates/deep-analysis.test.d.ts +1 -0
  39. package/dist/gates/deep-analysis.test.js +86 -0
  40. package/dist/gates/deep-intent.d.ts +3 -0
  41. package/dist/gates/deep-intent.js +57 -0
  42. package/dist/gates/deep-intent.test.d.ts +1 -0
  43. package/dist/gates/deep-intent.test.js +51 -0
  44. package/dist/gates/deep-runner.d.ts +15 -0
  45. package/dist/gates/deep-runner.js +66 -0
  46. package/dist/gates/dependency.js +4 -0
  47. package/dist/gates/deprecated-apis-ast.d.ts +8 -0
  48. package/dist/gates/deprecated-apis-ast.js +25 -0
  49. package/dist/gates/deprecated-apis-rules-node.js +2 -8
  50. package/dist/gates/deprecated-apis.js +5 -1
  51. package/dist/gates/deprecated-apis.test.js +30 -0
  52. package/dist/gates/frontend-secret-exposure.js +5 -1
  53. package/dist/gates/frontend-secret-exposure.test.js +20 -0
  54. package/dist/gates/hallucinated-imports/framework-modules.d.ts +14 -0
  55. package/dist/gates/hallucinated-imports/framework-modules.js +75 -0
  56. package/dist/gates/hallucinated-imports/index.js +14 -6
  57. package/dist/gates/hallucinated-imports/js-resolver.d.ts +16 -12
  58. package/dist/gates/hallucinated-imports/js-resolver.js +65 -153
  59. package/dist/gates/hallucinated-imports/ts-path-target.js +16 -0
  60. package/dist/gates/hallucinated-imports/tsconfig-paths.d.ts +16 -0
  61. package/dist/gates/hallucinated-imports/tsconfig-paths.js +120 -0
  62. package/dist/gates/hallucinated-imports-sveltekit.test.d.ts +1 -0
  63. package/dist/gates/hallucinated-imports-sveltekit.test.js +108 -0
  64. package/dist/gates/logic-drift.js +7 -1
  65. package/dist/gates/runner.js +10 -40
  66. package/dist/gates/runner.test.js +37 -25
  67. package/dist/gates/scoped-gates.test.d.ts +1 -0
  68. package/dist/gates/scoped-gates.test.js +52 -0
  69. package/dist/gates/semantic-bugs.d.ts +23 -0
  70. package/dist/gates/semantic-bugs.js +48 -0
  71. package/dist/gates/semantic-bugs.test.d.ts +1 -0
  72. package/dist/gates/semantic-bugs.test.js +70 -0
  73. package/dist/gates/side-effect-analysis/index.js +11 -1
  74. package/dist/gates/side-effect-analysis.test.d.ts +1 -0
  75. package/dist/gates/side-effect-analysis.test.js +162 -0
  76. package/dist/gates/side-effect-helpers/pattern-detection.js +35 -7
  77. package/dist/gates/side-effect-helpers/resource-tracking.js +17 -10
  78. package/dist/gates/side-effect-helpers/scope-analysis.d.ts +6 -0
  79. package/dist/gates/side-effect-helpers/scope-analysis.js +20 -0
  80. package/dist/gates/style-drift.js +3 -0
  81. package/dist/index.d.ts +11 -2
  82. package/dist/index.js +10 -2
  83. package/dist/inference/http-download.d.ts +22 -0
  84. package/dist/inference/http-download.js +132 -0
  85. package/dist/inference/http-download.test.d.ts +1 -0
  86. package/dist/inference/http-download.test.js +109 -0
  87. package/dist/inference/index.d.ts +5 -1
  88. package/dist/inference/index.js +2 -1
  89. package/dist/inference/llama-engine.d.ts +27 -0
  90. package/dist/inference/llama-engine.js +152 -0
  91. package/dist/inference/llama-engine.test.d.ts +1 -0
  92. package/dist/inference/llama-engine.test.js +51 -0
  93. package/dist/inference/llama-process.d.ts +72 -0
  94. package/dist/inference/llama-process.js +126 -0
  95. package/dist/inference/llama-process.test.d.ts +1 -0
  96. package/dist/inference/llama-process.test.js +61 -0
  97. package/dist/inference/model-download.test.d.ts +1 -0
  98. package/dist/inference/model-download.test.js +73 -0
  99. package/dist/inference/model-manager.d.ts +14 -1
  100. package/dist/inference/model-manager.js +65 -80
  101. package/dist/inference/sidecar-provider.d.ts +19 -10
  102. package/dist/inference/sidecar-provider.js +125 -342
  103. package/dist/inference/types.d.ts +2 -0
  104. package/dist/semantic/ast.d.ts +29 -0
  105. package/dist/semantic/ast.js +122 -0
  106. package/dist/semantic/benchmark.d.ts +16 -0
  107. package/dist/semantic/benchmark.js +31 -0
  108. package/dist/semantic/benchmark.test.d.ts +1 -0
  109. package/dist/semantic/benchmark.test.js +18 -0
  110. package/dist/semantic/engine.d.ts +8 -0
  111. package/dist/semantic/engine.js +45 -0
  112. package/dist/semantic/intent/benchmark.d.ts +31 -0
  113. package/dist/semantic/intent/benchmark.js +74 -0
  114. package/dist/semantic/intent/intent-check.d.ts +43 -0
  115. package/dist/semantic/intent/intent-check.js +72 -0
  116. package/dist/semantic/intent/intent.test.d.ts +1 -0
  117. package/dist/semantic/intent/intent.test.js +46 -0
  118. package/dist/semantic/intent/parallel-reads.d.ts +23 -0
  119. package/dist/semantic/intent/parallel-reads.js +59 -0
  120. package/dist/semantic/learn/benchmark.d.ts +20 -0
  121. package/dist/semantic/learn/benchmark.js +71 -0
  122. package/dist/semantic/learn/compile.d.ts +4 -0
  123. package/dist/semantic/learn/compile.js +56 -0
  124. package/dist/semantic/learn/conditions.d.ts +22 -0
  125. package/dist/semantic/learn/conditions.js +93 -0
  126. package/dist/semantic/learn/extract.d.ts +30 -0
  127. package/dist/semantic/learn/extract.js +122 -0
  128. package/dist/semantic/learn/git-trees.d.ts +8 -0
  129. package/dist/semantic/learn/git-trees.js +32 -0
  130. package/dist/semantic/learn/identity.d.ts +18 -0
  131. package/dist/semantic/learn/identity.js +97 -0
  132. package/dist/semantic/learn/learn.d.ts +25 -0
  133. package/dist/semantic/learn/learn.js +109 -0
  134. package/dist/semantic/learn/learn.test.d.ts +1 -0
  135. package/dist/semantic/learn/learn.test.js +67 -0
  136. package/dist/semantic/learn/store.d.ts +4 -0
  137. package/dist/semantic/learn/store.js +39 -0
  138. package/dist/semantic/learn/types.d.ts +468 -0
  139. package/dist/semantic/learn/types.js +55 -0
  140. package/dist/semantic/origins.d.ts +42 -0
  141. package/dist/semantic/origins.js +149 -0
  142. package/dist/semantic/origins.test.d.ts +1 -0
  143. package/dist/semantic/origins.test.js +77 -0
  144. package/dist/semantic/program.d.ts +13 -0
  145. package/dist/semantic/program.js +62 -0
  146. package/dist/semantic/rules/credential-redirect.d.ts +2 -0
  147. package/dist/semantic/rules/credential-redirect.js +209 -0
  148. package/dist/semantic/rules/degraded-response-cached.d.ts +2 -0
  149. package/dist/semantic/rules/degraded-response-cached.js +271 -0
  150. package/dist/semantic/rules/in-memory-aggregation.d.ts +2 -0
  151. package/dist/semantic/rules/in-memory-aggregation.js +179 -0
  152. package/dist/semantic/summaries.d.ts +18 -0
  153. package/dist/semantic/summaries.js +33 -0
  154. package/dist/semantic/types.d.ts +32 -0
  155. package/dist/semantic/types.js +1 -0
  156. package/dist/storage/index.d.ts +1 -0
  157. package/dist/storage/index.js +1 -0
  158. package/dist/storage/team-diagnostics.d.ts +22 -0
  159. package/dist/storage/team-diagnostics.js +58 -0
  160. package/dist/storage/team-diagnostics.test.d.ts +1 -0
  161. package/dist/storage/team-diagnostics.test.js +51 -0
  162. package/dist/storage/team-schema.d.ts +6 -0
  163. package/dist/storage/team-schema.js +82 -0
  164. package/dist/storage/team-store.js +25 -72
  165. package/dist/templates/universal-config.js +4 -2
  166. package/dist/types/index.d.ts +114 -14
  167. package/dist/types/index.js +16 -2
  168. package/dist/utils/deep-status.d.ts +7 -0
  169. package/dist/utils/deep-status.js +11 -0
  170. package/dist/utils/diff.d.ts +3 -0
  171. package/dist/utils/diff.js +52 -0
  172. package/dist/utils/diff.test.d.ts +1 -0
  173. package/dist/utils/diff.test.js +38 -0
  174. package/dist/utils/scanner.js +13 -2
  175. package/dist/utils/scanner.test.js +22 -8
  176. package/dist/utils/scope.d.ts +15 -0
  177. package/dist/utils/scope.js +51 -0
  178. package/dist/utils/scope.test.d.ts +1 -0
  179. package/dist/utils/scope.test.js +38 -0
  180. package/package.json +6 -6
@@ -9,21 +9,26 @@
9
9
  * - node_modules dependency verification
10
10
  */
11
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[];
12
+ import { type SvelteKitRoots } from './framework-modules.js';
13
+ import { type TsPathConfig } from './tsconfig-paths.js';
14
+ /** Everything a JS/TS import check needs besides the file itself. */
15
+ export interface JsImportContext {
16
+ cwd: string;
17
+ projectFiles: Set<string>;
18
+ rootDeps: Set<string>;
19
+ depCacheByDir: Map<string, Set<string>>;
20
+ hasNodeModules: boolean;
21
+ hallucinated: HallucinatedImport[];
22
+ tsPathCacheByDir: Map<string, TsPathConfig | null>;
23
+ kitRoots: SvelteKitRoots;
24
+ shouldIgnore: (importPath: string) => boolean;
25
+ resolveRelativeImport: (fromFile: string, importPath: string, projectFiles: Set<string>) => boolean;
26
+ extractPackageName: (importPath: string) => string;
22
27
  }
23
28
  /**
24
29
  * Check JavaScript/TypeScript imports in a source file and add hallucinated findings.
25
30
  */
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>;
31
+ export declare function checkJSImports(content: string, file: string, ctx: JsImportContext): Promise<void>;
27
32
  /**
28
33
  * Collect all import statements from JavaScript/TypeScript file content.
29
34
  * Handles import, export, require(), and dynamic import() statements.
@@ -42,4 +47,3 @@ export declare function resolveJSDepsForFile(file: string, cwd: string, rootDeps
42
47
  * Returns true if resolved, false if alias exists but doesn't resolve, null if no alias found.
43
48
  */
44
49
  export declare function resolveTsPathAlias(file: string, importPath: string, cwd: string, projectFiles: Set<string>, tsPathCacheByDir: Map<string, TsPathConfig | null>): Promise<boolean | null>;
45
- export {};
@@ -13,51 +13,78 @@ import path from 'path';
13
13
  import ts from 'typescript';
14
14
  import { isNodeBuiltin } from '../hallucinated-imports-stdlib.js';
15
15
  import { resolveTsPathTarget } from './ts-path-target.js';
16
+ import { isSvelteKitProvided, resolveSvelteKitLib } from './framework-modules.js';
17
+ import { loadTsPathConfig } from './tsconfig-paths.js';
16
18
  /**
17
19
  * Check JavaScript/TypeScript imports in a source file and add hallucinated findings.
18
20
  */
19
- export async function checkJSImports(content, file, cwd, projectFiles, rootDeps, depCacheByDir, hasNodeModules, hallucinated, tsPathCacheByDir, shouldIgnore, buildImportCandidates, resolveRelativeImport, extractPackageName) {
20
- const depsForFile = await resolveJSDepsForFile(file, cwd, rootDeps, depCacheByDir);
21
- for (const spec of collectJSImportSpecs(content, file)) {
22
- const { importPath, line } = spec;
23
- if (!importPath || shouldIgnore(importPath))
21
+ export async function checkJSImports(content, file, ctx) {
22
+ const depsForFile = await resolveJSDepsForFile(file, ctx.cwd, ctx.rootDeps, ctx.depCacheByDir);
23
+ const kitRoot = await ctx.kitRoots.rootFor(file);
24
+ for (const { importPath, line } of collectJSImportSpecs(content, file)) {
25
+ if (!importPath || ctx.shouldIgnore(importPath))
24
26
  continue;
25
- if (importPath.startsWith('.')) {
26
- const resolved = resolveRelativeImport(file, importPath, projectFiles);
27
- if (!resolved) {
28
- hallucinated.push({
29
- file, line, importPath, type: 'relative',
30
- reason: `File not found: ${importPath}`,
31
- });
32
- }
27
+ if (kitRoot && isSvelteKitProvided(importPath))
28
+ continue;
29
+ const reason = importPath.startsWith('.')
30
+ ? await checkRelativeImport(file, importPath, ctx)
31
+ : await checkBareImport(file, importPath, depsForFile, kitRoot, ctx);
32
+ if (reason) {
33
+ ctx.hallucinated.push({
34
+ file, line, importPath,
35
+ type: importPath.startsWith('.') ? 'relative' : 'package',
36
+ reason,
37
+ });
33
38
  }
34
- else {
35
- const aliasResolution = await resolveTsPathAlias(file, importPath, cwd, projectFiles, tsPathCacheByDir);
36
- if (aliasResolution === true)
37
- continue;
38
- if (aliasResolution === false) {
39
- hallucinated.push({
40
- file, line, importPath, type: 'package',
41
- reason: `Path alias '${importPath}' does not resolve to a project file`,
42
- });
43
- continue;
44
- }
45
- const pkgName = extractPackageName(importPath);
46
- if (isNodeBuiltin(pkgName))
47
- continue;
48
- if (!depsForFile.has(pkgName)) {
49
- if (hasNodeModules) {
50
- const pkgPath = path.join(cwd, 'node_modules', pkgName);
51
- if (await fs.pathExists(pkgPath))
52
- continue;
53
- }
54
- hallucinated.push({
55
- file, line, importPath, type: 'package',
56
- reason: `Package '${pkgName}' not in package.json dependencies`,
57
- });
58
- }
39
+ }
40
+ }
41
+ async function checkRelativeImport(file, importPath, ctx) {
42
+ if (ctx.resolveRelativeImport(file, importPath, ctx.projectFiles))
43
+ return null;
44
+ const config = await resolveTsPathConfigForFile(file, ctx.cwd, ctx.tsPathCacheByDir);
45
+ if (config?.rootDirs.length && await resolveThroughRootDirs(file, importPath, config.rootDirs, ctx))
46
+ return null;
47
+ return `File not found: ${importPath}`;
48
+ }
49
+ /**
50
+ * tsconfig `rootDirs`: directories merged into one virtual root, so
51
+ * `./$types` next to `src/routes/+page.ts` can live in
52
+ * `.svelte-kit/types/src/routes/$types.d.ts`. Generated folders are often
53
+ * git-ignored and absent from the file list, so resolution checks the disk.
54
+ */
55
+ async function resolveThroughRootDirs(file, importPath, rootDirs, ctx) {
56
+ const target = path.resolve(path.dirname(path.resolve(ctx.cwd, file)), importPath);
57
+ for (const root of rootDirs) {
58
+ const relative = path.relative(root, target);
59
+ if (relative.startsWith('..') || path.isAbsolute(relative))
60
+ continue;
61
+ for (const other of rootDirs) {
62
+ if (other !== root && await resolveTsPathTarget(other, relative, ctx.cwd, ctx.projectFiles))
63
+ return true;
59
64
  }
60
65
  }
66
+ return false;
67
+ }
68
+ /** Reason the bare import is hallucinated, or null when it resolves. */
69
+ async function checkBareImport(file, importPath, depsForFile, kitRoot, ctx) {
70
+ const aliasResolution = await resolveTsPathAlias(file, importPath, ctx.cwd, ctx.projectFiles, ctx.tsPathCacheByDir);
71
+ if (aliasResolution === true)
72
+ return null;
73
+ if (aliasResolution === false)
74
+ return `Path alias '${importPath}' does not resolve to a project file`;
75
+ if (kitRoot) {
76
+ const lib = await resolveSvelteKitLib(importPath, kitRoot, ctx.cwd, ctx.projectFiles);
77
+ if (lib === true)
78
+ return null;
79
+ if (lib === false)
80
+ return `Path alias '${importPath}' does not resolve to a file under src/lib`;
81
+ }
82
+ const pkgName = ctx.extractPackageName(importPath);
83
+ if (isNodeBuiltin(pkgName) || depsForFile.has(pkgName))
84
+ return null;
85
+ if (ctx.hasNodeModules && await fs.pathExists(path.join(ctx.cwd, 'node_modules', pkgName)))
86
+ return null;
87
+ return `Package '${pkgName}' not in package.json dependencies`;
61
88
  }
62
89
  /**
63
90
  * Collect all import statements from JavaScript/TypeScript file content.
@@ -196,118 +223,3 @@ async function resolveTsPathConfigForFile(file, cwd, tsPathCacheByDir) {
196
223
  }
197
224
  return null;
198
225
  }
199
- /**
200
- * Load TypeScript path aliases from tsconfig.json, jsconfig.json, or tsconfig.base.json.
201
- * Follows the extends chain to merge path mappings.
202
- */
203
- async function loadTsPathConfig(searchDir) {
204
- const candidates = ['tsconfig.json', 'jsconfig.json', 'tsconfig.base.json'];
205
- for (const configName of candidates) {
206
- const configPath = path.join(searchDir, configName);
207
- if (!(await fs.pathExists(configPath)))
208
- continue;
209
- const mergedPaths = {};
210
- let baseUrl = '.';
211
- let resolvedBaseDir = searchDir;
212
- await resolveExtendsChain(configPath, mergedPaths, (bu) => { baseUrl = bu; }, new Set());
213
- const parsed = await readLooseJson(configPath);
214
- const compilerOptions = parsed?.compilerOptions || {};
215
- if (compilerOptions.paths) {
216
- for (const [key, value] of Object.entries(compilerOptions.paths)) {
217
- if (typeof key === 'string' && Array.isArray(value)) {
218
- mergedPaths[key] = value;
219
- }
220
- }
221
- }
222
- if (compilerOptions.baseUrl)
223
- baseUrl = compilerOptions.baseUrl;
224
- if (Object.keys(mergedPaths).length === 0)
225
- continue;
226
- resolvedBaseDir = path.resolve(searchDir, baseUrl);
227
- const rules = [];
228
- for (const [key, value] of Object.entries(mergedPaths)) {
229
- if (typeof key !== 'string' || !Array.isArray(value) || value.length === 0)
230
- continue;
231
- const hasWildcard = key.includes('*');
232
- const [prefix, suffix = ''] = key.split('*');
233
- const targets = value.filter(v => typeof v === 'string');
234
- if (targets.length === 0)
235
- continue;
236
- rules.push({ key, hasWildcard, prefix, suffix, targets });
237
- }
238
- rules.sort((left, right) => {
239
- if (left.hasWildcard !== right.hasWildcard)
240
- return left.hasWildcard ? 1 : -1;
241
- return (right.prefix.length + right.suffix.length)
242
- - (left.prefix.length + left.suffix.length);
243
- });
244
- if (rules.length === 0)
245
- continue;
246
- return { baseDir: resolvedBaseDir, rules };
247
- }
248
- return null;
249
- }
250
- /**
251
- * Recursively follow tsconfig `extends` chain to collect path mappings.
252
- */
253
- async function resolveExtendsChain(configPath, mergedPaths, setBaseUrl, visited) {
254
- const resolved = path.resolve(configPath);
255
- if (visited.has(resolved))
256
- return;
257
- visited.add(resolved);
258
- const parsed = await readLooseJson(configPath);
259
- if (!parsed)
260
- return;
261
- if (parsed.extends) {
262
- const extendsPath = typeof parsed.extends === 'string' ? parsed.extends : null;
263
- if (extendsPath) {
264
- let resolvedExtends;
265
- if (extendsPath.startsWith('.')) {
266
- resolvedExtends = path.resolve(path.dirname(configPath), extendsPath);
267
- }
268
- else {
269
- resolvedExtends = path.resolve(path.dirname(configPath), 'node_modules', extendsPath);
270
- }
271
- if (!resolvedExtends.endsWith('.json')) {
272
- const withJson = resolvedExtends + '.json';
273
- if (await fs.pathExists(withJson)) {
274
- resolvedExtends = withJson;
275
- }
276
- }
277
- if (await fs.pathExists(resolvedExtends)) {
278
- await resolveExtendsChain(resolvedExtends, mergedPaths, setBaseUrl, visited);
279
- }
280
- }
281
- }
282
- const compilerOptions = parsed.compilerOptions || {};
283
- if (compilerOptions.baseUrl) {
284
- setBaseUrl(compilerOptions.baseUrl);
285
- }
286
- if (compilerOptions.paths) {
287
- for (const [key, value] of Object.entries(compilerOptions.paths)) {
288
- if (typeof key === 'string' && Array.isArray(value)) {
289
- mergedPaths[key] = value;
290
- }
291
- }
292
- }
293
- }
294
- /**
295
- * Read JSON file with support for comments and trailing commas.
296
- */
297
- async function readLooseJson(filePath) {
298
- try {
299
- const text = await fs.readFile(filePath, 'utf-8');
300
- try {
301
- return JSON.parse(text);
302
- }
303
- catch {
304
- const noBlockComments = text.replace(/\/\*[\s\S]*?\*\//g, '');
305
- const noLineComments = noBlockComments.replace(/(^|\s)\/\/.*$/gm, '$1');
306
- const noTrailingCommas = noLineComments.replace(/,\s*([}\]])/g, '$1');
307
- return JSON.parse(noTrailingCommas);
308
- }
309
- }
310
- catch {
311
- return null;
312
- }
313
- }
@@ -1,6 +1,21 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  const sourceExtensions = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.d.ts'];
4
+ /** TypeScript ESM imports name the emitted file: `utils.js` for `utils.ts`. */
5
+ const RUNTIME_TO_SOURCE = {
6
+ '.js': ['.ts', '.tsx', '.d.ts'],
7
+ '.jsx': ['.tsx'],
8
+ '.mjs': ['.mts', '.d.mts'],
9
+ '.cjs': ['.cts', '.d.cts'],
10
+ };
11
+ function sourceSiblings(target) {
12
+ const extension = path.extname(target);
13
+ const sources = RUNTIME_TO_SOURCE[extension];
14
+ if (!sources)
15
+ return [];
16
+ const base = target.slice(0, -extension.length);
17
+ return sources.map(source => base + source);
18
+ }
4
19
  export async function resolveTsPathTarget(baseDir, candidatePattern, cwd, projectFiles) {
5
20
  const absolute = path.resolve(baseDir, candidatePattern);
6
21
  const relative = path.relative(cwd, absolute).replace(/\\/g, '/');
@@ -10,6 +25,7 @@ export async function resolveTsPathTarget(baseDir, candidatePattern, cwd, projec
10
25
  const candidates = [
11
26
  ...sourceExtensions.map(extension => normalized + extension),
12
27
  ...sourceExtensions.map(extension => `${normalized}/index${extension}`),
28
+ ...sourceSiblings(normalized),
13
29
  ];
14
30
  if (candidates.some(candidate => projectFiles.has(candidate)))
15
31
  return true;
@@ -0,0 +1,16 @@
1
+ export interface TsPathRule {
2
+ key: string;
3
+ hasWildcard: boolean;
4
+ prefix: string;
5
+ suffix: string;
6
+ targets: string[];
7
+ }
8
+ export interface TsPathConfig {
9
+ /** Directory `paths` targets are relative to: the effective baseUrl, else the declaring config's folder. */
10
+ baseDir: string;
11
+ rules: TsPathRule[];
12
+ /** Absolute `rootDirs`, each resolved against its declaring config. */
13
+ rootDirs: string[];
14
+ }
15
+ /** The first config in `searchDir` that defines `paths` or `rootDirs` (directly or inherited). */
16
+ export declare function loadTsPathConfig(searchDir: string): Promise<TsPathConfig | null>;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * tsconfig `paths`, `baseUrl` and `rootDirs`, resolved the way tsc does.
3
+ *
4
+ * Each option is resolved against the config that declares it, not the one
5
+ * that extends it: SvelteKit's generated `.svelte-kit/tsconfig.json` says
6
+ * `"$lib": ["../src/lib"]`, which means `<repo>/src/lib`, not `<repo>/../src/lib`.
7
+ * A child's `paths` replaces its parent's; later entries of an `extends`
8
+ * array override earlier ones.
9
+ */
10
+ import fs from 'fs-extra';
11
+ import path from 'path';
12
+ import { Logger } from '../../utils/logger.js';
13
+ const CONFIG_NAMES = ['tsconfig.json', 'jsconfig.json', 'tsconfig.base.json'];
14
+ /** The first config in `searchDir` that defines `paths` or `rootDirs` (directly or inherited). */
15
+ export async function loadTsPathConfig(searchDir) {
16
+ for (const name of CONFIG_NAMES) {
17
+ const configPath = path.join(searchDir, name);
18
+ if (!(await fs.pathExists(configPath)))
19
+ continue;
20
+ const effective = await readChain(configPath, new Set());
21
+ if (!effective.paths && !effective.rootDirs?.length)
22
+ continue;
23
+ return {
24
+ baseDir: effective.baseUrl ?? effective.paths?.dir ?? searchDir,
25
+ rules: toRules(effective.paths?.entries ?? {}),
26
+ rootDirs: effective.rootDirs ?? [],
27
+ };
28
+ }
29
+ return null;
30
+ }
31
+ async function readChain(configPath, visited) {
32
+ const resolved = path.resolve(configPath);
33
+ if (visited.has(resolved))
34
+ return {};
35
+ visited.add(resolved);
36
+ const parsed = await readLooseJson(resolved);
37
+ if (!parsed)
38
+ return {};
39
+ let effective = {};
40
+ for (const extendsPath of extendsList(parsed.extends)) {
41
+ const target = await resolveExtends(resolved, extendsPath);
42
+ if (target)
43
+ effective = { ...effective, ...await readChain(target, visited) };
44
+ else
45
+ warnMissingExtends(resolved, extendsPath);
46
+ }
47
+ const dir = path.dirname(resolved);
48
+ const options = parsed.compilerOptions ?? {};
49
+ if (typeof options.baseUrl === 'string')
50
+ effective.baseUrl = path.resolve(dir, options.baseUrl);
51
+ if (options.paths && typeof options.paths === 'object')
52
+ effective.paths = { entries: options.paths, dir };
53
+ if (Array.isArray(options.rootDirs)) {
54
+ effective.rootDirs = options.rootDirs.filter((r) => typeof r === 'string').map((r) => path.resolve(dir, r));
55
+ }
56
+ return effective;
57
+ }
58
+ function extendsList(value) {
59
+ if (typeof value === 'string')
60
+ return [value];
61
+ return Array.isArray(value) ? value.filter((v) => typeof v === 'string') : [];
62
+ }
63
+ async function resolveExtends(configPath, extendsPath) {
64
+ const base = extendsPath.startsWith('.') || path.isAbsolute(extendsPath)
65
+ ? path.resolve(path.dirname(configPath), extendsPath)
66
+ : path.resolve(path.dirname(configPath), 'node_modules', extendsPath);
67
+ for (const candidate of [base, `${base}.json`, path.join(base, 'tsconfig.json')]) {
68
+ if (await fs.pathExists(candidate) && (await fs.stat(candidate)).isFile())
69
+ return candidate;
70
+ }
71
+ return null;
72
+ }
73
+ function toRules(entries) {
74
+ const rules = [];
75
+ for (const [key, value] of Object.entries(entries)) {
76
+ if (!Array.isArray(value))
77
+ continue;
78
+ const targets = value.filter((v) => typeof v === 'string');
79
+ if (targets.length === 0)
80
+ continue;
81
+ const [prefix, suffix = ''] = key.split('*');
82
+ rules.push({ key, hasWildcard: key.includes('*'), prefix, suffix, targets });
83
+ }
84
+ // Exact keys first, then the longest wildcard prefix/suffix (tsc's precedence).
85
+ return rules.sort((left, right) => {
86
+ if (left.hasWildcard !== right.hasWildcard)
87
+ return left.hasWildcard ? 1 : -1;
88
+ return (right.prefix.length + right.suffix.length) - (left.prefix.length + left.suffix.length);
89
+ });
90
+ }
91
+ const warnedMissingExtends = new Set();
92
+ /**
93
+ * A missing `extends` target drops its options. Say so once, instead of
94
+ * reporting every aliased import as missing without a reason.
95
+ */
96
+ function warnMissingExtends(configPath, extendsPath) {
97
+ const key = `${configPath}\0${extendsPath}`;
98
+ if (warnedMissingExtends.has(key))
99
+ return;
100
+ warnedMissingExtends.add(key);
101
+ const hint = extendsPath.includes('.svelte-kit') ? ' Run `svelte-kit sync` first to generate it.' : '';
102
+ Logger.warn(`${path.basename(configPath)} extends '${extendsPath}', which does not exist; its path aliases are ignored.${hint}`);
103
+ }
104
+ /** JSON with comments and trailing commas, as tsconfig files allow. */
105
+ async function readLooseJson(filePath) {
106
+ try {
107
+ const text = await fs.readFile(filePath, 'utf-8');
108
+ try {
109
+ return JSON.parse(text);
110
+ }
111
+ catch {
112
+ const noBlockComments = text.replace(/\/\*[\s\S]*?\*\//g, '');
113
+ const noLineComments = noBlockComments.replace(/(^|\s)\/\/.*$/gm, '$1');
114
+ return JSON.parse(noLineComments.replace(/,\s*([}\]])/g, '$1'));
115
+ }
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * SvelteKit on a fresh clone: `.svelte-kit/` (and its tsconfig with the
3
+ * `$lib` alias) does not exist until `svelte-kit sync` runs.
4
+ */
5
+ import fs from 'fs';
6
+ import os from 'os';
7
+ import path from 'path';
8
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
9
+ import { HallucinatedImportsGate } from './hallucinated-imports/index.js';
10
+ function write(root, rel, body) {
11
+ fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
12
+ fs.writeFileSync(path.join(root, rel), body);
13
+ }
14
+ async function missingImports(cwd) {
15
+ const failures = await new HallucinatedImportsGate().run({ cwd });
16
+ return failures.map(f => f.details).join('\n');
17
+ }
18
+ describe('HallucinatedImportsGate — SvelteKit', () => {
19
+ let cwd;
20
+ beforeEach(() => {
21
+ cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'kit-imports-'));
22
+ write(cwd, 'package.json', JSON.stringify({ devDependencies: { '@sveltejs/kit': '2.0.0', svelte: '5.0.0' } }));
23
+ write(cwd, 'tsconfig.json', JSON.stringify({ extends: './.svelte-kit/tsconfig.json' }));
24
+ write(cwd, 'src/lib/format.ts', 'export const format = (n: number) => String(n);\n');
25
+ write(cwd, 'src/lib/components/badge.svelte', '<span><slot /></span>\n');
26
+ });
27
+ afterEach(() => { fs.rmSync(cwd, { recursive: true, force: true }); });
28
+ it('accepts kit virtual modules, generated $types, $lib and .svelte imports without svelte-kit sync', async () => {
29
+ write(cwd, 'src/routes/+page.ts', [
30
+ "import { goto } from '$app/navigation';",
31
+ "import { page } from '$app/state';",
32
+ "import { PUBLIC_API_URL } from '$env/static/public';",
33
+ "import { env } from '$env/dynamic/private';",
34
+ "import type { PageLoad } from './$types';",
35
+ "import { format } from '$lib/format';",
36
+ "import Badge from '$lib/components/badge.svelte';",
37
+ "import Local from '../lib/components/badge.svelte';",
38
+ 'export const load: PageLoad = () => ({ goto, page, PUBLIC_API_URL, env, format, Badge, Local });',
39
+ ].join('\n'));
40
+ expect(await missingImports(cwd)).toBe('');
41
+ });
42
+ it('still flags a $lib import that points at no file', async () => {
43
+ write(cwd, 'src/routes/+page.ts', "import { gone } from '$lib/does-not-exist';\nexport const x = gone;\n");
44
+ expect(await missingImports(cwd)).toContain("'$lib/does-not-exist'");
45
+ });
46
+ it('still flags a missing relative .svelte component', async () => {
47
+ write(cwd, 'src/routes/+page.ts', "import Missing from './missing.svelte';\nexport const x = Missing;\n");
48
+ expect(await missingImports(cwd)).toContain("'./missing.svelte'");
49
+ });
50
+ it('still flags a real missing package in a kit project', async () => {
51
+ write(cwd, 'src/routes/+page.ts', "import made from 'package-that-does-not-exist';\nexport const x = made;\n");
52
+ expect(await missingImports(cwd)).toContain("'package-that-does-not-exist'");
53
+ });
54
+ it('does not treat $app or $lib as provided outside a SvelteKit project', async () => {
55
+ write(cwd, 'package.json', JSON.stringify({ dependencies: {} }));
56
+ write(cwd, 'src/index.ts', "import { goto } from '$app/navigation';\nimport { format } from '$lib/format';\nexport const x = [goto, format];\n");
57
+ const details = await missingImports(cwd);
58
+ expect(details).toContain("'$app/navigation'");
59
+ expect(details).toContain("'$lib/format'");
60
+ });
61
+ it('resolves a $lib import written with the emitted .js extension to its .ts source', async () => {
62
+ write(cwd, 'src/lib/utils.ts', 'export const cn = (...c: string[]) => c.join(" ");\n');
63
+ write(cwd, 'src/lib/components/button-variants.ts', "import { cn } from '$lib/utils.js';\nexport const x = cn;\n");
64
+ expect(await missingImports(cwd)).toBe('');
65
+ });
66
+ it('resolves a tsconfig path alias written with .js to its .ts source (any framework)', async () => {
67
+ write(cwd, 'package.json', JSON.stringify({ dependencies: {} }));
68
+ write(cwd, 'tsconfig.json', JSON.stringify({ compilerOptions: { baseUrl: '.', paths: { '@/*': ['src/*'] } } }));
69
+ write(cwd, 'src/util/math.ts', 'export const add = (a: number, b: number) => a + b;\n');
70
+ write(cwd, 'src/index.ts', "import { add } from '@/util/math.js';\nexport const x = add;\n");
71
+ expect(await missingImports(cwd)).toBe('');
72
+ });
73
+ it('resolves inherited paths against the config that declares them (after svelte-kit sync)', async () => {
74
+ write(cwd, '.svelte-kit/tsconfig.json', JSON.stringify({
75
+ compilerOptions: { paths: { $lib: ['../src/lib'], '$lib/*': ['../src/lib/*'] }, rootDirs: ['..', './types'] },
76
+ }));
77
+ write(cwd, 'src/routes/+page.ts', "import { format } from '$lib/format';\nimport { gone } from '$lib/does-not-exist';\nexport const x = [format, gone];\n");
78
+ const details = await missingImports(cwd);
79
+ expect(details).not.toContain("'$lib/format'");
80
+ expect(details).toContain("'$lib/does-not-exist'");
81
+ });
82
+ });
83
+ describe('HallucinatedImportsGate — tsconfig inheritance and rootDirs', () => {
84
+ let cwd;
85
+ beforeEach(() => { cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'tsconfig-inherit-')); });
86
+ afterEach(() => { fs.rmSync(cwd, { recursive: true, force: true }); });
87
+ it('resolves paths inherited from a shared base config against that config (Nx-style)', async () => {
88
+ write(cwd, 'package.json', JSON.stringify({ dependencies: {} }));
89
+ write(cwd, 'libs/tsconfig.base.json', JSON.stringify({ compilerOptions: { baseUrl: '.', paths: { '@org/util': ['util/src/index.ts'] } } }));
90
+ write(cwd, 'libs/util/src/index.ts', 'export const util = 1;\n');
91
+ write(cwd, 'apps/web/tsconfig.json', JSON.stringify({ extends: '../../libs/tsconfig.base.json' }));
92
+ write(cwd, 'apps/web/src/main.ts', "import { util } from '@org/util';\nexport const x = util;\n");
93
+ expect(await missingImports(cwd)).toBe('');
94
+ });
95
+ it('resolves a relative import through rootDirs into a generated folder', async () => {
96
+ write(cwd, 'package.json', JSON.stringify({ dependencies: {} }));
97
+ write(cwd, 'tsconfig.json', JSON.stringify({ compilerOptions: { rootDirs: ['src', 'generated'] } }));
98
+ write(cwd, 'generated/schema.ts', 'export const schema = {};\n');
99
+ write(cwd, 'src/api.ts', "import { schema } from './schema';\nexport const x = schema;\n");
100
+ expect(await missingImports(cwd)).toBe('');
101
+ });
102
+ it('still flags a relative import found in no root', async () => {
103
+ write(cwd, 'package.json', JSON.stringify({ dependencies: {} }));
104
+ write(cwd, 'tsconfig.json', JSON.stringify({ compilerOptions: { rootDirs: ['src', 'generated'] } }));
105
+ write(cwd, 'src/api.ts', "import { schema } from './nowhere';\nexport const x = schema;\n");
106
+ expect(await missingImports(cwd)).toContain("'./nowhere'");
107
+ });
108
+ });
@@ -22,6 +22,7 @@ import { Logger } from '../utils/logger.js';
22
22
  import { languageAdapters } from './language-adapters/index.js';
23
23
  import { extractCallSequence, isDangerousMutation } from './logic-drift-extractors.js';
24
24
  import { isGitWorktree, resolveGitLogicBase } from './logic-drift-git-base.js';
25
+ import { isScoped } from '../utils/scope.js';
25
26
  import fs from 'fs-extra';
26
27
  import path from 'path';
27
28
  import crypto from 'crypto';
@@ -94,6 +95,11 @@ export class LogicDriftGate extends Gate {
94
95
  Logger.debug('Failed to load logic baseline');
95
96
  }
96
97
  }
98
+ // A scoped run sees only some files: compare against the baseline but
99
+ // never create or overwrite it, or the rest of the repo drops out of it.
100
+ const scoped = isScoped(context.patterns);
101
+ if (!previousBaseline && scoped)
102
+ return [];
97
103
  if (!previousBaseline) {
98
104
  // First scan: save baseline, no comparisons yet
99
105
  const baseline = {
@@ -151,7 +157,7 @@ export class LogicDriftGate extends Gate {
151
157
  lastUpdated: new Date().toISOString(),
152
158
  scanCount: previousBaseline.scanCount + 1,
153
159
  };
154
- if (!gitBase)
160
+ if (!gitBase && !scoped)
155
161
  await fs.writeJson(baselinePath, updatedBaseline, { spaces: 2 });
156
162
  if (failures.length > 0) {
157
163
  Logger.info(`Logic Drift: Found ${failures.length} logic mutations`);