@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.
- package/README.md +2 -0
- package/dist/deep/code-context.d.ts +28 -0
- package/dist/deep/code-context.js +93 -0
- package/dist/deep/code-context.test.d.ts +1 -0
- package/dist/deep/code-context.test.js +48 -0
- package/dist/deep/code-pass.d.ts +15 -0
- package/dist/deep/code-pass.js +44 -0
- package/dist/deep/code-review-prompt.d.ts +17 -0
- package/dist/deep/code-review-prompt.js +30 -0
- package/dist/deep/code-review-prompt.test.d.ts +1 -0
- package/dist/deep/code-review-prompt.test.js +20 -0
- package/dist/deep/code-verifier.d.ts +19 -0
- package/dist/deep/code-verifier.js +48 -0
- package/dist/deep/code-verifier.test.d.ts +1 -0
- package/dist/deep/code-verifier.test.js +46 -0
- package/dist/deep/fact-extractor.d.ts +7 -1
- package/dist/deep/fact-extractor.js +16 -8
- package/dist/deep/fact-extractor.test.js +34 -1
- package/dist/deep/facts-pass.d.ts +24 -0
- package/dist/deep/facts-pass.js +69 -0
- package/dist/deep/index.d.ts +6 -1
- package/dist/deep/index.js +5 -1
- package/dist/deep/parse-findings.d.ts +12 -0
- package/dist/deep/parse-findings.js +73 -0
- package/dist/deep/parse-findings.test.d.ts +1 -0
- package/dist/deep/parse-findings.test.js +25 -0
- package/dist/deep/prompts.d.ts +1 -1
- package/dist/deep/prompts.js +7 -8
- package/dist/deep/prompts.test.js +15 -0
- package/dist/discovery-signals.d.ts +27 -0
- package/dist/discovery-signals.js +96 -0
- package/dist/discovery.d.ts +1 -2
- package/dist/discovery.js +36 -49
- package/dist/discovery.test.js +84 -71
- package/dist/gates/context.js +7 -0
- package/dist/gates/deep-analysis.d.ts +33 -8
- package/dist/gates/deep-analysis.js +169 -306
- package/dist/gates/deep-analysis.test.d.ts +1 -0
- package/dist/gates/deep-analysis.test.js +86 -0
- package/dist/gates/deep-intent.d.ts +3 -0
- package/dist/gates/deep-intent.js +57 -0
- package/dist/gates/deep-intent.test.d.ts +1 -0
- package/dist/gates/deep-intent.test.js +51 -0
- package/dist/gates/deep-runner.d.ts +15 -0
- package/dist/gates/deep-runner.js +66 -0
- package/dist/gates/dependency.js +4 -0
- package/dist/gates/deprecated-apis-ast.d.ts +8 -0
- package/dist/gates/deprecated-apis-ast.js +25 -0
- package/dist/gates/deprecated-apis-rules-node.js +2 -8
- package/dist/gates/deprecated-apis.js +5 -1
- package/dist/gates/deprecated-apis.test.js +30 -0
- package/dist/gates/frontend-secret-exposure.js +5 -1
- package/dist/gates/frontend-secret-exposure.test.js +20 -0
- package/dist/gates/hallucinated-imports/framework-modules.d.ts +14 -0
- package/dist/gates/hallucinated-imports/framework-modules.js +75 -0
- package/dist/gates/hallucinated-imports/index.js +14 -6
- package/dist/gates/hallucinated-imports/js-resolver.d.ts +16 -12
- package/dist/gates/hallucinated-imports/js-resolver.js +65 -153
- package/dist/gates/hallucinated-imports/ts-path-target.js +16 -0
- package/dist/gates/hallucinated-imports/tsconfig-paths.d.ts +16 -0
- package/dist/gates/hallucinated-imports/tsconfig-paths.js +120 -0
- package/dist/gates/hallucinated-imports-sveltekit.test.d.ts +1 -0
- package/dist/gates/hallucinated-imports-sveltekit.test.js +108 -0
- package/dist/gates/logic-drift.js +7 -1
- package/dist/gates/runner.js +10 -40
- package/dist/gates/runner.test.js +37 -25
- package/dist/gates/scoped-gates.test.d.ts +1 -0
- package/dist/gates/scoped-gates.test.js +52 -0
- package/dist/gates/semantic-bugs.d.ts +23 -0
- package/dist/gates/semantic-bugs.js +48 -0
- package/dist/gates/semantic-bugs.test.d.ts +1 -0
- package/dist/gates/semantic-bugs.test.js +70 -0
- package/dist/gates/side-effect-analysis/index.js +11 -1
- package/dist/gates/side-effect-analysis.test.d.ts +1 -0
- package/dist/gates/side-effect-analysis.test.js +162 -0
- package/dist/gates/side-effect-helpers/pattern-detection.js +35 -7
- package/dist/gates/side-effect-helpers/resource-tracking.js +17 -10
- package/dist/gates/side-effect-helpers/scope-analysis.d.ts +6 -0
- package/dist/gates/side-effect-helpers/scope-analysis.js +20 -0
- package/dist/gates/style-drift.js +3 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.js +10 -2
- package/dist/inference/http-download.d.ts +22 -0
- package/dist/inference/http-download.js +132 -0
- package/dist/inference/http-download.test.d.ts +1 -0
- package/dist/inference/http-download.test.js +109 -0
- package/dist/inference/index.d.ts +5 -1
- package/dist/inference/index.js +2 -1
- package/dist/inference/llama-engine.d.ts +27 -0
- package/dist/inference/llama-engine.js +152 -0
- package/dist/inference/llama-engine.test.d.ts +1 -0
- package/dist/inference/llama-engine.test.js +51 -0
- package/dist/inference/llama-process.d.ts +72 -0
- package/dist/inference/llama-process.js +126 -0
- package/dist/inference/llama-process.test.d.ts +1 -0
- package/dist/inference/llama-process.test.js +61 -0
- package/dist/inference/model-download.test.d.ts +1 -0
- package/dist/inference/model-download.test.js +73 -0
- package/dist/inference/model-manager.d.ts +14 -1
- package/dist/inference/model-manager.js +65 -80
- package/dist/inference/sidecar-provider.d.ts +19 -10
- package/dist/inference/sidecar-provider.js +125 -342
- package/dist/inference/types.d.ts +2 -0
- package/dist/semantic/ast.d.ts +29 -0
- package/dist/semantic/ast.js +122 -0
- package/dist/semantic/benchmark.d.ts +16 -0
- package/dist/semantic/benchmark.js +31 -0
- package/dist/semantic/benchmark.test.d.ts +1 -0
- package/dist/semantic/benchmark.test.js +18 -0
- package/dist/semantic/engine.d.ts +8 -0
- package/dist/semantic/engine.js +45 -0
- package/dist/semantic/intent/benchmark.d.ts +31 -0
- package/dist/semantic/intent/benchmark.js +74 -0
- package/dist/semantic/intent/intent-check.d.ts +43 -0
- package/dist/semantic/intent/intent-check.js +72 -0
- package/dist/semantic/intent/intent.test.d.ts +1 -0
- package/dist/semantic/intent/intent.test.js +46 -0
- package/dist/semantic/intent/parallel-reads.d.ts +23 -0
- package/dist/semantic/intent/parallel-reads.js +59 -0
- package/dist/semantic/learn/benchmark.d.ts +20 -0
- package/dist/semantic/learn/benchmark.js +71 -0
- package/dist/semantic/learn/compile.d.ts +4 -0
- package/dist/semantic/learn/compile.js +56 -0
- package/dist/semantic/learn/conditions.d.ts +22 -0
- package/dist/semantic/learn/conditions.js +93 -0
- package/dist/semantic/learn/extract.d.ts +30 -0
- package/dist/semantic/learn/extract.js +122 -0
- package/dist/semantic/learn/git-trees.d.ts +8 -0
- package/dist/semantic/learn/git-trees.js +32 -0
- package/dist/semantic/learn/identity.d.ts +18 -0
- package/dist/semantic/learn/identity.js +97 -0
- package/dist/semantic/learn/learn.d.ts +25 -0
- package/dist/semantic/learn/learn.js +109 -0
- package/dist/semantic/learn/learn.test.d.ts +1 -0
- package/dist/semantic/learn/learn.test.js +67 -0
- package/dist/semantic/learn/store.d.ts +4 -0
- package/dist/semantic/learn/store.js +39 -0
- package/dist/semantic/learn/types.d.ts +468 -0
- package/dist/semantic/learn/types.js +55 -0
- package/dist/semantic/origins.d.ts +42 -0
- package/dist/semantic/origins.js +149 -0
- package/dist/semantic/origins.test.d.ts +1 -0
- package/dist/semantic/origins.test.js +77 -0
- package/dist/semantic/program.d.ts +13 -0
- package/dist/semantic/program.js +62 -0
- package/dist/semantic/rules/credential-redirect.d.ts +2 -0
- package/dist/semantic/rules/credential-redirect.js +209 -0
- package/dist/semantic/rules/degraded-response-cached.d.ts +2 -0
- package/dist/semantic/rules/degraded-response-cached.js +271 -0
- package/dist/semantic/rules/in-memory-aggregation.d.ts +2 -0
- package/dist/semantic/rules/in-memory-aggregation.js +179 -0
- package/dist/semantic/summaries.d.ts +18 -0
- package/dist/semantic/summaries.js +33 -0
- package/dist/semantic/types.d.ts +32 -0
- package/dist/semantic/types.js +1 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/team-diagnostics.d.ts +22 -0
- package/dist/storage/team-diagnostics.js +58 -0
- package/dist/storage/team-diagnostics.test.d.ts +1 -0
- package/dist/storage/team-diagnostics.test.js +51 -0
- package/dist/storage/team-schema.d.ts +6 -0
- package/dist/storage/team-schema.js +82 -0
- package/dist/storage/team-store.js +25 -72
- package/dist/templates/universal-config.js +4 -2
- package/dist/types/index.d.ts +114 -14
- package/dist/types/index.js +16 -2
- package/dist/utils/deep-status.d.ts +7 -0
- package/dist/utils/deep-status.js +11 -0
- package/dist/utils/diff.d.ts +3 -0
- package/dist/utils/diff.js +52 -0
- package/dist/utils/diff.test.d.ts +1 -0
- package/dist/utils/diff.test.js +38 -0
- package/dist/utils/scanner.js +13 -2
- package/dist/utils/scanner.test.js +22 -8
- package/dist/utils/scope.d.ts +15 -0
- package/dist/utils/scope.js +51 -0
- package/dist/utils/scope.test.d.ts +1 -0
- package/dist/utils/scope.test.js +38 -0
- package/package.json +6 -6
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
const provider = vi.hoisted(() => ({
|
|
6
|
+
setup: vi.fn(async () => undefined),
|
|
7
|
+
analyze: vi.fn(async (_prompt) => '{"findings": []}'),
|
|
8
|
+
}));
|
|
9
|
+
vi.mock('../inference/index.js', () => ({
|
|
10
|
+
createProvider: () => ({
|
|
11
|
+
name: 'fake',
|
|
12
|
+
isAvailable: async () => true,
|
|
13
|
+
setup: provider.setup,
|
|
14
|
+
analyze: provider.analyze,
|
|
15
|
+
dispose: () => undefined,
|
|
16
|
+
}),
|
|
17
|
+
}));
|
|
18
|
+
vi.mock('../storage/local-memory.js', () => ({ checkLocalPatterns: async () => [] }));
|
|
19
|
+
const { DeepAnalysisGate } = await import('./deep-analysis.js');
|
|
20
|
+
const REPORT_TS = [
|
|
21
|
+
'export async function loadReport(days: number) {',
|
|
22
|
+
' const rows = await fetchAll(days);',
|
|
23
|
+
' return rows.length;',
|
|
24
|
+
'}',
|
|
25
|
+
].join('\n');
|
|
26
|
+
describe('DeepAnalysisGate', () => {
|
|
27
|
+
let cwd;
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
cwd = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-deep-gate-'));
|
|
30
|
+
await fs.outputFile(path.join(cwd, 'src/report.ts'), REPORT_TS);
|
|
31
|
+
await fs.outputFile(path.join(cwd, 'src/other.ts'), 'export const other = 1;\nexport const two = 2;\nexport const three = 3;\n');
|
|
32
|
+
provider.setup.mockReset().mockResolvedValue(undefined);
|
|
33
|
+
provider.analyze.mockReset().mockResolvedValue('{"findings": []}');
|
|
34
|
+
});
|
|
35
|
+
afterEach(async () => { await fs.remove(cwd); });
|
|
36
|
+
const gate = () => new DeepAnalysisGate({ options: { enabled: true, provider: 'local' } });
|
|
37
|
+
it('records an error outcome when setup fails', async () => {
|
|
38
|
+
provider.setup.mockRejectedValue(new Error('engine missing'));
|
|
39
|
+
const g = gate();
|
|
40
|
+
expect(await g.run({ cwd })).toEqual([]);
|
|
41
|
+
expect(g.getOutcome()).toMatchObject({ status: 'error', error: 'engine missing' });
|
|
42
|
+
});
|
|
43
|
+
it('records an error outcome when every inference call fails', async () => {
|
|
44
|
+
provider.analyze.mockRejectedValue(new Error('invalid argument: --json'));
|
|
45
|
+
const g = gate();
|
|
46
|
+
await g.run({ cwd, patterns: ['src/report.ts', 'src/other.ts'] });
|
|
47
|
+
expect(g.getOutcome()).toMatchObject({ status: 'error', chunksTotal: 2, chunksFailed: 2 });
|
|
48
|
+
expect(g.getOutcome().error).toContain('invalid argument: --json');
|
|
49
|
+
});
|
|
50
|
+
it('records a partial outcome when some calls fail', async () => {
|
|
51
|
+
provider.analyze.mockRejectedValueOnce(new Error('timed out')).mockResolvedValue('{"findings": []}');
|
|
52
|
+
const g = gate();
|
|
53
|
+
await g.run({ cwd, patterns: ['src/report.ts', 'src/other.ts'] });
|
|
54
|
+
expect(g.getOutcome()).toMatchObject({ status: 'partial', chunksTotal: 2, chunksFailed: 1 });
|
|
55
|
+
});
|
|
56
|
+
it('reviews only the scoped file, with its numbered source', async () => {
|
|
57
|
+
const g = gate();
|
|
58
|
+
await g.run({ cwd, patterns: ['src/report.ts'] });
|
|
59
|
+
expect(provider.analyze).toHaveBeenCalledTimes(1);
|
|
60
|
+
const prompt = provider.analyze.mock.calls[0][0];
|
|
61
|
+
expect(prompt).toContain('2| const rows = await fetchAll(days);');
|
|
62
|
+
expect(prompt).not.toContain('src/other.ts');
|
|
63
|
+
expect(g.getOutcome()).toMatchObject({ status: 'ok', mode: 'code', filesAnalyzed: 1 });
|
|
64
|
+
});
|
|
65
|
+
it('keeps grounded findings and drops ones about code that was not sent', async () => {
|
|
66
|
+
provider.analyze.mockResolvedValue(JSON.stringify({ findings: [
|
|
67
|
+
{ category: 'scalability', severity: 'high', file: 'src/report.ts', line: 2, description: '`fetchAll` loads every row.', suggestion: 'Aggregate in SQL.', confidence: 0.8 },
|
|
68
|
+
{ category: 'security', severity: 'high', file: 'src/report.ts', line: 3, description: '`sendSecret` leaks a token.', suggestion: 'x', confidence: 0.9 },
|
|
69
|
+
] }));
|
|
70
|
+
const failures = await gate().run({ cwd, patterns: ['src/report.ts'] });
|
|
71
|
+
expect(failures).toHaveLength(1);
|
|
72
|
+
expect(failures[0]).toMatchObject({ id: 'deep-analysis', files: ['src/report.ts'], line: 2, provenance: 'deep-analysis', verified: true });
|
|
73
|
+
});
|
|
74
|
+
it('uses the facts prompt for an unscoped run', async () => {
|
|
75
|
+
const g = gate();
|
|
76
|
+
await g.run({ cwd });
|
|
77
|
+
expect(provider.analyze.mock.calls[0][0]).toContain('AST-EXTRACTED FACTS');
|
|
78
|
+
expect(g.getOutcome()).toMatchObject({ status: 'ok', mode: 'facts', filesAnalyzed: 2 });
|
|
79
|
+
});
|
|
80
|
+
it('is ok with no analyzable files in scope', async () => {
|
|
81
|
+
const g = gate();
|
|
82
|
+
expect(await g.run({ cwd, patterns: ['README.md'] })).toEqual([]);
|
|
83
|
+
expect(g.getOutcome()).toMatchObject({ status: 'ok', filesAnalyzed: 0 });
|
|
84
|
+
expect(provider.analyze).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { InferenceProvider } from '../inference/types.js';
|
|
2
|
+
import type { Failure } from '../types/index.js';
|
|
3
|
+
export declare function runIntentChecks(cwd: string, files: string[], provider: InferenceProvider, onProgress?: (message: string) => void): Promise<Failure[]>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent checks in a scoped deep review (`rigour review --deep`,
|
|
3
|
+
* `rigour check <paths> --deep`): the only questions put to the model are
|
|
4
|
+
* ones the code cannot answer, at sites the engine has already proven.
|
|
5
|
+
*
|
|
6
|
+
* Today that is optional-read-no-fallback: at an `await Promise.all([...])`
|
|
7
|
+
* with no failure handling, is one read optional while another is required?
|
|
8
|
+
* Off unless `deep.intent_checks` is set; see docs/DEEP_ANALYSIS.md for the
|
|
9
|
+
* measured accuracy that decides the default.
|
|
10
|
+
*/
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { loadProjectConfig, programBatches } from '../semantic/program.js';
|
|
13
|
+
import { findParallelReads } from '../semantic/intent/parallel-reads.js';
|
|
14
|
+
import { classifyReads, findingsFor, yesNoModel } from '../semantic/intent/intent-check.js';
|
|
15
|
+
/** Each read costs two model calls; bound the cost of one review. */
|
|
16
|
+
const MAX_SITES = 10;
|
|
17
|
+
const SOURCE = /\.(?:[cm]?[jt]s|[jt]sx)$/i;
|
|
18
|
+
export async function runIntentChecks(cwd, files, provider, onProgress) {
|
|
19
|
+
const sites = findSites(cwd, files.filter(f => SOURCE.test(f) && !/\.d\.[cm]?ts$/i.test(f)));
|
|
20
|
+
if (sites.length === 0)
|
|
21
|
+
return [];
|
|
22
|
+
const checked = sites.slice(0, MAX_SITES);
|
|
23
|
+
onProgress?.(` Asking the model about ${checked.length} Promise.all site(s)${sites.length > MAX_SITES ? ` (first ${MAX_SITES} of ${sites.length})` : ''}...`);
|
|
24
|
+
const model = yesNoModel(provider);
|
|
25
|
+
const failures = [];
|
|
26
|
+
for (const site of checked) {
|
|
27
|
+
failures.push(...findingsFor(site, await classifyReads(model, site)).map(toFailure));
|
|
28
|
+
}
|
|
29
|
+
return failures;
|
|
30
|
+
}
|
|
31
|
+
function findSites(cwd, files) {
|
|
32
|
+
const roots = files.map(f => path.resolve(cwd, f));
|
|
33
|
+
const wanted = new Set(roots.map(r => path.normalize(r)));
|
|
34
|
+
const sites = [];
|
|
35
|
+
for (const program of programBatches(roots, loadProjectConfig(cwd).options)) {
|
|
36
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
37
|
+
if (wanted.has(path.normalize(sourceFile.fileName)))
|
|
38
|
+
sites.push(...findParallelReads(cwd, program.getTypeChecker(), sourceFile));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return sites;
|
|
42
|
+
}
|
|
43
|
+
function toFailure(finding) {
|
|
44
|
+
return {
|
|
45
|
+
id: 'deep-analysis',
|
|
46
|
+
title: `[${finding.rule}] ${finding.message.split('. ')[0]}`.slice(0, 120),
|
|
47
|
+
details: `${finding.message}\n${finding.evidence.map(e => ` at ${e}`).join('\n')}`,
|
|
48
|
+
files: [finding.file],
|
|
49
|
+
line: finding.line,
|
|
50
|
+
hint: finding.hint,
|
|
51
|
+
severity: finding.severity,
|
|
52
|
+
provenance: finding.provenance,
|
|
53
|
+
category: finding.rule,
|
|
54
|
+
source: 'hybrid',
|
|
55
|
+
verified: false,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { runIntentChecks } from './deep-intent.js';
|
|
6
|
+
const DASHBOARD = [
|
|
7
|
+
'declare function getAccount(id: string): Promise<{ name: string }>;',
|
|
8
|
+
'declare function getRecommendedCourses(id: string): Promise<string[]>;',
|
|
9
|
+
'export async function loadDashboard(id: string) {',
|
|
10
|
+
' const [account, recommended] = await Promise.all([getAccount(id), getRecommendedCourses(id)]);',
|
|
11
|
+
' return { account, recommended };',
|
|
12
|
+
'}',
|
|
13
|
+
].join('\n');
|
|
14
|
+
/** Answers as if getRecommendedCourses were optional and getAccount required. */
|
|
15
|
+
function provider(calls) {
|
|
16
|
+
return {
|
|
17
|
+
name: 'fake',
|
|
18
|
+
isAvailable: async () => true,
|
|
19
|
+
setup: async () => { },
|
|
20
|
+
dispose: () => { },
|
|
21
|
+
analyze: async (prompt) => {
|
|
22
|
+
calls.push(prompt);
|
|
23
|
+
const optional = /Question: [^\n]*getRecommendedCourses/.test(prompt);
|
|
24
|
+
const ifFails = prompt.includes('Question: If');
|
|
25
|
+
return JSON.stringify({ answer: ifFails === optional ? 'yes' : 'no' });
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
describe('runIntentChecks', () => {
|
|
30
|
+
let cwd;
|
|
31
|
+
beforeEach(() => { cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'deep-intent-')); });
|
|
32
|
+
afterEach(() => { fs.rmSync(cwd, { recursive: true, force: true }); });
|
|
33
|
+
it('reports an optional read that can take down a required one', async () => {
|
|
34
|
+
fs.writeFileSync(path.join(cwd, 'dashboard.ts'), DASHBOARD);
|
|
35
|
+
const calls = [];
|
|
36
|
+
const [failure, ...rest] = await runIntentChecks(cwd, ['dashboard.ts'], provider(calls));
|
|
37
|
+
expect(rest).toHaveLength(0);
|
|
38
|
+
expect(calls).toHaveLength(4);
|
|
39
|
+
expect(failure).toMatchObject({
|
|
40
|
+
id: 'deep-analysis', files: ['dashboard.ts'], line: 4, category: 'optional-read-no-fallback',
|
|
41
|
+
provenance: 'deep-analysis', source: 'hybrid', verified: false,
|
|
42
|
+
});
|
|
43
|
+
expect(failure.details).toContain('`getRecommendedCourses` looks optional');
|
|
44
|
+
});
|
|
45
|
+
it('asks nothing when no file has an unhandled Promise.all', async () => {
|
|
46
|
+
fs.writeFileSync(path.join(cwd, 'plain.ts'), 'export const x = 1;\n');
|
|
47
|
+
const calls = [];
|
|
48
|
+
expect(await runIntentChecks(cwd, ['plain.ts', 'README.md'], provider(calls))).toEqual([]);
|
|
49
|
+
expect(calls).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { GateContext } from './base.js';
|
|
2
|
+
import type { Config, DeepOptions, Failure, Report, Status } from '../types/index.js';
|
|
3
|
+
export interface DeepRunResult {
|
|
4
|
+
failures: Failure[];
|
|
5
|
+
summary: Status;
|
|
6
|
+
stats: NonNullable<Report['stats']['deep']>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Run the deep gate and turn its outcome into summary, stats and failures.
|
|
10
|
+
* A deep run that could not analyze anything is ERROR with a visible
|
|
11
|
+
* failure, never a clean PASS.
|
|
12
|
+
*/
|
|
13
|
+
export declare function runDeepAnalysis(config: Config, context: Pick<GateContext, 'cwd' | 'ignore' | 'patterns'>, deepOptions: DeepOptions & {
|
|
14
|
+
onProgress?: (msg: string) => void;
|
|
15
|
+
}): Promise<DeepRunResult>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep analysis as the gate runner reports it: summary status, report stats
|
|
3
|
+
* and failures, derived from the deep gate's outcome.
|
|
4
|
+
*/
|
|
5
|
+
import { DeepAnalysisGate } from './deep-analysis.js';
|
|
6
|
+
import { Logger } from '../utils/logger.js';
|
|
7
|
+
/**
|
|
8
|
+
* Run the deep gate and turn its outcome into summary, stats and failures.
|
|
9
|
+
* A deep run that could not analyze anything is ERROR with a visible
|
|
10
|
+
* failure, never a clean PASS.
|
|
11
|
+
*/
|
|
12
|
+
export async function runDeepAnalysis(config, context, deepOptions) {
|
|
13
|
+
const start = Date.now();
|
|
14
|
+
const deepGate = new DeepAnalysisGate({
|
|
15
|
+
options: deepOptions,
|
|
16
|
+
checks: config.gates.deep?.checks,
|
|
17
|
+
threads: config.gates.deep?.threads,
|
|
18
|
+
maxTokens: config.gates.deep?.max_tokens,
|
|
19
|
+
temperature: config.gates.deep?.temperature,
|
|
20
|
+
timeoutMs: config.gates.deep?.timeout_ms,
|
|
21
|
+
intentChecks: config.gates.deep?.intent_checks,
|
|
22
|
+
onProgress: deepOptions.onProgress,
|
|
23
|
+
});
|
|
24
|
+
let findings = [];
|
|
25
|
+
let thrown;
|
|
26
|
+
try {
|
|
27
|
+
findings = await deepGate.run(context);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
thrown = error?.message ?? String(error);
|
|
31
|
+
Logger.error(`Deep analysis failed: ${thrown}`);
|
|
32
|
+
}
|
|
33
|
+
// A gate that threw produced nothing: never report its last outcome as ok.
|
|
34
|
+
const outcome = thrown ? { ...deepGate.getOutcome(), status: 'error', error: thrown } : deepGate.getOutcome();
|
|
35
|
+
const isLocal = !deepOptions.apiKey || (deepOptions.provider || '').toLowerCase() === 'local';
|
|
36
|
+
const stats = {
|
|
37
|
+
enabled: true,
|
|
38
|
+
status: outcome.status,
|
|
39
|
+
mode: outcome.mode,
|
|
40
|
+
tier: isLocal ? (deepOptions.pro ? 'deep' : 'lite') : 'cloud',
|
|
41
|
+
model: outcome.model,
|
|
42
|
+
model_fallback: outcome.modelFallback,
|
|
43
|
+
total_ms: Date.now() - start,
|
|
44
|
+
files_analyzed: outcome.filesAnalyzed,
|
|
45
|
+
chunks_total: outcome.chunksTotal,
|
|
46
|
+
chunks_failed: outcome.chunksFailed,
|
|
47
|
+
error: outcome.error,
|
|
48
|
+
findings_count: findings.length,
|
|
49
|
+
findings_verified: findings.filter(f => f.verified).length,
|
|
50
|
+
};
|
|
51
|
+
if (outcome.status === 'error') {
|
|
52
|
+
return {
|
|
53
|
+
failures: [{
|
|
54
|
+
id: 'deep-analysis',
|
|
55
|
+
title: 'Deep analysis did not run',
|
|
56
|
+
details: outcome.error || 'Deep analysis failed before producing results.',
|
|
57
|
+
severity: 'medium',
|
|
58
|
+
provenance: 'traditional',
|
|
59
|
+
hint: 'Run `rigour doctor`. For local mode, `rigour deep pull` installs the engine and model.',
|
|
60
|
+
}],
|
|
61
|
+
summary: 'ERROR',
|
|
62
|
+
stats,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return { failures: findings, summary: findings.length > 0 ? 'FAIL' : 'PASS', stats };
|
|
66
|
+
}
|
package/dist/gates/dependency.js
CHANGED
|
@@ -18,6 +18,7 @@ import path from 'path';
|
|
|
18
18
|
import { Gate } from './base.js';
|
|
19
19
|
import { FileScanner } from '../utils/scanner.js';
|
|
20
20
|
import { Logger } from '../utils/logger.js';
|
|
21
|
+
import { scopeExcludes } from '../utils/scope.js';
|
|
21
22
|
/**
|
|
22
23
|
* Known heavy packages with lighter alternatives.
|
|
23
24
|
* Format: package → "alternative (size comparison)"
|
|
@@ -84,6 +85,9 @@ export class DependencyGate extends Gate {
|
|
|
84
85
|
const depConfig = this.config.gates.dependencies || {};
|
|
85
86
|
const forbidden = depConfig.forbid || [];
|
|
86
87
|
const { cwd } = context;
|
|
88
|
+
// Dependency health belongs to the manifest: a scoped run checks it only with package.json in scope.
|
|
89
|
+
if (await scopeExcludes(context, 'package.json'))
|
|
90
|
+
return [];
|
|
87
91
|
// 1. Scan Node.js (package.json) — forbidden + new checks
|
|
88
92
|
const pkgPath = path.join(cwd, 'package.json');
|
|
89
93
|
if (await fs.pathExists(pkgPath)) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deprecated JS/TS constructs that need the syntax tree to detect reliably.
|
|
3
|
+
*
|
|
4
|
+
* A `with` statement is found from the parsed tree, so the words "with (" in
|
|
5
|
+
* a comment, a string or an identifier never match.
|
|
6
|
+
*/
|
|
7
|
+
import ts from 'typescript';
|
|
8
|
+
export function findWithStatements(content, file) {
|
|
9
|
+
const source = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
|
|
10
|
+
const found = [];
|
|
11
|
+
const visit = (node) => {
|
|
12
|
+
if (node.kind === ts.SyntaxKind.WithStatement) {
|
|
13
|
+
found.push({
|
|
14
|
+
line: source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1,
|
|
15
|
+
api: 'with statement',
|
|
16
|
+
reason: 'Removed in strict mode, creates ambiguous scope, security risk',
|
|
17
|
+
replacement: 'Destructuring or explicit property access',
|
|
18
|
+
category: 'removed',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
ts.forEachChild(node, visit);
|
|
22
|
+
};
|
|
23
|
+
visit(source);
|
|
24
|
+
return found;
|
|
25
|
+
}
|
|
@@ -15,7 +15,8 @@ export const NODE_DEPRECATED_RULES = [
|
|
|
15
15
|
category: 'security',
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
|
-
|
|
18
|
+
// Not ArrayBuffer(n) / SharedArrayBuffer(n) / x.Buffer(...).
|
|
19
|
+
pattern: /(?<![\w$.])Buffer\s*\(\s*(?:\d|['"])/,
|
|
19
20
|
api: 'Buffer() constructor',
|
|
20
21
|
reason: 'DEP0005: Security vulnerability — uninitialized memory exposure',
|
|
21
22
|
replacement: 'Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe()',
|
|
@@ -168,13 +169,6 @@ export const WEB_DEPRECATED_RULES = [
|
|
|
168
169
|
replacement: 'JSON.parse(), Function constructor (if absolutely needed), or structured approach',
|
|
169
170
|
category: 'security',
|
|
170
171
|
},
|
|
171
|
-
{
|
|
172
|
-
pattern: /with\s*\(\s*\w/,
|
|
173
|
-
api: 'with statement',
|
|
174
|
-
reason: 'Removed in strict mode, creates ambiguous scope, security risk',
|
|
175
|
-
replacement: 'Destructuring or explicit property access',
|
|
176
|
-
category: 'removed',
|
|
177
|
-
},
|
|
178
172
|
{
|
|
179
173
|
pattern: /document\.all\b/,
|
|
180
174
|
api: 'document.all',
|
|
@@ -22,6 +22,7 @@ import { Gate } from './base.js';
|
|
|
22
22
|
import { FileScanner } from '../utils/scanner.js';
|
|
23
23
|
import { Logger } from '../utils/logger.js';
|
|
24
24
|
import { languageAdapters } from './language-adapters/index.js';
|
|
25
|
+
import { findWithStatements } from './deprecated-apis-ast.js';
|
|
25
26
|
import fs from 'fs-extra';
|
|
26
27
|
import path from 'path';
|
|
27
28
|
import { NODE_DEPRECATED_RULES, WEB_DEPRECATED_RULES, PYTHON_DEPRECATED_RULES, GO_DEPRECATED_RULES, CSHARP_DEPRECATED_RULES, JAVA_DEPRECATED_RULES } from './deprecated-apis-rules.js';
|
|
@@ -81,8 +82,11 @@ export class DeprecatedApisGate extends Gate {
|
|
|
81
82
|
case 'js':
|
|
82
83
|
if (this.config.check_node)
|
|
83
84
|
this.checkNodeDeprecated(content, file, deprecated);
|
|
84
|
-
if (this.config.check_web)
|
|
85
|
+
if (this.config.check_web) {
|
|
85
86
|
this.checkWebDeprecated(content, file, deprecated);
|
|
87
|
+
for (const w of findWithStatements(content, file))
|
|
88
|
+
deprecated.push({ file, ...w });
|
|
89
|
+
}
|
|
86
90
|
break;
|
|
87
91
|
case 'python':
|
|
88
92
|
this.checkPythonDeprecated(content, file, deprecated);
|
|
@@ -286,3 +286,33 @@ class Resource {
|
|
|
286
286
|
expect(failures[0].details).toContain('finalize');
|
|
287
287
|
});
|
|
288
288
|
});
|
|
289
|
+
describe('DeprecatedApisGate — word fragments and non-code text', () => {
|
|
290
|
+
let gate;
|
|
291
|
+
beforeEach(() => {
|
|
292
|
+
gate = new DeprecatedApisGate();
|
|
293
|
+
vi.clearAllMocks();
|
|
294
|
+
});
|
|
295
|
+
async function apisFor(file, source) {
|
|
296
|
+
mockFindFiles.mockResolvedValue([file]);
|
|
297
|
+
mockReadFile.mockResolvedValue(source);
|
|
298
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
299
|
+
return failures.map(f => f.details).join('\n');
|
|
300
|
+
}
|
|
301
|
+
it('does not flag ArrayBuffer or SharedArrayBuffer as the Buffer() constructor', async () => {
|
|
302
|
+
const details = await apisFor('src/lib/ingestion-proxy.ts', 'const empty = new ArrayBuffer(0);\nconst shared = new SharedArrayBuffer(8);\n');
|
|
303
|
+
expect(details).not.toContain('Buffer() constructor');
|
|
304
|
+
});
|
|
305
|
+
it('still flags a bare Buffer() constructor call', async () => {
|
|
306
|
+
expect(await apisFor('src/handler.js', "const b = Buffer(10);\n")).toContain('Buffer() constructor');
|
|
307
|
+
});
|
|
308
|
+
it('does not flag "with (" inside a trailing comment or a string', async () => {
|
|
309
|
+
const details = await apisFor('src/lib/aliases.ts', [
|
|
310
|
+
"const pattern = compile(source); // match with (optional) prefix",
|
|
311
|
+
"const label = 'works with (legacy) clients';",
|
|
312
|
+
].join('\n'));
|
|
313
|
+
expect(details).not.toContain('with statement');
|
|
314
|
+
});
|
|
315
|
+
it('still flags a real with statement', async () => {
|
|
316
|
+
expect(await apisFor('src/legacy.js', 'with (obj) {\n x = 1;\n}\n')).toContain('with statement');
|
|
317
|
+
});
|
|
318
|
+
});
|
|
@@ -62,8 +62,12 @@ export class FrontendSecretExposureGate extends Gate {
|
|
|
62
62
|
'(^|/)e2e/',
|
|
63
63
|
'(^|/)tests/',
|
|
64
64
|
'(^|/)server/',
|
|
65
|
-
'(^|/)(?:playwright|vitest|vite|jest|next|webpack|eslint|prettier|tailwind|postcss|babel|tsup|rollup)\\.config\\.[cm]?[jt]s$',
|
|
65
|
+
'(^|/)(?:playwright|vitest|vite|jest|next|webpack|eslint|prettier|tailwind|postcss|babel|tsup|rollup|drizzle|svelte|astro|nuxt|remix|wrangler|knip|commitlint|esbuild|turbo)\\.config\\.[cm]?[jt]s$',
|
|
66
|
+
// Any tool config at the repository root runs in Node, never in a browser bundle.
|
|
67
|
+
'^[^/]+\\.config\\.[cm]?[jt]s$',
|
|
66
68
|
'\\.server\\.(?:ts|tsx|js|jsx|mjs|cjs)$',
|
|
69
|
+
// SvelteKit endpoints (`+server.ts`) are server-only.
|
|
70
|
+
'(^|/)\\+server\\.[cm]?[jt]s$',
|
|
67
71
|
...(config.server_path_patterns ?? []),
|
|
68
72
|
],
|
|
69
73
|
allowlist_env_names: config.allowlist_env_names ?? [],
|
|
@@ -108,4 +108,24 @@ describe('FrontendSecretExposureGate', () => {
|
|
|
108
108
|
const failures = await gate.run({ cwd: testDir });
|
|
109
109
|
expect(failures).toHaveLength(0);
|
|
110
110
|
});
|
|
111
|
+
it('treats SvelteKit endpoints and root tool configs as server code', async () => {
|
|
112
|
+
const files = {
|
|
113
|
+
'src/routes/api/sync/+server.ts': 'export const GET = () => fetch(url, { headers: { key: process.env.STRIPE_SECRET_KEY } });\n',
|
|
114
|
+
'drizzle.config.ts': 'export default { dbCredentials: { url: process.env.DATABASE_SECRET_URL } };\n',
|
|
115
|
+
'apps/web/drizzle.config.ts': 'export default { dbCredentials: { url: process.env.DATABASE_SECRET_URL } };\n',
|
|
116
|
+
};
|
|
117
|
+
for (const [rel, body] of Object.entries(files)) {
|
|
118
|
+
fs.mkdirSync(path.dirname(path.join(testDir, rel)), { recursive: true });
|
|
119
|
+
fs.writeFileSync(path.join(testDir, rel), body);
|
|
120
|
+
}
|
|
121
|
+
const failures = await new FrontendSecretExposureGate().run({ cwd: testDir });
|
|
122
|
+
expect(failures).toHaveLength(0);
|
|
123
|
+
});
|
|
124
|
+
it('still flags a secret in a config module inside the client source tree', async () => {
|
|
125
|
+
const filePath = path.join(testDir, 'src/components/site.config.ts');
|
|
126
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
127
|
+
fs.writeFileSync(filePath, 'export const key = process.env.STRIPE_SECRET_KEY;\n');
|
|
128
|
+
const failures = await new FrontendSecretExposureGate().run({ cwd: testDir });
|
|
129
|
+
expect(failures.length).toBeGreaterThan(0);
|
|
130
|
+
});
|
|
111
131
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** SvelteKit project root for a file (nearest package.json depending on the kit), cached per directory. */
|
|
2
|
+
export declare class SvelteKitRoots {
|
|
3
|
+
private readonly cwd;
|
|
4
|
+
private cache;
|
|
5
|
+
constructor(cwd: string);
|
|
6
|
+
rootFor(file: string): Promise<string | null>;
|
|
7
|
+
}
|
|
8
|
+
/** `$app/*`, `$env/*`, `$service-worker`, or a route's generated `./$types`. */
|
|
9
|
+
export declare function isSvelteKitProvided(importPath: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Resolve `$lib` / `$lib/...` to the kit's default `src/lib` folder.
|
|
12
|
+
* Returns null when the import is not a `$lib` import.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveSvelteKitLib(importPath: string, kitRoot: string, cwd: string, projectFiles: Set<string>): Promise<boolean | null>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-provided modules that no package.json or tsconfig declares.
|
|
3
|
+
*
|
|
4
|
+
* SvelteKit supplies `$app/*`, `$env/*` and `$service-worker` as virtual
|
|
5
|
+
* modules and generates each route's `./$types`. Its `$lib` alias lives in
|
|
6
|
+
* `.svelte-kit/tsconfig.json`, which `svelte-kit sync` generates and git
|
|
7
|
+
* ignores, so on a fresh clone or in CI the tsconfig mapping is missing and
|
|
8
|
+
* `$lib/...` would be read as an uninstalled package.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'fs-extra';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { resolveTsPathTarget } from './ts-path-target.js';
|
|
13
|
+
const SVELTEKIT_PACKAGE = '@sveltejs/kit';
|
|
14
|
+
const SVELTEKIT_VIRTUAL = /^(?:\$app|\$env)\/|^\$service-worker$/;
|
|
15
|
+
const GENERATED_TYPES = /^\.{1,2}\/(?:.*\/)?\$types(?:\.d\.ts|\.js)?$/;
|
|
16
|
+
const LIB_ALIAS = /^\$lib(?:\/(.*))?$/;
|
|
17
|
+
/** SvelteKit project root for a file (nearest package.json depending on the kit), cached per directory. */
|
|
18
|
+
export class SvelteKitRoots {
|
|
19
|
+
cwd;
|
|
20
|
+
cache = new Map();
|
|
21
|
+
constructor(cwd) {
|
|
22
|
+
this.cwd = cwd;
|
|
23
|
+
}
|
|
24
|
+
async rootFor(file) {
|
|
25
|
+
const top = path.resolve(this.cwd);
|
|
26
|
+
const visited = [];
|
|
27
|
+
let dir = path.dirname(path.resolve(this.cwd, file));
|
|
28
|
+
let found = null;
|
|
29
|
+
while (dir.startsWith(top)) {
|
|
30
|
+
if (this.cache.has(dir)) {
|
|
31
|
+
found = this.cache.get(dir) ?? null;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
visited.push(dir);
|
|
35
|
+
if (await declaresKit(path.join(dir, 'package.json'))) {
|
|
36
|
+
found = dir;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
const parent = path.dirname(dir);
|
|
40
|
+
if (parent === dir)
|
|
41
|
+
break;
|
|
42
|
+
dir = parent;
|
|
43
|
+
}
|
|
44
|
+
for (const d of visited)
|
|
45
|
+
this.cache.set(d, found);
|
|
46
|
+
return found;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function declaresKit(packageJsonPath) {
|
|
50
|
+
if (!(await fs.pathExists(packageJsonPath)))
|
|
51
|
+
return false;
|
|
52
|
+
try {
|
|
53
|
+
const pkg = await fs.readJson(packageJsonPath);
|
|
54
|
+
return [pkg?.dependencies, pkg?.devDependencies, pkg?.peerDependencies]
|
|
55
|
+
.some(deps => !!deps && Object.prototype.hasOwnProperty.call(deps, SVELTEKIT_PACKAGE));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** `$app/*`, `$env/*`, `$service-worker`, or a route's generated `./$types`. */
|
|
62
|
+
export function isSvelteKitProvided(importPath) {
|
|
63
|
+
return SVELTEKIT_VIRTUAL.test(importPath) || GENERATED_TYPES.test(importPath);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve `$lib` / `$lib/...` to the kit's default `src/lib` folder.
|
|
67
|
+
* Returns null when the import is not a `$lib` import.
|
|
68
|
+
*/
|
|
69
|
+
export async function resolveSvelteKitLib(importPath, kitRoot, cwd, projectFiles) {
|
|
70
|
+
const match = importPath.match(LIB_ALIAS);
|
|
71
|
+
if (!match)
|
|
72
|
+
return null;
|
|
73
|
+
const target = match[1] ? path.join('src/lib', match[1]) : 'src/lib';
|
|
74
|
+
return resolveTsPathTarget(kitRoot, target, cwd, projectFiles);
|
|
75
|
+
}
|
|
@@ -24,6 +24,7 @@ import fs from 'fs-extra';
|
|
|
24
24
|
import path from 'path';
|
|
25
25
|
import { checkGoImports, checkRubyImports, checkCSharpImports, checkRustImports, checkJavaKotlinImports } from '../hallucinated-imports-lang.js';
|
|
26
26
|
import { checkJSImports } from './js-resolver.js';
|
|
27
|
+
import { SvelteKitRoots } from './framework-modules.js';
|
|
27
28
|
import { checkPyImports } from './python-resolver.js';
|
|
28
29
|
import { discoverWorkspacePackages, loadPackageJson } from './manifest-discovery.js';
|
|
29
30
|
export class HallucinatedImportsGate extends Gate {
|
|
@@ -47,6 +48,8 @@ export class HallucinatedImportsGate extends Gate {
|
|
|
47
48
|
const failures = [];
|
|
48
49
|
const hallucinated = [];
|
|
49
50
|
const defaultPatterns = ['**/*.{ts,js,tsx,jsx,py,go,rb,cs,rs,java,kt}'];
|
|
51
|
+
// Imports can target component files that are not scanned themselves.
|
|
52
|
+
const resolutionPatterns = ['**/*.{ts,js,tsx,jsx,mjs,cjs,py,go,rb,cs,rs,java,kt,svelte,vue,astro}'];
|
|
50
53
|
const scanPatterns = context.patterns || defaultPatterns;
|
|
51
54
|
const ignore = [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
|
|
52
55
|
'**/examples/**', '**/studio-dist/**', '**/.next/**', '**/coverage/**',
|
|
@@ -60,11 +63,9 @@ export class HallucinatedImportsGate extends Gate {
|
|
|
60
63
|
});
|
|
61
64
|
const analyzableFiles = files.filter(file => !this.shouldSkipFile(file));
|
|
62
65
|
Logger.info(`Hallucinated Imports: Scanning ${analyzableFiles.length} files`);
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
const resolutionFiles = context.patterns
|
|
66
|
-
? await FileScanner.findFiles({ cwd: context.cwd, patterns: defaultPatterns, ignore })
|
|
67
|
-
: files;
|
|
66
|
+
// Resolve against the whole project, not the scanned set: a scoped
|
|
67
|
+
// review scans changed files only, and imports may target components.
|
|
68
|
+
const resolutionFiles = await FileScanner.findFiles({ cwd: context.cwd, patterns: resolutionPatterns, ignore });
|
|
68
69
|
const projectFiles = new Set(resolutionFiles.map(f => f.replace(/\\/g, '/')));
|
|
69
70
|
const allProjectFiles = projectFiles;
|
|
70
71
|
const packageJson = await loadPackageJson(context.cwd);
|
|
@@ -80,6 +81,13 @@ export class HallucinatedImportsGate extends Gate {
|
|
|
80
81
|
const depCacheByDir = new Map();
|
|
81
82
|
const tsPathCacheByDir = new Map();
|
|
82
83
|
const hasNodeModules = await fs.pathExists(path.join(context.cwd, 'node_modules'));
|
|
84
|
+
const jsContext = {
|
|
85
|
+
cwd: context.cwd, projectFiles, rootDeps, depCacheByDir, hasNodeModules, hallucinated, tsPathCacheByDir,
|
|
86
|
+
kitRoots: new SvelteKitRoots(context.cwd),
|
|
87
|
+
shouldIgnore: (importPath) => this.shouldIgnore(importPath),
|
|
88
|
+
resolveRelativeImport: (fromFile, importPath, files) => this.resolveRelativeImport(fromFile, importPath, files),
|
|
89
|
+
extractPackageName: (importPath) => this.extractPackageName(importPath),
|
|
90
|
+
};
|
|
83
91
|
for (const file of analyzableFiles) {
|
|
84
92
|
try {
|
|
85
93
|
const fullPath = path.join(context.cwd, file);
|
|
@@ -90,7 +98,7 @@ export class HallucinatedImportsGate extends Gate {
|
|
|
90
98
|
continue;
|
|
91
99
|
switch (adapter.id) {
|
|
92
100
|
case 'js':
|
|
93
|
-
await checkJSImports(content, file,
|
|
101
|
+
await checkJSImports(content, file, jsContext);
|
|
94
102
|
break;
|
|
95
103
|
case 'python':
|
|
96
104
|
await checkPyImports(content, file, context.cwd, projectFiles, hallucinated);
|