@rigour-labs/core 6.2.4 → 6.2.5
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/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/gates/context.js +7 -0
- package/dist/gates/deep-analysis.d.ts +31 -8
- package/dist/gates/deep-analysis.js +165 -306
- package/dist/gates/deep-analysis.test.d.ts +1 -0
- package/dist/gates/deep-analysis.test.js +86 -0
- package/dist/gates/deep-runner.d.ts +15 -0
- package/dist/gates/deep-runner.js +65 -0
- package/dist/gates/dependency.js +4 -0
- package/dist/gates/logic-drift.js +7 -1
- package/dist/gates/runner.js +5 -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/style-drift.js +3 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- 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 +115 -343
- package/dist/templates/universal-config.js +0 -2
- package/dist/types/index.d.ts +58 -14
- package/dist/types/index.js +9 -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
package/dist/deep/index.js
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Neither AST nor LLM works alone. Together they're accurate.
|
|
9
9
|
*/
|
|
10
|
-
export { extractFacts, factsToPromptString } from './fact-extractor.js';
|
|
10
|
+
export { extractFacts, factsToPromptString, isTestFile } from './fact-extractor.js';
|
|
11
11
|
export { buildAnalysisPrompt, buildCrossFilePrompt, chunkFacts, DEEP_SYSTEM_PROMPT } from './prompts.js';
|
|
12
12
|
export { verifyFindings } from './verifier.js';
|
|
13
|
+
export { parseFindings } from './parse-findings.js';
|
|
14
|
+
export { buildCodeContext } from './code-context.js';
|
|
15
|
+
export { buildCodeReviewPrompt, REVIEW_CATEGORIES } from './code-review-prompt.js';
|
|
16
|
+
export { verifyCodeFindings } from './code-verifier.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse an LLM response into structured findings.
|
|
3
|
+
*
|
|
4
|
+
* Handles raw JSON, markdown-wrapped JSON, a findings object embedded in
|
|
5
|
+
* prose, and truncated responses (the model hit its token budget mid-array).
|
|
6
|
+
*/
|
|
7
|
+
import type { DeepFinding } from '../inference/types.js';
|
|
8
|
+
export declare function parseFindings(response: string): DeepFinding[];
|
|
9
|
+
/** Extract the complete finding objects from a response cut off mid-array. */
|
|
10
|
+
export declare function recoverTruncatedFindings(response: string): DeepFinding[];
|
|
11
|
+
/** Drop malformed entries and normalise confidence and severity. */
|
|
12
|
+
export declare function validateFindings(raw: unknown[]): DeepFinding[];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Logger } from '../utils/logger.js';
|
|
2
|
+
const VALID_SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
|
|
3
|
+
const FINDING_OBJECT = /\{\s*"category"\s*:\s*"[^"]+"\s*,[\s\S]*?"description"\s*:\s*"[^"]*"[^}]*\}/g;
|
|
4
|
+
export function parseFindings(response) {
|
|
5
|
+
if (!response || response.trim().length === 0) {
|
|
6
|
+
Logger.warn('Empty LLM response received');
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
const candidates = [
|
|
10
|
+
response,
|
|
11
|
+
response.match(/```(?:json)?\s*([\s\S]*?)```/)?.[1],
|
|
12
|
+
response.match(/\{[\s\S]*"findings"[\s\S]*\}/)?.[0],
|
|
13
|
+
];
|
|
14
|
+
for (const candidate of candidates) {
|
|
15
|
+
const parsed = candidate === undefined ? null : tryParseFindings(candidate);
|
|
16
|
+
if (parsed)
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
const recovered = recoverTruncatedFindings(response);
|
|
20
|
+
if (recovered.length > 0) {
|
|
21
|
+
Logger.info(`Recovered ${recovered.length} findings from truncated response`);
|
|
22
|
+
return recovered;
|
|
23
|
+
}
|
|
24
|
+
Logger.warn(`Could not parse LLM response as findings JSON. First 200 chars: ${response.substring(0, 200)}`);
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
function tryParseFindings(text) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(text);
|
|
30
|
+
if (Array.isArray(parsed?.findings))
|
|
31
|
+
return validateFindings(parsed.findings);
|
|
32
|
+
if (Array.isArray(parsed))
|
|
33
|
+
return validateFindings(parsed);
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Extract the complete finding objects from a response cut off mid-array. */
|
|
41
|
+
export function recoverTruncatedFindings(response) {
|
|
42
|
+
const findings = [];
|
|
43
|
+
for (const match of response.matchAll(FINDING_OBJECT)) {
|
|
44
|
+
try {
|
|
45
|
+
findings.push(JSON.parse(match[0]));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// The object itself was truncated; skip it.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return validateFindings(findings);
|
|
52
|
+
}
|
|
53
|
+
/** Drop malformed entries and normalise confidence and severity. */
|
|
54
|
+
export function validateFindings(raw) {
|
|
55
|
+
return raw.filter(hasRequiredFields).map(normalizeFinding);
|
|
56
|
+
}
|
|
57
|
+
function hasRequiredFields(f) {
|
|
58
|
+
return !!f && typeof f === 'object'
|
|
59
|
+
&& isNonEmptyString(f.category)
|
|
60
|
+
&& isNonEmptyString(f.file)
|
|
61
|
+
&& isNonEmptyString(f.description);
|
|
62
|
+
}
|
|
63
|
+
function isNonEmptyString(value) {
|
|
64
|
+
return typeof value === 'string' && value.length > 0;
|
|
65
|
+
}
|
|
66
|
+
function normalizeFinding(f) {
|
|
67
|
+
const confidenceOk = typeof f.confidence === 'number' && f.confidence >= 0 && f.confidence <= 1;
|
|
68
|
+
return {
|
|
69
|
+
...f,
|
|
70
|
+
confidence: confidenceOk ? f.confidence : 0.5,
|
|
71
|
+
severity: VALID_SEVERITIES.includes(f.severity) ? f.severity : 'medium',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { parseFindings } from './parse-findings.js';
|
|
3
|
+
const finding = { category: 'correctness', severity: 'high', file: 'a.ts', line: 3, description: 'bug', suggestion: 'fix', confidence: 0.8 };
|
|
4
|
+
describe('parseFindings', () => {
|
|
5
|
+
it('parses a findings object, a bare array and fenced JSON', () => {
|
|
6
|
+
expect(parseFindings(JSON.stringify({ findings: [finding] }))).toHaveLength(1);
|
|
7
|
+
expect(parseFindings(JSON.stringify([finding]))).toHaveLength(1);
|
|
8
|
+
expect(parseFindings('Here you go:\n```json\n' + JSON.stringify({ findings: [finding] }) + '\n```')).toHaveLength(1);
|
|
9
|
+
});
|
|
10
|
+
it('recovers complete findings from a response cut off by the token budget', () => {
|
|
11
|
+
const cut = `{"findings": [${JSON.stringify(finding)}, {"category": "security", "severity": "hi`;
|
|
12
|
+
expect(parseFindings(cut)).toEqual([finding]);
|
|
13
|
+
});
|
|
14
|
+
it('drops malformed entries and normalises confidence and severity', () => {
|
|
15
|
+
const parsed = parseFindings(JSON.stringify({ findings: [
|
|
16
|
+
{ ...finding, severity: 'urgent', confidence: 7 },
|
|
17
|
+
{ category: 'x', description: 'no file' },
|
|
18
|
+
] }));
|
|
19
|
+
expect(parsed).toEqual([{ ...finding, severity: 'medium', confidence: 0.5 }]);
|
|
20
|
+
});
|
|
21
|
+
it('returns nothing for empty or non-JSON output', () => {
|
|
22
|
+
expect(parseFindings('')).toEqual([]);
|
|
23
|
+
expect(parseFindings('I could not find any issues.')).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
});
|
package/dist/deep/prompts.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Prompt Engineering — Step 2 of the three-step pipeline.
|
|
3
3
|
* Constructs structured prompts that ask the LLM to interpret AST-extracted facts.
|
|
4
4
|
*/
|
|
5
|
-
import type
|
|
5
|
+
import { type FileFacts } from './fact-extractor.js';
|
|
6
6
|
/**
|
|
7
7
|
* System prompt that defines the LLM's role and output format.
|
|
8
8
|
*/
|
package/dist/deep/prompts.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Engineering — Step 2 of the three-step pipeline.
|
|
3
|
+
* Constructs structured prompts that ask the LLM to interpret AST-extracted facts.
|
|
4
|
+
*/
|
|
5
|
+
import { factsToPromptString } from './fact-extractor.js';
|
|
1
6
|
/**
|
|
2
7
|
* System prompt that defines the LLM's role and output format.
|
|
3
8
|
*/
|
|
@@ -362,13 +367,7 @@ export function chunkFacts(facts, maxCharsPerChunk = 6000) {
|
|
|
362
367
|
}
|
|
363
368
|
return chunks;
|
|
364
369
|
}
|
|
370
|
+
/** Serialized size of one file's facts, so chunks never overrun the prompt budget. */
|
|
365
371
|
function estimateFactSize(f) {
|
|
366
|
-
|
|
367
|
-
size += f.classes.reduce((a, c) => a + c.name.length + c.methods.length * 20 + 50, 0);
|
|
368
|
-
size += (f.structs || []).reduce((a, s) => a + s.name.length + s.methods.length * 20 + s.embeds.length * 15 + 60, 0);
|
|
369
|
-
size += (f.interfaces || []).reduce((a, i) => a + i.name.length + i.methods.length * 15 + 40, 0);
|
|
370
|
-
size += f.functions.reduce((a, fn) => a + fn.name.length + fn.params.length * 15 + 50, 0);
|
|
371
|
-
size += f.imports.length * 30;
|
|
372
|
-
size += f.errorHandling.length * 30;
|
|
373
|
-
return size;
|
|
372
|
+
return factsToPromptString([f], Number.POSITIVE_INFINITY).length + 2;
|
|
374
373
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { buildAnalysisPrompt, buildCrossFilePrompt, chunkFacts, DEEP_SYSTEM_PROMPT } from './prompts.js';
|
|
3
|
+
import { factsToPromptString } from './fact-extractor.js';
|
|
3
4
|
// ── Test helpers ──
|
|
4
5
|
function makeGoFacts(overrides = {}) {
|
|
5
6
|
return {
|
|
@@ -195,6 +196,20 @@ describe('Deep Analysis Prompts', () => {
|
|
|
195
196
|
});
|
|
196
197
|
// ── chunkFacts ──
|
|
197
198
|
describe('chunkFacts', () => {
|
|
199
|
+
it('never lets the serialized chunk overrun the prompt budget, so no file is dropped', () => {
|
|
200
|
+
const facts = Array.from({ length: 40 }, (_, i) => makeTsFacts({
|
|
201
|
+
path: `src/deeply/nested/module-${i}/implementation-file-with-a-long-name-${i}.ts`,
|
|
202
|
+
lineCount: 300,
|
|
203
|
+
imports: Array.from({ length: 8 }, (_, j) => `@scope/package-with-long-name-${j}/sub/path`),
|
|
204
|
+
}));
|
|
205
|
+
const chunks = chunkFacts(facts, 6000);
|
|
206
|
+
expect(chunks.flat()).toHaveLength(facts.length);
|
|
207
|
+
for (const chunk of chunks) {
|
|
208
|
+
const text = factsToPromptString(chunk);
|
|
209
|
+
for (const f of chunk)
|
|
210
|
+
expect(text).toContain(`FILE: ${f.path}`);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
198
213
|
it('should split facts into token-limited chunks', () => {
|
|
199
214
|
const manyFacts = Array.from({ length: 50 }, (_, i) => makeTsFacts({
|
|
200
215
|
path: `src/file${i}.ts`,
|
package/dist/gates/context.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Gate } from './base.js';
|
|
2
2
|
import { FileScanner } from '../utils/scanner.js';
|
|
3
|
+
import { isScoped, resolveScopedFiles, touchesScope } from '../utils/scope.js';
|
|
3
4
|
import fs from 'fs-extra';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
export class ContextGate extends Gate {
|
|
@@ -57,6 +58,12 @@ export class ContextGate extends Gate {
|
|
|
57
58
|
if (this.extendedConfig.import_relationships) {
|
|
58
59
|
this.analyzeImportPatterns(importPatterns, failures);
|
|
59
60
|
}
|
|
61
|
+
// Cross-file patterns need the whole repo, but a scoped run reports
|
|
62
|
+
// only findings that cite a file in scope.
|
|
63
|
+
if (isScoped(context.patterns)) {
|
|
64
|
+
const scoped = await resolveScopedFiles(context.cwd, context.patterns, context.ignore);
|
|
65
|
+
return failures.filter(f => touchesScope(f.files, scoped));
|
|
66
|
+
}
|
|
60
67
|
return failures;
|
|
61
68
|
}
|
|
62
69
|
checkEnvDrift(content, file, anchors, failures) {
|
|
@@ -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';
|
|
@@ -19,12 +22,32 @@ export interface DeepGateConfig {
|
|
|
19
22
|
timeoutMs?: number;
|
|
20
23
|
onProgress?: (message: string) => void;
|
|
21
24
|
}
|
|
25
|
+
export type DeepRunStatus = 'ok' | 'partial' | 'error';
|
|
26
|
+
export interface DeepRunOutcome {
|
|
27
|
+
status: DeepRunStatus;
|
|
28
|
+
mode: 'facts' | 'code';
|
|
29
|
+
filesAnalyzed: number;
|
|
30
|
+
chunksTotal: number;
|
|
31
|
+
chunksFailed: number;
|
|
32
|
+
/** Model actually used (the stock fallback is named as such). */
|
|
33
|
+
model?: string;
|
|
34
|
+
modelFallback?: boolean;
|
|
35
|
+
error?: string;
|
|
36
|
+
}
|
|
22
37
|
export declare class DeepAnalysisGate extends Gate {
|
|
23
38
|
private config;
|
|
24
39
|
private provider;
|
|
40
|
+
private outcome;
|
|
25
41
|
constructor(config: DeepGateConfig);
|
|
26
42
|
protected get provenance(): Provenance;
|
|
27
|
-
/**
|
|
28
|
-
|
|
43
|
+
/** Outcome of the last run(). */
|
|
44
|
+
getOutcome(): DeepRunOutcome;
|
|
29
45
|
run(context: GateContext): Promise<Failure[]>;
|
|
46
|
+
private setupProvider;
|
|
47
|
+
private reviewCode;
|
|
48
|
+
private analyzeFacts;
|
|
49
|
+
private recordPass;
|
|
50
|
+
private localMemoryFindings;
|
|
51
|
+
private createCloudAgent;
|
|
52
|
+
private toFailure;
|
|
30
53
|
}
|