@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.
Files changed (80) hide show
  1. package/dist/deep/code-context.d.ts +28 -0
  2. package/dist/deep/code-context.js +93 -0
  3. package/dist/deep/code-context.test.d.ts +1 -0
  4. package/dist/deep/code-context.test.js +48 -0
  5. package/dist/deep/code-pass.d.ts +15 -0
  6. package/dist/deep/code-pass.js +44 -0
  7. package/dist/deep/code-review-prompt.d.ts +17 -0
  8. package/dist/deep/code-review-prompt.js +30 -0
  9. package/dist/deep/code-review-prompt.test.d.ts +1 -0
  10. package/dist/deep/code-review-prompt.test.js +20 -0
  11. package/dist/deep/code-verifier.d.ts +19 -0
  12. package/dist/deep/code-verifier.js +48 -0
  13. package/dist/deep/code-verifier.test.d.ts +1 -0
  14. package/dist/deep/code-verifier.test.js +46 -0
  15. package/dist/deep/fact-extractor.d.ts +7 -1
  16. package/dist/deep/fact-extractor.js +16 -8
  17. package/dist/deep/fact-extractor.test.js +34 -1
  18. package/dist/deep/facts-pass.d.ts +24 -0
  19. package/dist/deep/facts-pass.js +69 -0
  20. package/dist/deep/index.d.ts +6 -1
  21. package/dist/deep/index.js +5 -1
  22. package/dist/deep/parse-findings.d.ts +12 -0
  23. package/dist/deep/parse-findings.js +73 -0
  24. package/dist/deep/parse-findings.test.d.ts +1 -0
  25. package/dist/deep/parse-findings.test.js +25 -0
  26. package/dist/deep/prompts.d.ts +1 -1
  27. package/dist/deep/prompts.js +7 -8
  28. package/dist/deep/prompts.test.js +15 -0
  29. package/dist/gates/context.js +7 -0
  30. package/dist/gates/deep-analysis.d.ts +31 -8
  31. package/dist/gates/deep-analysis.js +165 -306
  32. package/dist/gates/deep-analysis.test.d.ts +1 -0
  33. package/dist/gates/deep-analysis.test.js +86 -0
  34. package/dist/gates/deep-runner.d.ts +15 -0
  35. package/dist/gates/deep-runner.js +65 -0
  36. package/dist/gates/dependency.js +4 -0
  37. package/dist/gates/logic-drift.js +7 -1
  38. package/dist/gates/runner.js +5 -40
  39. package/dist/gates/runner.test.js +37 -25
  40. package/dist/gates/scoped-gates.test.d.ts +1 -0
  41. package/dist/gates/scoped-gates.test.js +52 -0
  42. package/dist/gates/style-drift.js +3 -0
  43. package/dist/index.d.ts +6 -1
  44. package/dist/index.js +6 -1
  45. package/dist/inference/http-download.d.ts +22 -0
  46. package/dist/inference/http-download.js +132 -0
  47. package/dist/inference/http-download.test.d.ts +1 -0
  48. package/dist/inference/http-download.test.js +109 -0
  49. package/dist/inference/index.d.ts +5 -1
  50. package/dist/inference/index.js +2 -1
  51. package/dist/inference/llama-engine.d.ts +27 -0
  52. package/dist/inference/llama-engine.js +152 -0
  53. package/dist/inference/llama-engine.test.d.ts +1 -0
  54. package/dist/inference/llama-engine.test.js +51 -0
  55. package/dist/inference/llama-process.d.ts +72 -0
  56. package/dist/inference/llama-process.js +126 -0
  57. package/dist/inference/llama-process.test.d.ts +1 -0
  58. package/dist/inference/llama-process.test.js +61 -0
  59. package/dist/inference/model-download.test.d.ts +1 -0
  60. package/dist/inference/model-download.test.js +73 -0
  61. package/dist/inference/model-manager.d.ts +14 -1
  62. package/dist/inference/model-manager.js +65 -80
  63. package/dist/inference/sidecar-provider.d.ts +19 -10
  64. package/dist/inference/sidecar-provider.js +115 -343
  65. package/dist/templates/universal-config.js +0 -2
  66. package/dist/types/index.d.ts +58 -14
  67. package/dist/types/index.js +9 -2
  68. package/dist/utils/deep-status.d.ts +7 -0
  69. package/dist/utils/deep-status.js +11 -0
  70. package/dist/utils/diff.d.ts +3 -0
  71. package/dist/utils/diff.js +52 -0
  72. package/dist/utils/diff.test.d.ts +1 -0
  73. package/dist/utils/diff.test.js +38 -0
  74. package/dist/utils/scanner.js +13 -2
  75. package/dist/utils/scanner.test.js +22 -8
  76. package/dist/utils/scope.d.ts +15 -0
  77. package/dist/utils/scope.js +51 -0
  78. package/dist/utils/scope.test.d.ts +1 -0
  79. package/dist/utils/scope.test.js +38 -0
  80. package/package.json +6 -6
@@ -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
+ });
@@ -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 { FileFacts } from './fact-extractor.js';
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
  */
@@ -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
- let size = f.path.length + 50;
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`,
@@ -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 quality analysis.
2
+ * Deep Analysis Gate — LLM-powered code analysis.
3
3
  *
4
- * Three-step pipeline:
5
- * 1. AST extracts facts → "UserService has 8 public methods touching 4 domains"
6
- * 2. LLM interprets facts → "UserService violates Single Responsibility"
7
- * 3. AST verifies LLM → Does UserService actually have those methods? ✓
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
- * AST grounds LLM. LLM interprets AST. Neither works alone.
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
- /** Check if a file is a likely entry point (higher analysis priority) */
28
- private isEntryPoint;
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
  }