@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
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Deep Analysis Gate — LLM-powered code
|
|
2
|
+
* Deep Analysis Gate — LLM-powered code analysis.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* - Unscoped (`rigour check --deep`): repo-wide analysis from AST facts.
|
|
6
|
+
* AST extracts facts, the LLM interprets them, AST verifies the findings.
|
|
7
|
+
* - Scoped (`rigour check <paths> --deep`, `rigour review --deep`): each
|
|
8
|
+
* scoped file is reviewed with its numbered source; findings must cite a
|
|
9
|
+
* line and identifiers that were actually sent.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
11
|
+
* The gate records an outcome. A run where setup failed or every chunk
|
|
12
|
+
* failed is an error, never a clean pass.
|
|
10
13
|
*/
|
|
11
14
|
import { Gate, GateContext } from './base.js';
|
|
12
15
|
import { Failure, Provenance, DeepOptions } from '../types/index.js';
|
|
@@ -17,14 +20,36 @@ export interface DeepGateConfig {
|
|
|
17
20
|
maxTokens?: number;
|
|
18
21
|
temperature?: number;
|
|
19
22
|
timeoutMs?: number;
|
|
23
|
+
/** Ask intent questions at engine-proven sites in scoped reviews (deep-intent.ts). */
|
|
24
|
+
intentChecks?: boolean;
|
|
20
25
|
onProgress?: (message: string) => void;
|
|
21
26
|
}
|
|
27
|
+
export type DeepRunStatus = 'ok' | 'partial' | 'error';
|
|
28
|
+
export interface DeepRunOutcome {
|
|
29
|
+
status: DeepRunStatus;
|
|
30
|
+
mode: 'facts' | 'code';
|
|
31
|
+
filesAnalyzed: number;
|
|
32
|
+
chunksTotal: number;
|
|
33
|
+
chunksFailed: number;
|
|
34
|
+
/** Model actually used (the stock fallback is named as such). */
|
|
35
|
+
model?: string;
|
|
36
|
+
modelFallback?: boolean;
|
|
37
|
+
error?: string;
|
|
38
|
+
}
|
|
22
39
|
export declare class DeepAnalysisGate extends Gate {
|
|
23
40
|
private config;
|
|
24
41
|
private provider;
|
|
42
|
+
private outcome;
|
|
25
43
|
constructor(config: DeepGateConfig);
|
|
26
44
|
protected get provenance(): Provenance;
|
|
27
|
-
/**
|
|
28
|
-
|
|
45
|
+
/** Outcome of the last run(). */
|
|
46
|
+
getOutcome(): DeepRunOutcome;
|
|
29
47
|
run(context: GateContext): Promise<Failure[]>;
|
|
48
|
+
private setupProvider;
|
|
49
|
+
private reviewCode;
|
|
50
|
+
private analyzeFacts;
|
|
51
|
+
private recordPass;
|
|
52
|
+
private localMemoryFindings;
|
|
53
|
+
private createCloudAgent;
|
|
54
|
+
private toFailure;
|
|
30
55
|
}
|
|
@@ -1,28 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Deep Analysis Gate — LLM-powered code
|
|
2
|
+
* Deep Analysis Gate — LLM-powered code analysis.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* - Unscoped (`rigour check --deep`): repo-wide analysis from AST facts.
|
|
6
|
+
* AST extracts facts, the LLM interprets them, AST verifies the findings.
|
|
7
|
+
* - Scoped (`rigour check <paths> --deep`, `rigour review --deep`): each
|
|
8
|
+
* scoped file is reviewed with its numbered source; findings must cite a
|
|
9
|
+
* line and identifiers that were actually sent.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
11
|
+
* The gate records an outcome. A run where setup failed or every chunk
|
|
12
|
+
* failed is an error, never a clean pass.
|
|
10
13
|
*/
|
|
11
14
|
import { Gate } from './base.js';
|
|
12
15
|
import { createProvider } from '../inference/index.js';
|
|
13
|
-
import {
|
|
16
|
+
import { SidecarProvider } from '../inference/sidecar-provider.js';
|
|
17
|
+
import { extractFacts, verifyFindings } from '../deep/index.js';
|
|
18
|
+
import { runFactsPass } from '../deep/facts-pass.js';
|
|
19
|
+
import { runCodePass } from '../deep/code-pass.js';
|
|
20
|
+
import { verifyCodeFindings } from '../deep/code-verifier.js';
|
|
21
|
+
import { runIntentChecks } from './deep-intent.js';
|
|
14
22
|
import { checkLocalPatterns } from '../storage/local-memory.js';
|
|
23
|
+
import { isScoped } from '../utils/scope.js';
|
|
15
24
|
import { Logger } from '../utils/logger.js';
|
|
16
25
|
import path from 'path';
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
/** Cloud setup (API connection) must not hang a check. Local setup is bounded by download stall timeouts instead. */
|
|
27
|
+
const CLOUD_SETUP_TIMEOUT_MS = 120_000;
|
|
28
|
+
/**
|
|
29
|
+
* Source budget per file in the code-review prompt. Local: ~36k chars is
|
|
30
|
+
* ~12k tokens; measured on Apple Silicon at ~19s per file for the 1.5B model
|
|
31
|
+
* (prompt eval ~670 tok/s), inside the 60s per-call timeout.
|
|
32
|
+
*/
|
|
33
|
+
const LOCAL_SOURCE_CHARS = 36_000;
|
|
34
|
+
const CLOUD_SOURCE_CHARS = 60_000;
|
|
23
35
|
export class DeepAnalysisGate extends Gate {
|
|
24
36
|
config;
|
|
25
37
|
provider = null;
|
|
38
|
+
outcome = emptyOutcome();
|
|
26
39
|
constructor(config) {
|
|
27
40
|
super('deep-analysis', 'Deep Code Quality Analysis');
|
|
28
41
|
this.config = config;
|
|
@@ -30,321 +43,171 @@ export class DeepAnalysisGate extends Gate {
|
|
|
30
43
|
get provenance() {
|
|
31
44
|
return 'deep-analysis';
|
|
32
45
|
}
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const entryNames = [
|
|
37
|
-
'index.ts', 'index.js', 'index.tsx', 'index.jsx', 'index.mjs',
|
|
38
|
-
'main.ts', 'main.js', 'main.py', 'main.go', 'main.rs', 'main.java', 'main.kt',
|
|
39
|
-
'app.ts', 'app.js', 'app.py', 'app.go', 'app.rb',
|
|
40
|
-
'server.ts', 'server.js', 'server.py', 'server.go',
|
|
41
|
-
'mod.rs', 'lib.rs',
|
|
42
|
-
];
|
|
43
|
-
return entryNames.includes(basename);
|
|
46
|
+
/** Outcome of the last run(). */
|
|
47
|
+
getOutcome() {
|
|
48
|
+
return this.outcome;
|
|
44
49
|
}
|
|
45
50
|
async run(context) {
|
|
46
|
-
const { onProgress } = this.config;
|
|
47
|
-
const failures = [];
|
|
48
51
|
const startTime = Date.now();
|
|
52
|
+
const scoped = isScoped(context.patterns);
|
|
53
|
+
this.outcome = { ...emptyOutcome(), mode: scoped ? 'code' : 'facts' };
|
|
49
54
|
try {
|
|
50
|
-
|
|
51
|
-
onProgress?.('
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const available = await this.provider.isAvailable();
|
|
57
|
-
if (!available) {
|
|
58
|
-
onProgress?.(' ⚠ Local inference binary not found. Attempting auto-install...');
|
|
59
|
-
onProgress?.(' (This may take a moment on first run)');
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
await Promise.race([
|
|
63
|
-
this.provider.setup(onProgress),
|
|
64
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Deep analysis setup timed out.\n' +
|
|
65
|
-
' If local: run `rigour doctor` to check sidecar binary status.\n' +
|
|
66
|
-
' If cloud: check your API key with `rigour settings show`.')), SETUP_TIMEOUT_MS)),
|
|
67
|
-
]);
|
|
68
|
-
const isLocal = !this.config.options.apiKey || this.config.options.provider === 'local';
|
|
69
|
-
if (isLocal) {
|
|
70
|
-
onProgress?.('\n 🔒 Local sidecar/model execution. Code remains on this machine.\n');
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
onProgress?.(`\n ☁️ Using ${this.config.options.provider} API. Code context may be sent to the provider.\n`);
|
|
74
|
-
}
|
|
75
|
-
// Step 1: AST extracts facts
|
|
76
|
-
onProgress?.(' Extracting code facts...');
|
|
77
|
-
let allFacts = await extractFacts(context.cwd, context.ignore);
|
|
78
|
-
if (allFacts.length === 0) {
|
|
79
|
-
onProgress?.(' No analyzable files found. Check ignore patterns and file extensions.');
|
|
55
|
+
await this.setupProvider();
|
|
56
|
+
this.config.onProgress?.(' Extracting code facts...');
|
|
57
|
+
const facts = prioritize(await extractFacts(context.cwd, context.ignore, scoped ? context.patterns : undefined));
|
|
58
|
+
this.outcome.filesAnalyzed = facts.length;
|
|
59
|
+
if (facts.length === 0) {
|
|
60
|
+
this.config.onProgress?.(' No analyzable files found. Check ignore patterns and file extensions.');
|
|
80
61
|
return [];
|
|
81
62
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// Optional soft limit — if user configures deep.maxFiles, respect it
|
|
91
|
-
// Otherwise process ALL files in batches (no hard cap)
|
|
92
|
-
if (this.config.options.maxFiles && allFacts.length > this.config.options.maxFiles) {
|
|
93
|
-
onProgress?.(` Limiting to ${this.config.options.maxFiles} files (configured in rigour.yml).`);
|
|
94
|
-
allFacts = allFacts.slice(0, this.config.options.maxFiles);
|
|
95
|
-
}
|
|
96
|
-
const agentCount = this.config.options.agents || 1;
|
|
97
|
-
const isCloud = !!this.config.options.apiKey;
|
|
98
|
-
onProgress?.(` Found ${allFacts.length} files to analyze${agentCount > 1 ? ` with ${agentCount} parallel agents` : ''}.`);
|
|
99
|
-
// Step 1.5: Check local project memory for known patterns (instant, no LLM)
|
|
100
|
-
// Wrapped in try/catch: sqlite3 may not be available in all environments
|
|
101
|
-
const fileList = allFacts.map(f => f.path).filter(Boolean);
|
|
102
|
-
let localFindings = [];
|
|
103
|
-
try {
|
|
104
|
-
localFindings = await checkLocalPatterns(context.cwd, fileList);
|
|
105
|
-
if (localFindings.length > 0) {
|
|
106
|
-
onProgress?.(` 🧠 Local memory: ${localFindings.length} known pattern(s) matched instantly.`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
catch (error) {
|
|
110
|
-
Logger.debug(`Local memory check skipped (${error.message?.substring(0, 80)})`);
|
|
111
|
-
onProgress?.(' ℹ Local memory unavailable — continuing with LLM analysis only.');
|
|
112
|
-
}
|
|
113
|
-
// Step 2: LLM interprets facts (in chunks)
|
|
114
|
-
const chunks = chunkFacts(allFacts);
|
|
115
|
-
const allFindings = [...localFindings];
|
|
116
|
-
let failedChunks = 0;
|
|
117
|
-
if (agentCount > 1 && isCloud) {
|
|
118
|
-
// ── Multi-agent mode: partition chunks across N agents, analyze in parallel ──
|
|
119
|
-
// Each agent gets its own provider instance for true parallelism.
|
|
120
|
-
// Local mode stays sequential (single sidecar process).
|
|
121
|
-
onProgress?.(` Spawning ${agentCount} parallel agents...`);
|
|
122
|
-
const agentBuckets = Array.from({ length: agentCount }, () => []);
|
|
123
|
-
chunks.forEach((chunk, i) => agentBuckets[i % agentCount].push(chunk));
|
|
124
|
-
// Create N independent provider instances
|
|
125
|
-
const agentProviders = [];
|
|
126
|
-
for (let a = 0; a < agentCount; a++) {
|
|
127
|
-
if (agentBuckets[a].length === 0)
|
|
128
|
-
continue;
|
|
129
|
-
const p = createProvider(this.config.options);
|
|
130
|
-
await p.setup(); // Already connected — cloud setup is instant after first
|
|
131
|
-
agentProviders.push(p);
|
|
132
|
-
}
|
|
133
|
-
// Run all agents in parallel
|
|
134
|
-
const agentResults = await Promise.allSettled(agentProviders.map(async (provider, agentIdx) => {
|
|
135
|
-
const bucket = agentBuckets[agentIdx];
|
|
136
|
-
const findings = [];
|
|
137
|
-
let failed = 0;
|
|
138
|
-
for (let ci = 0; ci < bucket.length; ci++) {
|
|
139
|
-
const globalIdx = agentIdx + ci * agentCount + 1;
|
|
140
|
-
onProgress?.(` Agent ${agentIdx + 1}: batch ${ci + 1}/${bucket.length} (global ${globalIdx}/${chunks.length})`);
|
|
141
|
-
const factsStr = factsToPromptString(bucket[ci]);
|
|
142
|
-
const prompt = buildAnalysisPrompt(factsStr, this.config.checks);
|
|
143
|
-
try {
|
|
144
|
-
const response = await provider.analyze(prompt, {
|
|
145
|
-
maxTokens: this.config.maxTokens || 8192,
|
|
146
|
-
temperature: this.config.temperature || 0.1,
|
|
147
|
-
timeout: this.config.timeoutMs || 120000,
|
|
148
|
-
jsonMode: true,
|
|
149
|
-
});
|
|
150
|
-
findings.push(...parseFindings(response));
|
|
151
|
-
}
|
|
152
|
-
catch (error) {
|
|
153
|
-
failed++;
|
|
154
|
-
Logger.warn(`Agent ${agentIdx + 1} chunk ${ci + 1} failed: ${error.message}`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return { findings, failed };
|
|
158
|
-
}));
|
|
159
|
-
// Merge results and dispose extra providers
|
|
160
|
-
for (let i = 0; i < agentResults.length; i++) {
|
|
161
|
-
const result = agentResults[i];
|
|
162
|
-
if (result.status === 'fulfilled') {
|
|
163
|
-
allFindings.push(...result.value.findings);
|
|
164
|
-
failedChunks += result.value.failed;
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
failedChunks += agentBuckets[i].length;
|
|
168
|
-
Logger.warn(`Agent ${i + 1} failed entirely: ${result.reason?.message || 'unknown'}`);
|
|
169
|
-
}
|
|
170
|
-
agentProviders[i]?.dispose();
|
|
171
|
-
}
|
|
172
|
-
onProgress?.(` All ${agentCount} agents completed.`);
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
// ── Single-agent mode: sequential chunk processing ──
|
|
176
|
-
let chunkIndex = 0;
|
|
177
|
-
for (const chunk of chunks) {
|
|
178
|
-
chunkIndex++;
|
|
179
|
-
onProgress?.(` Analyzing batch ${chunkIndex}/${chunks.length}...`);
|
|
180
|
-
const factsStr = factsToPromptString(chunk);
|
|
181
|
-
const prompt = buildAnalysisPrompt(factsStr, this.config.checks);
|
|
182
|
-
try {
|
|
183
|
-
const response = await this.provider.analyze(prompt, {
|
|
184
|
-
maxTokens: this.config.maxTokens || (isCloud ? 4096 : 512),
|
|
185
|
-
temperature: this.config.temperature || 0.1,
|
|
186
|
-
timeout: this.config.timeoutMs || (isCloud ? 120000 : 60000),
|
|
187
|
-
jsonMode: true,
|
|
188
|
-
});
|
|
189
|
-
const findings = parseFindings(response);
|
|
190
|
-
allFindings.push(...findings);
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
failedChunks++;
|
|
194
|
-
Logger.warn(`Chunk ${chunkIndex} inference failed: ${error.message}`);
|
|
195
|
-
onProgress?.(` ⚠ Batch ${chunkIndex} failed: ${error.message}`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
// Cross-file analysis (if we have enough files and at least some chunks succeeded)
|
|
200
|
-
if (allFacts.length >= 3 && failedChunks < chunks.length) {
|
|
201
|
-
onProgress?.(' Running cross-file analysis...');
|
|
202
|
-
try {
|
|
203
|
-
const crossPrompt = buildCrossFilePrompt(allFacts);
|
|
204
|
-
const crossResponse = await this.provider.analyze(crossPrompt, {
|
|
205
|
-
maxTokens: this.config.maxTokens || (isCloud ? 4096 : 512),
|
|
206
|
-
temperature: this.config.temperature || 0.1,
|
|
207
|
-
timeout: this.config.timeoutMs || (isCloud ? 120000 : 60000),
|
|
208
|
-
jsonMode: true,
|
|
209
|
-
});
|
|
210
|
-
const crossFindings = parseFindings(crossResponse);
|
|
211
|
-
allFindings.push(...crossFindings);
|
|
212
|
-
}
|
|
213
|
-
catch (error) {
|
|
214
|
-
Logger.warn(`Cross-file analysis failed: ${error.message}`);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
// Step 3: AST verifies LLM
|
|
218
|
-
onProgress?.(' Verifying findings...');
|
|
219
|
-
const verified = verifyFindings(allFindings, allFacts);
|
|
220
|
-
const durationMs = Date.now() - startTime;
|
|
221
|
-
onProgress?.(` ✓ ${verified.length} verified findings (${allFindings.length - verified.length} dropped) in ${(durationMs / 1000).toFixed(1)}s`);
|
|
222
|
-
if (failedChunks > 0) {
|
|
223
|
-
onProgress?.(` ⚠ ${failedChunks}/${chunks.length} batches failed — results may be incomplete.`);
|
|
224
|
-
}
|
|
225
|
-
// Convert to Failure format
|
|
226
|
-
for (const finding of verified) {
|
|
227
|
-
const failure = this.createFailure(finding.description, [finding.file], finding.suggestion, `[${finding.category}] ${finding.description.substring(0, 80)}`, finding.line, undefined, finding.severity);
|
|
228
|
-
// Tag with deep analysis metadata
|
|
229
|
-
failure.confidence = finding.confidence;
|
|
230
|
-
failure.source = 'llm';
|
|
231
|
-
failure.category = finding.category;
|
|
232
|
-
failure.verified = finding.verified;
|
|
233
|
-
failures.push(failure);
|
|
234
|
-
}
|
|
63
|
+
const verified = scoped
|
|
64
|
+
? await this.reviewCode(context.cwd, facts)
|
|
65
|
+
: await this.analyzeFacts(context.cwd, limitFiles(facts, this.config));
|
|
66
|
+
this.config.onProgress?.(` ✓ ${verified.length} verified findings in ${((Date.now() - startTime) / 1000).toFixed(1)}s`);
|
|
67
|
+
const intent = scoped && this.config.intentChecks
|
|
68
|
+
? await runIntentChecks(context.cwd, facts.map(f => f.path), this.provider, this.config.onProgress)
|
|
69
|
+
: [];
|
|
70
|
+
return [...verified.map(f => this.toFailure(f)), ...intent];
|
|
235
71
|
}
|
|
236
72
|
catch (error) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
73
|
+
this.outcome.status = 'error';
|
|
74
|
+
this.outcome.error = error?.message ?? String(error);
|
|
75
|
+
Logger.error(`Deep analysis failed: ${this.outcome.error}`);
|
|
76
|
+
this.config.onProgress?.(` ⚠ Deep analysis error: ${this.outcome.error}`);
|
|
77
|
+
return [];
|
|
240
78
|
}
|
|
241
79
|
finally {
|
|
242
80
|
this.provider?.dispose();
|
|
243
81
|
}
|
|
244
|
-
return failures;
|
|
245
82
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
83
|
+
async setupProvider() {
|
|
84
|
+
const { options, onProgress } = this.config;
|
|
85
|
+
onProgress?.('\n Setting up Rigour Brain...\n');
|
|
86
|
+
const provider = createProvider(options);
|
|
87
|
+
this.provider = provider;
|
|
88
|
+
if (isCloud(this.config.options)) {
|
|
89
|
+
await withTimeout(provider.setup(onProgress), CLOUD_SETUP_TIMEOUT_MS, 'Deep analysis setup timed out. Check your API key with `rigour settings show`.');
|
|
90
|
+
this.outcome.model = options.modelName || options.provider || 'cloud';
|
|
91
|
+
onProgress?.(`\n ☁️ Using ${options.provider} API. Code context may be sent to the provider.\n`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
await provider.setup(onProgress);
|
|
95
|
+
const active = provider instanceof SidecarProvider ? provider.getActiveModel() : null;
|
|
96
|
+
this.outcome.model = active?.name;
|
|
97
|
+
this.outcome.modelFallback = active?.fallback;
|
|
98
|
+
onProgress?.('\n 🔒 Local sidecar/model execution. Code remains on this machine.\n');
|
|
255
99
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
100
|
+
async reviewCode(cwd, facts) {
|
|
101
|
+
this.config.onProgress?.(` Reviewing ${facts.length} scoped file(s) with source...`);
|
|
102
|
+
const result = await runCodePass(this.provider, facts, {
|
|
103
|
+
cwd,
|
|
104
|
+
inference: inferenceOptions(this.config),
|
|
105
|
+
maxSourceChars: isCloud(this.config.options) ? CLOUD_SOURCE_CHARS : LOCAL_SOURCE_CHARS,
|
|
106
|
+
focusLines: this.config.options.focusLines,
|
|
107
|
+
onProgress: this.config.onProgress,
|
|
108
|
+
});
|
|
109
|
+
this.recordPass(result);
|
|
110
|
+
return verifyCodeFindings(result.findings, result.contexts);
|
|
264
111
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
return validateFindings(parsed.findings);
|
|
287
|
-
}
|
|
288
|
-
catch {
|
|
289
|
-
// Give up
|
|
290
|
-
}
|
|
112
|
+
async analyzeFacts(cwd, facts) {
|
|
113
|
+
this.config.onProgress?.(` Found ${facts.length} files to analyze.`);
|
|
114
|
+
const memory = await this.localMemoryFindings(cwd, facts);
|
|
115
|
+
const agentCount = this.config.options.agents || 1;
|
|
116
|
+
const result = await runFactsPass(this.provider, facts, {
|
|
117
|
+
inference: inferenceOptions(this.config),
|
|
118
|
+
checks: this.config.checks,
|
|
119
|
+
onProgress: this.config.onProgress,
|
|
120
|
+
agents: isCloud(this.config.options) && agentCount > 1
|
|
121
|
+
? { count: agentCount, create: () => this.createCloudAgent() }
|
|
122
|
+
: undefined,
|
|
123
|
+
});
|
|
124
|
+
this.recordPass(result);
|
|
125
|
+
return verifyFindings([...memory, ...result.findings], facts);
|
|
126
|
+
}
|
|
127
|
+
recordPass(result) {
|
|
128
|
+
this.outcome.chunksTotal = result.chunksTotal;
|
|
129
|
+
this.outcome.chunksFailed = result.chunksFailed;
|
|
130
|
+
if (result.chunksTotal > 0 && result.chunksFailed === result.chunksTotal) {
|
|
131
|
+
this.outcome.status = 'error';
|
|
132
|
+
this.outcome.error = `All ${result.chunksTotal} inference call(s) failed. First error: ${result.firstError ?? 'unknown'}`;
|
|
291
133
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (recovered.length > 0) {
|
|
296
|
-
Logger.info(`Recovered ${recovered.length} findings from truncated response`);
|
|
297
|
-
return recovered;
|
|
134
|
+
else if (result.chunksFailed > 0) {
|
|
135
|
+
this.outcome.status = 'partial';
|
|
136
|
+
this.config.onProgress?.(` ⚠ ${result.chunksFailed}/${result.chunksTotal} batches failed — results are incomplete.`);
|
|
298
137
|
}
|
|
299
|
-
Logger.warn(`Could not parse LLM response as findings JSON. First 200 chars: ${response.substring(0, 200)}`);
|
|
300
|
-
return [];
|
|
301
138
|
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Attempt to recover individual finding objects from a truncated JSON response.
|
|
305
|
-
* Extracts complete JSON objects from partial arrays.
|
|
306
|
-
*/
|
|
307
|
-
function recoverTruncatedFindings(response) {
|
|
308
|
-
const findings = [];
|
|
309
|
-
// Match individual complete objects within the response
|
|
310
|
-
const objectRegex = /\{\s*"category"\s*:\s*"[^"]+"\s*,[\s\S]*?"description"\s*:\s*"[^"]*"[^}]*\}/g;
|
|
311
|
-
let match;
|
|
312
|
-
while ((match = objectRegex.exec(response)) !== null) {
|
|
139
|
+
async localMemoryFindings(cwd, facts) {
|
|
313
140
|
try {
|
|
314
|
-
const
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
141
|
+
const found = await checkLocalPatterns(cwd, facts.map(f => f.path).filter(Boolean));
|
|
142
|
+
if (found.length > 0)
|
|
143
|
+
this.config.onProgress?.(` 🧠 Local memory: ${found.length} known pattern(s) matched instantly.`);
|
|
144
|
+
return found;
|
|
318
145
|
}
|
|
319
|
-
catch {
|
|
320
|
-
|
|
146
|
+
catch (error) {
|
|
147
|
+
Logger.debug(`Local memory check skipped (${error.message?.substring(0, 80)})`);
|
|
148
|
+
return [];
|
|
321
149
|
}
|
|
322
150
|
}
|
|
323
|
-
|
|
151
|
+
async createCloudAgent() {
|
|
152
|
+
const provider = createProvider(this.config.options);
|
|
153
|
+
await provider.setup();
|
|
154
|
+
return provider;
|
|
155
|
+
}
|
|
156
|
+
toFailure(finding) {
|
|
157
|
+
const failure = this.createFailure(finding.description, [finding.file], finding.suggestion, `[${finding.category}] ${finding.description.substring(0, 80)}`, finding.line, undefined, finding.severity);
|
|
158
|
+
return { ...failure, confidence: finding.confidence, source: 'llm', category: finding.category, verified: finding.verified };
|
|
159
|
+
}
|
|
324
160
|
}
|
|
325
|
-
/**
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
161
|
+
/** Cloud when an API key and a non-local provider are set (same rule as createProvider). */
|
|
162
|
+
function isCloud(options) {
|
|
163
|
+
return !!options.apiKey && !!options.provider && options.provider !== 'local';
|
|
164
|
+
}
|
|
165
|
+
/** Per-call options; unset config falls back to per-provider defaults. */
|
|
166
|
+
function inferenceOptions(config) {
|
|
167
|
+
const cloud = isCloud(config.options);
|
|
168
|
+
return {
|
|
169
|
+
maxTokens: config.maxTokens ?? (cloud ? 4096 : 1024),
|
|
170
|
+
temperature: config.temperature ?? 0.1,
|
|
171
|
+
timeout: config.timeoutMs ?? (cloud ? 120_000 : 60_000),
|
|
172
|
+
jsonMode: true,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function limitFiles(facts, config) {
|
|
176
|
+
const max = config.options.maxFiles;
|
|
177
|
+
if (!max || facts.length <= max)
|
|
178
|
+
return facts;
|
|
179
|
+
config.onProgress?.(` Limiting to ${max} files (configured in rigour.yml).`);
|
|
180
|
+
return facts.slice(0, max);
|
|
181
|
+
}
|
|
182
|
+
function emptyOutcome() {
|
|
183
|
+
return { status: 'ok', mode: 'facts', filesAnalyzed: 0, chunksTotal: 0, chunksFailed: 0 };
|
|
184
|
+
}
|
|
185
|
+
/** Entry points first, then larger files. */
|
|
186
|
+
function prioritize(facts) {
|
|
187
|
+
return [...facts].sort((a, b) => {
|
|
188
|
+
const entry = Number(isEntryPoint(b.path)) - Number(isEntryPoint(a.path));
|
|
189
|
+
return entry !== 0 ? entry : b.lineCount - a.lineCount;
|
|
349
190
|
});
|
|
350
191
|
}
|
|
192
|
+
const ENTRY_POINTS = new Set([
|
|
193
|
+
'index.ts', 'index.js', 'index.tsx', 'index.jsx', 'index.mjs',
|
|
194
|
+
'main.ts', 'main.js', 'main.py', 'main.go', 'main.rs', 'main.java', 'main.kt',
|
|
195
|
+
'app.ts', 'app.js', 'app.py', 'app.go', 'app.rb',
|
|
196
|
+
'server.ts', 'server.js', 'server.py', 'server.go',
|
|
197
|
+
'mod.rs', 'lib.rs',
|
|
198
|
+
]);
|
|
199
|
+
function isEntryPoint(filePath) {
|
|
200
|
+
return ENTRY_POINTS.has(path.basename(filePath).toLowerCase());
|
|
201
|
+
}
|
|
202
|
+
async function withTimeout(promise, ms, message) {
|
|
203
|
+
let timer;
|
|
204
|
+
try {
|
|
205
|
+
return await Promise.race([
|
|
206
|
+
promise,
|
|
207
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), ms); }),
|
|
208
|
+
]);
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
clearTimeout(timer);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|