@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.
Files changed (123) hide show
  1. package/dist/deep/agent-review.d.ts +19 -0
  2. package/dist/deep/agent-review.js +19 -0
  3. package/dist/deep/agent-review.test.d.ts +1 -0
  4. package/dist/deep/agent-review.test.js +53 -0
  5. package/dist/deep/code-pass.d.ts +10 -0
  6. package/dist/deep/code-pass.js +64 -33
  7. package/dist/deep/code-pass.test.js +53 -12
  8. package/dist/deep/code-verifier.d.ts +3 -1
  9. package/dist/deep/code-verifier.js +11 -11
  10. package/dist/deep/code-verifier.test.js +8 -0
  11. package/dist/deep/pr-diff.d.ts +7 -0
  12. package/dist/deep/pr-diff.js +47 -0
  13. package/dist/deep/pr-review.d.ts +33 -0
  14. package/dist/deep/pr-review.js +81 -0
  15. package/dist/deep/pr-review.test.d.ts +1 -0
  16. package/dist/deep/pr-review.test.js +80 -0
  17. package/dist/deep/related-changes.d.ts +13 -0
  18. package/dist/deep/related-changes.js +56 -0
  19. package/dist/deep/related-changes.test.d.ts +1 -0
  20. package/dist/deep/related-changes.test.js +33 -0
  21. package/dist/deep/review-tools.d.ts +16 -0
  22. package/dist/deep/review-tools.js +111 -0
  23. package/dist/deep/review-tools.test.d.ts +1 -0
  24. package/dist/deep/review-tools.test.js +39 -0
  25. package/dist/deep/risk.d.ts +41 -0
  26. package/dist/deep/risk.js +135 -0
  27. package/dist/deep/risk.test.d.ts +1 -0
  28. package/dist/deep/risk.test.js +63 -0
  29. package/dist/deep/router.d.ts +27 -0
  30. package/dist/deep/router.js +15 -0
  31. package/dist/deep/self-check.d.ts +14 -9
  32. package/dist/deep/self-check.js +41 -32
  33. package/dist/deep/tool-loop.d.ts +19 -0
  34. package/dist/deep/tool-loop.js +23 -0
  35. package/dist/gates/deep-analysis.d.ts +27 -0
  36. package/dist/gates/deep-analysis.js +73 -2
  37. package/dist/gates/deep-runner.js +14 -0
  38. package/dist/gates/frontend-secret-exposure.d.ts +2 -0
  39. package/dist/gates/frontend-secret-exposure.js +8 -0
  40. package/dist/gates/frontend-secret-exposure.test.js +14 -0
  41. package/dist/gates/hallucinated-imports/framework-modules-nuxt.test.d.ts +1 -0
  42. package/dist/gates/hallucinated-imports/framework-modules-nuxt.test.js +27 -0
  43. package/dist/gates/hallucinated-imports/framework-modules.d.ts +12 -3
  44. package/dist/gates/hallucinated-imports/framework-modules.js +32 -6
  45. package/dist/gates/hallucinated-imports/index.js +2 -1
  46. package/dist/gates/hallucinated-imports/js-resolver-types.test.d.ts +1 -0
  47. package/dist/gates/hallucinated-imports/js-resolver-types.test.js +14 -0
  48. package/dist/gates/hallucinated-imports/js-resolver.d.ts +6 -1
  49. package/dist/gates/hallucinated-imports/js-resolver.js +25 -3
  50. package/dist/gates/phantom-apis.js +2 -2
  51. package/dist/gates/phantom-apis.test.js +10 -0
  52. package/dist/gates/promise-safety.js +1 -1
  53. package/dist/gates/test-quality-matchers.js +3 -0
  54. package/dist/gates/test-quality.test.js +13 -0
  55. package/dist/hooks/checker.test.js +9 -0
  56. package/dist/hooks/stop-review.d.ts +2 -0
  57. package/dist/hooks/stop-review.js +19 -3
  58. package/dist/hooks/templates.d.ts +2 -0
  59. package/dist/hooks/templates.js +16 -2
  60. package/dist/hooks/templates.test.d.ts +1 -0
  61. package/dist/hooks/templates.test.js +16 -0
  62. package/dist/index.d.ts +13 -0
  63. package/dist/index.js +13 -0
  64. package/dist/inference/cloud-provider.d.ts +12 -1
  65. package/dist/inference/cloud-provider.js +137 -2
  66. package/dist/inference/cloud-provider.test.d.ts +1 -0
  67. package/dist/inference/cloud-provider.test.js +118 -0
  68. package/dist/inference/pricing.d.ts +15 -0
  69. package/dist/inference/pricing.js +13 -0
  70. package/dist/inference/types.d.ts +42 -0
  71. package/dist/review/changed-function-spans.d.ts +4 -0
  72. package/dist/review/changed-function-spans.js +48 -0
  73. package/dist/review/changed-lines.d.ts +9 -3
  74. package/dist/review/changed-lines.js +19 -2
  75. package/dist/review/deep-runs.d.ts +22 -0
  76. package/dist/review/deep-runs.js +59 -0
  77. package/dist/review/deep-runs.test.d.ts +1 -0
  78. package/dist/review/deep-runs.test.js +23 -0
  79. package/dist/review/generated-files.d.ts +3 -0
  80. package/dist/review/generated-files.js +35 -0
  81. package/dist/review/generated-files.test.d.ts +1 -0
  82. package/dist/review/generated-files.test.js +25 -0
  83. package/dist/review/ledger.d.ts +27 -0
  84. package/dist/review/ledger.js +72 -0
  85. package/dist/review/quiet.d.ts +16 -0
  86. package/dist/review/quiet.js +71 -0
  87. package/dist/review/quiet.test.d.ts +1 -0
  88. package/dist/review/quiet.test.js +27 -0
  89. package/dist/review/review-ack.d.ts +16 -0
  90. package/dist/review/review-ack.js +25 -0
  91. package/dist/review/review-task.d.ts +40 -0
  92. package/dist/review/review-task.js +50 -0
  93. package/dist/review/review-task.test.d.ts +1 -0
  94. package/dist/review/review-task.test.js +80 -0
  95. package/dist/review/review.d.ts +12 -1
  96. package/dist/review/review.js +16 -6
  97. package/dist/review/review.test.js +25 -0
  98. package/dist/review-learning/acted-on.d.ts +25 -0
  99. package/dist/review-learning/acted-on.js +69 -0
  100. package/dist/review-learning/learn-from-reviews.d.ts +34 -0
  101. package/dist/review-learning/learn-from-reviews.js +65 -0
  102. package/dist/review-learning/lessons.d.ts +52 -0
  103. package/dist/review-learning/lessons.js +149 -0
  104. package/dist/review-learning/repo-rules.d.ts +16 -0
  105. package/dist/review-learning/repo-rules.js +84 -0
  106. package/dist/review-learning/repo-rules.test.d.ts +1 -0
  107. package/dist/review-learning/repo-rules.test.js +39 -0
  108. package/dist/review-learning/review-learning.test.d.ts +1 -0
  109. package/dist/review-learning/review-learning.test.js +104 -0
  110. package/dist/review-learning/team-lessons.d.ts +4 -0
  111. package/dist/review-learning/team-lessons.js +24 -0
  112. package/dist/semantic/rules/credential-redirect.js +4 -1
  113. package/dist/settings.js +4 -1
  114. package/dist/telemetry/telemetry.d.ts +28 -0
  115. package/dist/telemetry/telemetry.js +152 -0
  116. package/dist/telemetry/telemetry.test.d.ts +1 -0
  117. package/dist/telemetry/telemetry.test.js +59 -0
  118. package/dist/telemetry/token.d.ts +1 -0
  119. package/dist/telemetry/token.js +1 -0
  120. package/dist/templates/universal-config.js +7 -3
  121. package/dist/types/index.d.ts +276 -10
  122. package/dist/types/index.js +38 -5
  123. package/package.json +6 -6
@@ -1,12 +1,17 @@
1
1
  /**
2
- * Self-check: the model re-reads each finding against the code and confirms
3
- * or withdraws it.
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
- * finding by finding, whether it would misbehave at runtime keeps that extra
8
- * recall from arriving as noise. A check that cannot run keeps the finding:
9
- * an unavailable model is not evidence against it.
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, finding: DeepFinding): string;
20
- /** true, false, or undefined when the reply is not a verdict. */
21
- export declare function parseVerdict(reply: string): boolean | undefined;
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>;
@@ -1,49 +1,58 @@
1
- const MAX_CHECKED = 8;
2
- const VERDICT_SCHEMA = {
1
+ const MAX_CHECKED = 12;
2
+ const VERDICTS_SCHEMA = {
3
3
  type: 'object',
4
- properties: { real: { type: 'boolean' }, reason: { type: 'string' } },
5
- required: ['real'],
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 result = { kept: [], withdrawn: 0, failed: 0 };
9
- for (const [index, finding] of findings.entries()) {
10
- if (index >= MAX_CHECKED) {
11
- result.kept.push(finding);
12
- continue;
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
- return result;
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, finding) {
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
- 'You reported a defect in a code review. Check it against the code before it is posted.',
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 at ${context.file}:${finding.line}: ${finding.description}`,
34
- 'Is this a real defect: would this code misbehave at runtime or in production as described? '
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
- /** true, false, or undefined when the reply is not a verdict. */
40
- export function parseVerdict(reply) {
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 value = JSON.parse(json).real;
44
- return typeof value === 'boolean' ? value : undefined;
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
- return undefined;
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, facts) {
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
- return verifyCodeFindings(result.findings, result.contexts);
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: [{
@@ -54,3 +54,5 @@ export declare class FrontendSecretExposureGate extends Gate {
54
54
  private isDummyValue;
55
55
  private toFailures;
56
56
  }
57
+ /** Imports a Node-only module: server code, whatever its path. */
58
+ export declare function importsNodeBuiltins(content: string): boolean;
@@ -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,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
- /** SvelteKit project root for a file (nearest package.json depending on the kit), cached per directory. */
2
- export declare class SvelteKitRoots {
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
- /** SvelteKit project root for a file (nearest package.json depending on the kit), cached per directory. */
18
- export class SvelteKitRoots {
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 declaresKit(path.join(dir, 'package.json'))) {
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
- async function declaresKit(packageJsonPath) {
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, SVELTEKIT_PACKAGE));
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,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