@rigour-labs/core 6.4.4 → 6.5.1
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/dist/deep/agent-review.d.ts +19 -0
- package/dist/deep/agent-review.js +19 -0
- package/dist/deep/agent-review.test.d.ts +1 -0
- package/dist/deep/agent-review.test.js +53 -0
- package/dist/deep/code-pass.d.ts +10 -0
- package/dist/deep/code-pass.js +64 -33
- package/dist/deep/code-pass.test.js +53 -12
- package/dist/deep/code-verifier.d.ts +3 -1
- package/dist/deep/code-verifier.js +11 -11
- package/dist/deep/code-verifier.test.js +8 -0
- package/dist/deep/pr-diff.d.ts +7 -0
- package/dist/deep/pr-diff.js +47 -0
- package/dist/deep/pr-review.d.ts +33 -0
- package/dist/deep/pr-review.js +81 -0
- package/dist/deep/pr-review.test.d.ts +1 -0
- package/dist/deep/pr-review.test.js +80 -0
- package/dist/deep/related-changes.d.ts +13 -0
- package/dist/deep/related-changes.js +56 -0
- package/dist/deep/related-changes.test.d.ts +1 -0
- package/dist/deep/related-changes.test.js +33 -0
- package/dist/deep/review-tools.d.ts +16 -0
- package/dist/deep/review-tools.js +111 -0
- package/dist/deep/review-tools.test.d.ts +1 -0
- package/dist/deep/review-tools.test.js +39 -0
- package/dist/deep/risk.d.ts +41 -0
- package/dist/deep/risk.js +135 -0
- package/dist/deep/risk.test.d.ts +1 -0
- package/dist/deep/risk.test.js +63 -0
- package/dist/deep/router.d.ts +27 -0
- package/dist/deep/router.js +15 -0
- package/dist/deep/self-check.d.ts +14 -9
- package/dist/deep/self-check.js +41 -32
- package/dist/deep/tool-loop.d.ts +19 -0
- package/dist/deep/tool-loop.js +23 -0
- package/dist/gates/deep-analysis.d.ts +27 -0
- package/dist/gates/deep-analysis.js +73 -2
- package/dist/gates/deep-runner.js +14 -0
- package/dist/gates/frontend-secret-exposure.d.ts +2 -0
- package/dist/gates/frontend-secret-exposure.js +8 -0
- package/dist/gates/frontend-secret-exposure.test.js +14 -0
- package/dist/gates/hallucinated-imports/framework-modules-nuxt.test.d.ts +1 -0
- package/dist/gates/hallucinated-imports/framework-modules-nuxt.test.js +27 -0
- package/dist/gates/hallucinated-imports/framework-modules.d.ts +12 -3
- package/dist/gates/hallucinated-imports/framework-modules.js +32 -6
- package/dist/gates/hallucinated-imports/index.js +2 -1
- package/dist/gates/hallucinated-imports/js-resolver-types.test.d.ts +1 -0
- package/dist/gates/hallucinated-imports/js-resolver-types.test.js +14 -0
- package/dist/gates/hallucinated-imports/js-resolver.d.ts +6 -1
- package/dist/gates/hallucinated-imports/js-resolver.js +25 -3
- package/dist/gates/phantom-apis.js +2 -2
- package/dist/gates/phantom-apis.test.js +10 -0
- package/dist/gates/promise-safety.js +1 -1
- package/dist/gates/test-quality-matchers.js +3 -0
- package/dist/gates/test-quality.test.js +13 -0
- package/dist/hooks/checker.test.js +9 -0
- package/dist/hooks/stop-review.d.ts +2 -0
- package/dist/hooks/stop-review.js +19 -3
- package/dist/hooks/templates.d.ts +2 -0
- package/dist/hooks/templates.js +16 -2
- package/dist/hooks/templates.test.d.ts +1 -0
- package/dist/hooks/templates.test.js +16 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/inference/cloud-provider.d.ts +12 -1
- package/dist/inference/cloud-provider.js +137 -2
- package/dist/inference/cloud-provider.test.d.ts +1 -0
- package/dist/inference/cloud-provider.test.js +118 -0
- package/dist/inference/pricing.d.ts +15 -0
- package/dist/inference/pricing.js +13 -0
- package/dist/inference/types.d.ts +42 -0
- package/dist/review/changed-function-spans.d.ts +4 -0
- package/dist/review/changed-function-spans.js +48 -0
- package/dist/review/changed-lines.d.ts +9 -3
- package/dist/review/changed-lines.js +19 -2
- package/dist/review/deep-runs.d.ts +22 -0
- package/dist/review/deep-runs.js +59 -0
- package/dist/review/deep-runs.test.d.ts +1 -0
- package/dist/review/deep-runs.test.js +23 -0
- package/dist/review/generated-files.d.ts +3 -0
- package/dist/review/generated-files.js +35 -0
- package/dist/review/generated-files.test.d.ts +1 -0
- package/dist/review/generated-files.test.js +25 -0
- package/dist/review/ledger.d.ts +27 -0
- package/dist/review/ledger.js +72 -0
- package/dist/review/quiet.d.ts +16 -0
- package/dist/review/quiet.js +71 -0
- package/dist/review/quiet.test.d.ts +1 -0
- package/dist/review/quiet.test.js +27 -0
- package/dist/review/review-ack.d.ts +16 -0
- package/dist/review/review-ack.js +25 -0
- package/dist/review/review-task.d.ts +40 -0
- package/dist/review/review-task.js +50 -0
- package/dist/review/review-task.test.d.ts +1 -0
- package/dist/review/review-task.test.js +80 -0
- package/dist/review/review.d.ts +12 -1
- package/dist/review/review.js +16 -6
- package/dist/review/review.test.js +25 -0
- package/dist/review-learning/acted-on.d.ts +25 -0
- package/dist/review-learning/acted-on.js +69 -0
- package/dist/review-learning/learn-from-reviews.d.ts +34 -0
- package/dist/review-learning/learn-from-reviews.js +65 -0
- package/dist/review-learning/lessons.d.ts +52 -0
- package/dist/review-learning/lessons.js +149 -0
- package/dist/review-learning/repo-rules.d.ts +16 -0
- package/dist/review-learning/repo-rules.js +84 -0
- package/dist/review-learning/repo-rules.test.d.ts +1 -0
- package/dist/review-learning/repo-rules.test.js +39 -0
- package/dist/review-learning/review-learning.test.d.ts +1 -0
- package/dist/review-learning/review-learning.test.js +104 -0
- package/dist/review-learning/team-lessons.d.ts +4 -0
- package/dist/review-learning/team-lessons.js +24 -0
- package/dist/semantic/rules/credential-redirect.js +4 -1
- package/dist/settings.js +4 -1
- package/dist/telemetry/telemetry.d.ts +28 -0
- package/dist/telemetry/telemetry.js +152 -0
- package/dist/telemetry/telemetry.test.d.ts +1 -0
- package/dist/telemetry/telemetry.test.js +59 -0
- package/dist/telemetry/token.d.ts +1 -0
- package/dist/telemetry/token.js +1 -0
- package/dist/templates/universal-config.js +7 -3
- package/dist/types/index.d.ts +276 -10
- package/dist/types/index.js +38 -5
- package/package.json +6 -6
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Self-check: the model re-reads
|
|
3
|
-
* or withdraws
|
|
2
|
+
* Self-check: the model re-reads its findings against the code and confirms
|
|
3
|
+
* or withdraws each one.
|
|
4
4
|
*
|
|
5
5
|
* Two review passes with the reference material in different orders raise
|
|
6
|
-
* recall (a finding one ordering misses, the other catches); asking again
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* recall (a finding one ordering misses, the other catches); asking again
|
|
7
|
+
* whether each finding would misbehave at runtime keeps that extra recall
|
|
8
|
+
* from arriving as noise.
|
|
9
|
+
*
|
|
10
|
+
* All of a file's findings are checked in one call: the source and reference
|
|
11
|
+
* dominate the prompt, so checking finding by finding paid for them up to
|
|
12
|
+
* eight times per file. A check that cannot run, or a finding the reply does
|
|
13
|
+
* not mention, keeps the finding: an unavailable or vague model is not
|
|
14
|
+
* evidence against it.
|
|
10
15
|
*/
|
|
11
16
|
import type { DeepFinding, InferenceOptions, InferenceProvider } from '../inference/types.js';
|
|
12
17
|
import type { CodeContext } from './code-context.js';
|
|
@@ -16,6 +21,6 @@ export interface SelfCheckResult {
|
|
|
16
21
|
failed: number;
|
|
17
22
|
}
|
|
18
23
|
export declare function selfCheck(provider: InferenceProvider, context: CodeContext, reference: string, findings: DeepFinding[], inference: InferenceOptions): Promise<SelfCheckResult>;
|
|
19
|
-
export declare function checkPrompt(context: CodeContext, reference: string,
|
|
20
|
-
/**
|
|
21
|
-
export declare function
|
|
24
|
+
export declare function checkPrompt(context: CodeContext, reference: string, findings: DeepFinding[]): string;
|
|
25
|
+
/** Finding number → verdict; numbers the reply does not settle are absent. */
|
|
26
|
+
export declare function parseVerdicts(reply: string): Map<number, boolean>;
|
package/dist/deep/self-check.js
CHANGED
|
@@ -1,49 +1,58 @@
|
|
|
1
|
-
const MAX_CHECKED =
|
|
2
|
-
const
|
|
1
|
+
const MAX_CHECKED = 12;
|
|
2
|
+
const VERDICTS_SCHEMA = {
|
|
3
3
|
type: 'object',
|
|
4
|
-
properties: {
|
|
5
|
-
|
|
4
|
+
properties: {
|
|
5
|
+
verdicts: {
|
|
6
|
+
type: 'array',
|
|
7
|
+
items: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: { n: { type: 'integer' }, real: { type: 'boolean' }, reason: { type: 'string' } },
|
|
10
|
+
required: ['n', 'real'],
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
required: ['verdicts'],
|
|
6
15
|
};
|
|
7
16
|
export async function selfCheck(provider, context, reference, findings, inference) {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
try {
|
|
15
|
-
const reply = await provider.analyze(checkPrompt(context, reference, finding), { ...inference, jsonSchema: VERDICT_SCHEMA });
|
|
16
|
-
if (parseVerdict(reply) === false)
|
|
17
|
-
result.withdrawn++;
|
|
18
|
-
else
|
|
19
|
-
result.kept.push(finding);
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
result.failed++;
|
|
23
|
-
result.kept.push(finding);
|
|
24
|
-
}
|
|
17
|
+
const checked = findings.slice(0, MAX_CHECKED);
|
|
18
|
+
const unchecked = findings.slice(MAX_CHECKED);
|
|
19
|
+
let verdicts;
|
|
20
|
+
try {
|
|
21
|
+
verdicts = parseVerdicts(await provider.analyze(checkPrompt(context, reference, checked), { ...inference, jsonSchema: VERDICTS_SCHEMA }));
|
|
25
22
|
}
|
|
26
|
-
|
|
23
|
+
catch {
|
|
24
|
+
return { kept: findings, withdrawn: 0, failed: checked.length };
|
|
25
|
+
}
|
|
26
|
+
const kept = checked.filter((_, index) => verdicts.get(index + 1) !== false);
|
|
27
|
+
return { kept: [...kept, ...unchecked], withdrawn: checked.length - kept.length, failed: 0 };
|
|
27
28
|
}
|
|
28
|
-
export function checkPrompt(context, reference,
|
|
29
|
+
export function checkPrompt(context, reference, findings) {
|
|
30
|
+
const listed = findings.map((f, index) => `${index + 1}. ${context.file}:${f.line}: ${f.description}`).join('\n');
|
|
29
31
|
return [
|
|
30
|
-
|
|
32
|
+
`You reported ${findings.length} defect(s) in a code review. Check each against the code before it is posted.`,
|
|
31
33
|
`SOURCE (line numbers on the left):\n${context.text}`,
|
|
32
34
|
reference ? `REFERENCE (not under review):\n${reference}` : '',
|
|
33
|
-
`REPORTED
|
|
34
|
-
'
|
|
35
|
+
`REPORTED:\n${listed}`,
|
|
36
|
+
'For each one: is it a real defect, would this code misbehave at runtime or in production as described? '
|
|
35
37
|
+ 'Answer false if it is style, speculation about unseen callers, or already handled in the code shown. '
|
|
36
|
-
+ 'Respond ONLY with JSON: {"real": true|false, "reason": "..."}.',
|
|
38
|
+
+ 'Respond ONLY with JSON: {"verdicts": [{"n": 1, "real": true|false, "reason": "..."}, ...]}.',
|
|
37
39
|
].filter(Boolean).join('\n\n');
|
|
38
40
|
}
|
|
39
|
-
/**
|
|
40
|
-
export function
|
|
41
|
+
/** Finding number → verdict; numbers the reply does not settle are absent. */
|
|
42
|
+
export function parseVerdicts(reply) {
|
|
43
|
+
const verdicts = new Map();
|
|
41
44
|
const json = reply.slice(reply.indexOf('{'), reply.lastIndexOf('}') + 1);
|
|
42
45
|
try {
|
|
43
|
-
const
|
|
44
|
-
|
|
46
|
+
const list = JSON.parse(json).verdicts;
|
|
47
|
+
if (!Array.isArray(list))
|
|
48
|
+
return verdicts;
|
|
49
|
+
for (const item of list) {
|
|
50
|
+
if (Number.isInteger(item?.n) && typeof item?.real === 'boolean')
|
|
51
|
+
verdicts.set(item.n, item.real);
|
|
52
|
+
}
|
|
45
53
|
}
|
|
46
54
|
catch {
|
|
47
|
-
|
|
55
|
+
// Not a verdict list: nothing is withdrawn.
|
|
48
56
|
}
|
|
57
|
+
return verdicts;
|
|
49
58
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A bounded tool-using conversation: the model reads the repository through
|
|
3
|
+
* ReviewToolbox until it answers, runs out of tool calls, or runs out of
|
|
4
|
+
* turns. After the budget, tools stay defined (a history with tool calls
|
|
5
|
+
* needs them) but calls are forbidden, and the model is asked to answer.
|
|
6
|
+
*/
|
|
7
|
+
import type { InferenceOptions, InferenceProvider } from '../inference/types.js';
|
|
8
|
+
import { ReviewToolbox } from './review-tools.js';
|
|
9
|
+
export interface ToolLoopBudget {
|
|
10
|
+
maxToolCalls: number;
|
|
11
|
+
maxTurns: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ToolLoopResult {
|
|
14
|
+
/** The model's final answer. */
|
|
15
|
+
text: string;
|
|
16
|
+
toolbox: ReviewToolbox;
|
|
17
|
+
toolCalls: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function runToolLoop(provider: InferenceProvider, prompt: string, cwd: string, inference: InferenceOptions, budget: ToolLoopBudget): Promise<ToolLoopResult>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { REVIEW_TOOLS, ReviewToolbox } from './review-tools.js';
|
|
2
|
+
export async function runToolLoop(provider, prompt, cwd, inference, budget) {
|
|
3
|
+
if (!provider.chat)
|
|
4
|
+
throw new Error(`${provider.name} cannot call tools`);
|
|
5
|
+
const toolbox = new ReviewToolbox(cwd);
|
|
6
|
+
const messages = [{ role: 'user', content: prompt }];
|
|
7
|
+
let toolCalls = 0;
|
|
8
|
+
for (let turn = 0; turn < budget.maxTurns; turn++) {
|
|
9
|
+
const exhausted = toolCalls >= budget.maxToolCalls;
|
|
10
|
+
const reply = await provider.chat(messages, REVIEW_TOOLS, { ...inference, jsonMode: false, toolChoice: exhausted ? 'none' : 'auto' });
|
|
11
|
+
if (reply.toolCalls.length === 0 || exhausted)
|
|
12
|
+
return { text: reply.text, toolbox, toolCalls };
|
|
13
|
+
messages.push({ role: 'assistant', content: reply.text, toolCalls: reply.toolCalls });
|
|
14
|
+
for (const call of reply.toolCalls) {
|
|
15
|
+
toolCalls++;
|
|
16
|
+
const result = toolCalls <= budget.maxToolCalls ? toolbox.run(call) : 'Tool budget used up; answer now.';
|
|
17
|
+
messages.push({ role: 'tool', toolCallId: call.id, content: result });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
messages.push({ role: 'user', content: 'Answer now with the JSON findings only.' });
|
|
21
|
+
const final = await provider.chat(messages, REVIEW_TOOLS, { ...inference, jsonMode: false, toolChoice: 'none' });
|
|
22
|
+
return { text: final.text, toolbox, toolCalls };
|
|
23
|
+
}
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { Gate, GateContext } from './base.js';
|
|
15
15
|
import { Failure, Provenance, DeepOptions } from '../types/index.js';
|
|
16
|
+
import type { InferenceUsage } from '../inference/types.js';
|
|
17
|
+
import { type RouterPolicy, type RouterStats } from '../deep/router.js';
|
|
18
|
+
import { type LessonMode } from '../review-learning/team-lessons.js';
|
|
16
19
|
export interface DeepGateConfig {
|
|
17
20
|
options: DeepOptions;
|
|
18
21
|
checks?: Record<string, boolean>;
|
|
@@ -20,6 +23,16 @@ export interface DeepGateConfig {
|
|
|
20
23
|
maxTokens?: number;
|
|
21
24
|
temperature?: number;
|
|
22
25
|
timeoutMs?: number;
|
|
26
|
+
/** Cloud tier: let the model read the repository while it reviews (default on). */
|
|
27
|
+
agentic?: boolean;
|
|
28
|
+
/** Cloud tier: review only the riskiest changed functions. */
|
|
29
|
+
router?: RouterPolicy;
|
|
30
|
+
/** Which of the team's review lessons the reviewer is told about. */
|
|
31
|
+
reviewLessons?: LessonMode;
|
|
32
|
+
/** Show the reviewer the repository's own rules (AGENTS.md, …) that name what the change touches. */
|
|
33
|
+
repoRules?: boolean;
|
|
34
|
+
/** Whole-run budget: files not started in time are skipped and counted, never an error. */
|
|
35
|
+
budgetMs?: number;
|
|
23
36
|
/** Ask intent questions at engine-proven sites in scoped reviews (deep-intent.ts). */
|
|
24
37
|
intentChecks?: boolean;
|
|
25
38
|
onProgress?: (message: string) => void;
|
|
@@ -34,6 +47,15 @@ export interface DeepRunOutcome {
|
|
|
34
47
|
/** Code mode: findings the model proposed, and how many the self-check withdrew. */
|
|
35
48
|
findingsProposed?: number;
|
|
36
49
|
findingsWithdrawn?: number;
|
|
50
|
+
/** Findings the grounding check dropped, by reason. */
|
|
51
|
+
findingsRejected?: Record<string, number>;
|
|
52
|
+
/** Files the run budget left unreviewed. */
|
|
53
|
+
filesSkipped?: number;
|
|
54
|
+
/** Repository lookups the model made (agentic review). */
|
|
55
|
+
toolCalls?: number;
|
|
56
|
+
router?: RouterStats;
|
|
57
|
+
/** Tokens and cost, for cloud providers. */
|
|
58
|
+
usage?: InferenceUsage;
|
|
37
59
|
/** Model actually used (the stock fallback is named as such). */
|
|
38
60
|
model?: string;
|
|
39
61
|
modelFallback?: boolean;
|
|
@@ -50,6 +72,11 @@ export declare class DeepAnalysisGate extends Gate {
|
|
|
50
72
|
run(context: GateContext): Promise<Failure[]>;
|
|
51
73
|
private setupProvider;
|
|
52
74
|
private reviewCode;
|
|
75
|
+
/** A cloud model that can call tools reviews a change as one PR: it sees how files relate. */
|
|
76
|
+
private reviewsWholePr;
|
|
77
|
+
private reviewPr;
|
|
78
|
+
/** Cloud only: a paid model reviews the riskiest changed functions; local tiers cost nothing to run. */
|
|
79
|
+
private route;
|
|
53
80
|
/** The max and cloud tiers read reference material and review twice; the small models do not. */
|
|
54
81
|
private referenceOptions;
|
|
55
82
|
private analyzeFacts;
|
|
@@ -17,6 +17,14 @@ import { SidecarProvider } from '../inference/sidecar-provider.js';
|
|
|
17
17
|
import { extractFacts, verifyFindings } from '../deep/index.js';
|
|
18
18
|
import { runFactsPass } from '../deep/facts-pass.js';
|
|
19
19
|
import { runCodePass } from '../deep/code-pass.js';
|
|
20
|
+
import { routeFiles } from '../deep/router.js';
|
|
21
|
+
import { reviewedKeys } from '../review/ledger.js';
|
|
22
|
+
import { buildReviewTask } from '../review/review-task.js';
|
|
23
|
+
import { reviewPullRequest } from '../deep/pr-review.js';
|
|
24
|
+
import { relatedChanges } from '../deep/related-changes.js';
|
|
25
|
+
import { rankChangedFunctions } from '../deep/risk.js';
|
|
26
|
+
import { lessonsForDiff, lessonsSection } from '../review-learning/team-lessons.js';
|
|
27
|
+
import { rulesForDiff, rulesSection } from '../review-learning/repo-rules.js';
|
|
20
28
|
import { verifyCodeFindings } from '../deep/code-verifier.js';
|
|
21
29
|
import { runIntentChecks } from './deep-intent.js';
|
|
22
30
|
import { checkLocalPatterns } from '../storage/local-memory.js';
|
|
@@ -35,6 +43,8 @@ const CLOUD_SOURCE_CHARS = 60_000;
|
|
|
35
43
|
/** Reference material (removed lines, callees, callers, PR intent) for the max and cloud tiers. */
|
|
36
44
|
const LOCAL_REFERENCE_CHARS = 12_000;
|
|
37
45
|
const CLOUD_REFERENCE_CHARS = 24_000;
|
|
46
|
+
/** Files reviewed at once on a cloud provider; a local sidecar serves one request at a time. */
|
|
47
|
+
const CLOUD_CONCURRENCY = 4;
|
|
38
48
|
export class DeepAnalysisGate extends Gate {
|
|
39
49
|
config;
|
|
40
50
|
provider = null;
|
|
@@ -80,6 +90,7 @@ export class DeepAnalysisGate extends Gate {
|
|
|
80
90
|
return [];
|
|
81
91
|
}
|
|
82
92
|
finally {
|
|
93
|
+
this.outcome.usage = this.provider?.usage?.();
|
|
83
94
|
this.provider?.dispose();
|
|
84
95
|
}
|
|
85
96
|
}
|
|
@@ -100,7 +111,10 @@ export class DeepAnalysisGate extends Gate {
|
|
|
100
111
|
this.outcome.modelFallback = active?.fallback;
|
|
101
112
|
onProgress?.('\n 🔒 Local sidecar/model execution. Code remains on this machine.\n');
|
|
102
113
|
}
|
|
103
|
-
async reviewCode(cwd,
|
|
114
|
+
async reviewCode(cwd, scoped) {
|
|
115
|
+
if (this.reviewsWholePr())
|
|
116
|
+
return this.reviewPr(cwd, scoped);
|
|
117
|
+
const facts = this.route(cwd, scoped);
|
|
104
118
|
this.config.onProgress?.(` Reviewing ${facts.length} scoped file(s) with source...`);
|
|
105
119
|
const result = await runCodePass(this.provider, facts, {
|
|
106
120
|
cwd,
|
|
@@ -109,11 +123,68 @@ export class DeepAnalysisGate extends Gate {
|
|
|
109
123
|
focusLines: this.config.options.focusLines,
|
|
110
124
|
reference: this.referenceOptions(),
|
|
111
125
|
onProgress: this.config.onProgress,
|
|
126
|
+
deadline: this.config.budgetMs ? Date.now() + this.config.budgetMs : undefined,
|
|
127
|
+
concurrency: isCloud(this.config.options) ? CLOUD_CONCURRENCY : 1,
|
|
128
|
+
agentic: isCloud(this.config.options) && this.config.agentic !== false,
|
|
112
129
|
});
|
|
113
130
|
this.recordPass(result);
|
|
114
131
|
this.outcome.findingsProposed = result.proposed;
|
|
115
132
|
this.outcome.findingsWithdrawn = result.withdrawn;
|
|
116
|
-
|
|
133
|
+
this.outcome.filesSkipped = result.skipped;
|
|
134
|
+
this.outcome.toolCalls = result.toolCalls;
|
|
135
|
+
if (result.skipped > 0) {
|
|
136
|
+
if (this.outcome.status === 'ok')
|
|
137
|
+
this.outcome.status = 'partial';
|
|
138
|
+
this.config.onProgress?.(` ⚠ Run budget reached: ${result.skipped} file(s) not reviewed.`);
|
|
139
|
+
}
|
|
140
|
+
const rejected = {};
|
|
141
|
+
const verified = verifyCodeFindings(result.findings, result.contexts, rejected);
|
|
142
|
+
this.outcome.findingsRejected = rejected;
|
|
143
|
+
return verified;
|
|
144
|
+
}
|
|
145
|
+
/** A cloud model that can call tools reviews a change as one PR: it sees how files relate. */
|
|
146
|
+
reviewsWholePr() {
|
|
147
|
+
const { options } = this.config;
|
|
148
|
+
return isCloud(options) && this.config.agentic !== false && !!this.provider?.chat && !!options.diff;
|
|
149
|
+
}
|
|
150
|
+
async reviewPr(cwd, scoped) {
|
|
151
|
+
const { options } = this.config;
|
|
152
|
+
if (this.route(cwd, scoped).length === 0) {
|
|
153
|
+
this.config.onProgress?.(' Router: no risky change for the model; the gates cover this PR.');
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
const focus = buildReviewTask(cwd, options.diff, this.config.router).items;
|
|
157
|
+
const changedFiles = Object.keys(options.focusLines ?? {});
|
|
158
|
+
const related = relatedChanges(cwd, changedFiles, rankChangedFunctions(cwd, options.focusLines ?? {}, options.removedLines));
|
|
159
|
+
this.config.onProgress?.(` Reviewing the PR as a whole (${focus.length} risky function(s) first)...`);
|
|
160
|
+
try {
|
|
161
|
+
const result = await reviewPullRequest(this.provider, { cwd, diff: options.diff, focus, related, lessons: lessonsSection(lessonsForDiff(cwd, options.diff, this.config.reviewLessons)),
|
|
162
|
+
rules: rulesSection(rulesForDiff(cwd, options.diff, this.config.repoRules)), prBody: options.prBody }, inferenceOptions(this.config));
|
|
163
|
+
this.recordPass({ findings: [], chunksTotal: 1, chunksFailed: 0 });
|
|
164
|
+
this.outcome.findingsProposed = result.findings.length;
|
|
165
|
+
this.outcome.findingsWithdrawn = 0;
|
|
166
|
+
this.outcome.toolCalls = result.toolCalls;
|
|
167
|
+
const rejected = {};
|
|
168
|
+
const verified = verifyCodeFindings(result.findings, result.contexts, rejected);
|
|
169
|
+
this.outcome.findingsRejected = rejected;
|
|
170
|
+
return verified;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
this.recordPass({ findings: [], chunksTotal: 1, chunksFailed: 1, firstError: error?.message ?? String(error) });
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** Cloud only: a paid model reviews the riskiest changed functions; local tiers cost nothing to run. */
|
|
178
|
+
route(cwd, facts) {
|
|
179
|
+
const { options } = this.config;
|
|
180
|
+
if (!isCloud(options) || this.config.router?.enabled === false || !options.focusLines)
|
|
181
|
+
return facts;
|
|
182
|
+
const routed = routeFiles(cwd, facts.map(f => f.path), options.focusLines, options.removedLines, this.config.router, reviewedKeys(cwd));
|
|
183
|
+
this.outcome.router = routed.stats;
|
|
184
|
+
if (routed.stats.files_skipped > 0) {
|
|
185
|
+
this.config.onProgress?.(` Router: ${routed.stats.routed} of ${routed.stats.functions} changed function(s) to the model; ${routed.stats.files_skipped} file(s) left to the gates.`);
|
|
186
|
+
}
|
|
187
|
+
return facts.filter(f => routed.files.has(f.path));
|
|
117
188
|
}
|
|
118
189
|
/** The max and cloud tiers read reference material and review twice; the small models do not. */
|
|
119
190
|
referenceOptions() {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { DeepAnalysisGate } from './deep-analysis.js';
|
|
6
6
|
import { localTier } from '../inference/types.js';
|
|
7
7
|
import { Logger } from '../utils/logger.js';
|
|
8
|
+
import { appendDeepRun } from '../review/deep-runs.js';
|
|
8
9
|
/**
|
|
9
10
|
* Run the deep gate and turn its outcome into summary, stats and failures.
|
|
10
11
|
* A deep run that could not analyze anything is ERROR with a visible
|
|
@@ -19,6 +20,11 @@ export async function runDeepAnalysis(config, context, deepOptions) {
|
|
|
19
20
|
maxTokens: config.gates.deep?.max_tokens,
|
|
20
21
|
temperature: config.gates.deep?.temperature,
|
|
21
22
|
timeoutMs: config.gates.deep?.timeout_ms,
|
|
23
|
+
budgetMs: config.gates.deep?.budget_ms,
|
|
24
|
+
agentic: config.gates.deep?.agentic,
|
|
25
|
+
router: config.gates.deep?.router,
|
|
26
|
+
reviewLessons: config.gates.deep?.review_lessons,
|
|
27
|
+
repoRules: config.gates.deep?.repo_rules,
|
|
22
28
|
intentChecks: config.gates.deep?.intent_checks,
|
|
23
29
|
onProgress: deepOptions.onProgress,
|
|
24
30
|
});
|
|
@@ -48,9 +54,17 @@ export async function runDeepAnalysis(config, context, deepOptions) {
|
|
|
48
54
|
error: outcome.error,
|
|
49
55
|
findings_proposed: outcome.findingsProposed,
|
|
50
56
|
findings_withdrawn: outcome.findingsWithdrawn,
|
|
57
|
+
findings_rejected: outcome.findingsRejected,
|
|
58
|
+
files_skipped: outcome.filesSkipped,
|
|
59
|
+
tool_calls: outcome.toolCalls,
|
|
60
|
+
router: outcome.router,
|
|
61
|
+
input_tokens: outcome.usage?.inputTokens,
|
|
62
|
+
output_tokens: outcome.usage?.outputTokens,
|
|
63
|
+
cost_usd: outcome.usage?.costUsd,
|
|
51
64
|
findings_count: findings.length,
|
|
52
65
|
findings_verified: findings.filter(f => f.verified).length,
|
|
53
66
|
};
|
|
67
|
+
appendDeepRun(context.cwd, stats);
|
|
54
68
|
if (outcome.status === 'error') {
|
|
55
69
|
return {
|
|
56
70
|
failures: [{
|
|
@@ -106,6 +106,9 @@ export class FrontendSecretExposureGate extends Gate {
|
|
|
106
106
|
// Skip files guarded by Next.js server-only import
|
|
107
107
|
if (/import\s+['"]server-only['"]/.test(content))
|
|
108
108
|
continue;
|
|
109
|
+
// A file outside any frontend path that imports Node built-ins cannot be bundled for a browser.
|
|
110
|
+
if (fileContext === 'ambiguous' && importsNodeBuiltins(content))
|
|
111
|
+
continue;
|
|
109
112
|
this.scanFile(content, file, fileContext, exposures);
|
|
110
113
|
}
|
|
111
114
|
catch {
|
|
@@ -251,3 +254,8 @@ export class FrontendSecretExposureGate extends Gate {
|
|
|
251
254
|
return failures;
|
|
252
255
|
}
|
|
253
256
|
}
|
|
257
|
+
const NODE_BUILTINS = /(?:from\s+|require\(\s*|import\(\s*)['"](?:node:[\w/]+|fs|fs\/promises|fs-extra|child_process|os|net|tls|worker_threads|cluster|module)['"]/;
|
|
258
|
+
/** Imports a Node-only module: server code, whatever its path. */
|
|
259
|
+
export function importsNodeBuiltins(content) {
|
|
260
|
+
return NODE_BUILTINS.test(content);
|
|
261
|
+
}
|
|
@@ -38,6 +38,20 @@ describe('FrontendSecretExposureGate', () => {
|
|
|
38
38
|
const failures = await gate.run({ cwd: testDir });
|
|
39
39
|
expect(failures.length).toBeGreaterThan(0);
|
|
40
40
|
});
|
|
41
|
+
it('treats a file outside frontend paths that imports Node built-ins as server code', async () => {
|
|
42
|
+
const cli = path.join(testDir, 'packages/cli/src/commands/post.ts');
|
|
43
|
+
const shared = path.join(testDir, 'packages/shared/src/config.ts');
|
|
44
|
+
const component = path.join(testDir, 'src/components/Admin.tsx');
|
|
45
|
+
for (const file of [cli, shared, component])
|
|
46
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
47
|
+
fs.writeFileSync(cli, "import { execFileSync } from 'child_process';\nexport const token = process.env.GITHUB_TOKEN;\n");
|
|
48
|
+
fs.writeFileSync(shared, 'export const token = process.env.GITHUB_TOKEN;\n');
|
|
49
|
+
fs.writeFileSync(component, "import fs from 'node:fs';\nexport const key = process.env.STRIPE_SECRET_KEY;\n");
|
|
50
|
+
const files = (await new FrontendSecretExposureGate().run({ cwd: testDir })).flatMap(f => f.files ?? []);
|
|
51
|
+
expect(files).not.toContain('packages/cli/src/commands/post.ts'); // Node-only: never bundled
|
|
52
|
+
expect(files).toContain('packages/shared/src/config.ts'); // could be bundled: still flagged
|
|
53
|
+
expect(files).toContain('src/components/Admin.tsx'); // a frontend path stays frontend
|
|
54
|
+
});
|
|
41
55
|
it('does not flag public env prefixes in client files', async () => {
|
|
42
56
|
const filePath = path.join(testDir, 'components/Header.tsx');
|
|
43
57
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
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 { isNuxtProvided, NuxtRoots } from './framework-modules.js';
|
|
6
|
+
let dir;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nuxt-'));
|
|
9
|
+
fs.mkdirSync(path.join(dir, 'docs/server/api'), { recursive: true });
|
|
10
|
+
fs.writeFileSync(path.join(dir, 'docs/package.json'), JSON.stringify({ devDependencies: { nuxt: '^4.0.0' } }));
|
|
11
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ dependencies: { vue: '^3.0.0' } }));
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
|
|
14
|
+
describe('Nuxt-provided modules', () => {
|
|
15
|
+
it('recognises aliases, virtual modules, the generated .nuxt dir and packages Nuxt supplies', () => {
|
|
16
|
+
for (const [spec, pkg] of [['~~/server/api/ai.post', '~~'], ['~/utils/x', '~'], ['@/components/A.vue', '@'], ['#imports', '#imports'], ['../../.nuxt/ui', '..'], ['h3', 'h3'], ['@vue/shared', '@vue/shared']]) {
|
|
17
|
+
expect(isNuxtProvided(spec, pkg)).toBe(true);
|
|
18
|
+
}
|
|
19
|
+
expect(isNuxtProvided('left-pad', 'left-pad')).toBe(false);
|
|
20
|
+
expect(isNuxtProvided('@scope/pkg', '@scope/pkg')).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
it('applies only inside a project that depends on nuxt', async () => {
|
|
23
|
+
const roots = new NuxtRoots(dir);
|
|
24
|
+
expect(await roots.rootFor('docs/server/api/ai.post.ts')).toBe(path.join(dir, 'docs'));
|
|
25
|
+
expect(await roots.rootFor('src/index.ts')).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export declare class
|
|
1
|
+
/** A framework's project root for a file (nearest package.json depending on it), cached per directory. */
|
|
2
|
+
export declare class FrameworkRoots {
|
|
3
3
|
private readonly cwd;
|
|
4
|
+
private readonly frameworkPackage;
|
|
4
5
|
private cache;
|
|
5
|
-
constructor(cwd: string);
|
|
6
|
+
constructor(cwd: string, frameworkPackage: string);
|
|
6
7
|
rootFor(file: string): Promise<string | null>;
|
|
7
8
|
}
|
|
9
|
+
export declare class SvelteKitRoots extends FrameworkRoots {
|
|
10
|
+
constructor(cwd: string);
|
|
11
|
+
}
|
|
12
|
+
export declare class NuxtRoots extends FrameworkRoots {
|
|
13
|
+
constructor(cwd: string);
|
|
14
|
+
}
|
|
15
|
+
/** In a Nuxt app: an alias, a virtual `#` module, the generated `.nuxt/`, or a package Nuxt supplies. */
|
|
16
|
+
export declare function isNuxtProvided(importPath: string, packageName: string): boolean;
|
|
8
17
|
/** `$app/*`, `$env/*`, `$service-worker`, or a route's generated `./$types`. */
|
|
9
18
|
export declare function isSvelteKitProvided(importPath: string): boolean;
|
|
10
19
|
/**
|
|
@@ -14,12 +14,24 @@ const SVELTEKIT_PACKAGE = '@sveltejs/kit';
|
|
|
14
14
|
const SVELTEKIT_VIRTUAL = /^(?:\$app|\$env)\/|^\$service-worker$/;
|
|
15
15
|
const GENERATED_TYPES = /^\.{1,2}\/(?:.*\/)?\$types(?:\.d\.ts|\.js)?$/;
|
|
16
16
|
const LIB_ALIAS = /^\$lib(?:\/(.*))?$/;
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const NUXT_PACKAGE = 'nuxt';
|
|
18
|
+
/** Nuxt's aliases (`~`/`@` = srcDir, `~~`/`@@` = rootDir) and virtual `#imports`, `#app`, `#build`, …. */
|
|
19
|
+
const NUXT_ALIAS = /^(?:~~?|@@?)(?:\/|$)|^#/;
|
|
20
|
+
/** Nuxt's generated build directory, imported relatively. */
|
|
21
|
+
const NUXT_GENERATED = /(?:^|\/)\.nuxt(?:\/|$)/;
|
|
22
|
+
/** Packages Nuxt and Nitro supply to every app, imported without being declared. */
|
|
23
|
+
const NUXT_SUPPLIED = new Set([
|
|
24
|
+
'h3', 'ofetch', 'ufo', 'defu', 'destr', 'ohash', 'pathe', 'scule', 'consola', 'hookable', 'unstorage', 'unctx',
|
|
25
|
+
'cookie-es', 'radix3', 'knitwork', 'nitropack', 'vue', 'vue-router', 'unhead', '@unhead/vue', '@nuxt/schema', '@nuxt/kit',
|
|
26
|
+
]);
|
|
27
|
+
/** A framework's project root for a file (nearest package.json depending on it), cached per directory. */
|
|
28
|
+
export class FrameworkRoots {
|
|
19
29
|
cwd;
|
|
30
|
+
frameworkPackage;
|
|
20
31
|
cache = new Map();
|
|
21
|
-
constructor(cwd) {
|
|
32
|
+
constructor(cwd, frameworkPackage) {
|
|
22
33
|
this.cwd = cwd;
|
|
34
|
+
this.frameworkPackage = frameworkPackage;
|
|
23
35
|
}
|
|
24
36
|
async rootFor(file) {
|
|
25
37
|
const top = path.resolve(this.cwd);
|
|
@@ -32,7 +44,7 @@ export class SvelteKitRoots {
|
|
|
32
44
|
break;
|
|
33
45
|
}
|
|
34
46
|
visited.push(dir);
|
|
35
|
-
if (await
|
|
47
|
+
if (await declares(path.join(dir, 'package.json'), this.frameworkPackage)) {
|
|
36
48
|
found = dir;
|
|
37
49
|
break;
|
|
38
50
|
}
|
|
@@ -46,13 +58,27 @@ export class SvelteKitRoots {
|
|
|
46
58
|
return found;
|
|
47
59
|
}
|
|
48
60
|
}
|
|
49
|
-
|
|
61
|
+
export class SvelteKitRoots extends FrameworkRoots {
|
|
62
|
+
constructor(cwd) {
|
|
63
|
+
super(cwd, SVELTEKIT_PACKAGE);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export class NuxtRoots extends FrameworkRoots {
|
|
67
|
+
constructor(cwd) {
|
|
68
|
+
super(cwd, NUXT_PACKAGE);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** In a Nuxt app: an alias, a virtual `#` module, the generated `.nuxt/`, or a package Nuxt supplies. */
|
|
72
|
+
export function isNuxtProvided(importPath, packageName) {
|
|
73
|
+
return NUXT_ALIAS.test(importPath) || NUXT_GENERATED.test(importPath) || NUXT_SUPPLIED.has(packageName) || packageName.startsWith('@vue/');
|
|
74
|
+
}
|
|
75
|
+
async function declares(packageJsonPath, frameworkPackage) {
|
|
50
76
|
if (!(await fs.pathExists(packageJsonPath)))
|
|
51
77
|
return false;
|
|
52
78
|
try {
|
|
53
79
|
const pkg = await fs.readJson(packageJsonPath);
|
|
54
80
|
return [pkg?.dependencies, pkg?.devDependencies, pkg?.peerDependencies]
|
|
55
|
-
.some(deps => !!deps && Object.prototype.hasOwnProperty.call(deps,
|
|
81
|
+
.some(deps => !!deps && Object.prototype.hasOwnProperty.call(deps, frameworkPackage));
|
|
56
82
|
}
|
|
57
83
|
catch {
|
|
58
84
|
return false;
|
|
@@ -24,7 +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
|
+
import { NuxtRoots, SvelteKitRoots } from './framework-modules.js';
|
|
28
28
|
import { checkPyImports } from './python-resolver.js';
|
|
29
29
|
import { discoverWorkspacePackages, loadPackageJson } from './manifest-discovery.js';
|
|
30
30
|
export class HallucinatedImportsGate extends Gate {
|
|
@@ -84,6 +84,7 @@ export class HallucinatedImportsGate extends Gate {
|
|
|
84
84
|
const jsContext = {
|
|
85
85
|
cwd: context.cwd, projectFiles, rootDeps, depCacheByDir, hasNodeModules, hallucinated, tsPathCacheByDir,
|
|
86
86
|
kitRoots: new SvelteKitRoots(context.cwd),
|
|
87
|
+
nuxtRoots: new NuxtRoots(context.cwd),
|
|
87
88
|
shouldIgnore: (importPath) => this.shouldIgnore(importPath),
|
|
88
89
|
resolveRelativeImport: (fromFile, importPath, files) => this.resolveRelativeImport(fromFile, importPath, files),
|
|
89
90
|
extractPackageName: (importPath) => this.extractPackageName(importPath),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { typeOnlySpecifiers, typesPackageFor } from './js-resolver.js';
|
|
3
|
+
describe('type-only imports', () => {
|
|
4
|
+
it('finds specifiers imported only for types, and everything in a .d.ts file', () => {
|
|
5
|
+
const ts = "import type { Context } from 'aws-lambda';\nimport { handler } from 'aws-lambda';\nexport type { Ev } from 'events-lib';\n";
|
|
6
|
+
expect([...typeOnlySpecifiers(ts, 'src/a.ts')]).toEqual(['aws-lambda', 'events-lib']);
|
|
7
|
+
const dts = "import type { Writable } from 'node:stream';\nimport { Context } from 'aws-lambda';\ntype X = import('express').Request;\n";
|
|
8
|
+
expect([...typeOnlySpecifiers(dts, 'src/global.d.ts')]).toEqual(['node:stream', 'aws-lambda', 'express']);
|
|
9
|
+
});
|
|
10
|
+
it('names the DefinitelyTyped package, scoped ones included', () => {
|
|
11
|
+
expect(typesPackageFor('aws-lambda')).toBe('@types/aws-lambda');
|
|
12
|
+
expect(typesPackageFor('@babel/core')).toBe('@types/babel__core');
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - node_modules dependency verification
|
|
10
10
|
*/
|
|
11
11
|
import { HallucinatedImport } from './index.js';
|
|
12
|
-
import { type SvelteKitRoots } from './framework-modules.js';
|
|
12
|
+
import { type NuxtRoots, type SvelteKitRoots } from './framework-modules.js';
|
|
13
13
|
import { type TsPathConfig } from './tsconfig-paths.js';
|
|
14
14
|
/** Everything a JS/TS import check needs besides the file itself. */
|
|
15
15
|
export interface JsImportContext {
|
|
@@ -21,6 +21,7 @@ export interface JsImportContext {
|
|
|
21
21
|
hallucinated: HallucinatedImport[];
|
|
22
22
|
tsPathCacheByDir: Map<string, TsPathConfig | null>;
|
|
23
23
|
kitRoots: SvelteKitRoots;
|
|
24
|
+
nuxtRoots?: NuxtRoots;
|
|
24
25
|
shouldIgnore: (importPath: string) => boolean;
|
|
25
26
|
resolveRelativeImport: (fromFile: string, importPath: string, projectFiles: Set<string>) => boolean;
|
|
26
27
|
extractPackageName: (importPath: string) => string;
|
|
@@ -29,6 +30,10 @@ export interface JsImportContext {
|
|
|
29
30
|
* Check JavaScript/TypeScript imports in a source file and add hallucinated findings.
|
|
30
31
|
*/
|
|
31
32
|
export declare function checkJSImports(content: string, file: string, ctx: JsImportContext): Promise<void>;
|
|
33
|
+
/** Specifiers imported only for types: `import type`, `export type … from`, or anything in a .d.ts file. */
|
|
34
|
+
export declare function typeOnlySpecifiers(content: string, file: string): Set<string>;
|
|
35
|
+
/** `@types/x` for `x`; `@types/scope__x` for `@scope/x`. */
|
|
36
|
+
export declare function typesPackageFor(pkgName: string): string;
|
|
32
37
|
/**
|
|
33
38
|
* Bundler conventions: a query on a relative import (`./logo.svg?raw`, `./worker.ts?worker`)
|
|
34
39
|
* names a transform of that file, so the file is what must exist; a `virtual:` module or a
|