@rigour-labs/core 6.3.1 → 6.4.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 CHANGED
@@ -26,7 +26,7 @@ The core library powering [Rigour](https://rigour.run) — 27+ quality gates, fi
26
26
  - **Dependency bloat** — unused deps, heavy alternatives (moment→dayjs), duplicate purpose packages.
27
27
  - **Context-window artifacts**, inconsistent error handling, promise safety, deprecated APIs.
28
28
 
29
- **Semantic Bugs (type-aware, opt-in):** a TypeScript program with the checker and cross-file value tracing proves a defect before reporting it: credentials forwarded on redirect, paged reads aggregated in memory, cached responses that can carry a failure fallback. Rules learned from fixes (`rigour learn`) run alongside them from `.rigour/rules/`.
29
+ **Semantic Bugs (type-aware, on by default):** a TypeScript program with the checker and cross-file value tracing proves a defect before reporting it: credentials forwarded on redirect, paged reads aggregated in memory, cached responses that can carry a failure fallback. Rules learned from fixes (`rigour learn`) run alongside them from `.rigour/rules/`.
30
30
 
31
31
  **Agent Governance:** Multi-agent scope isolation, EWMA-based checkpoint supervision, context drift, retry loop breaker, memory & skills governance with DLP scanning.
32
32
 
@@ -29,6 +29,13 @@ export interface JsImportContext {
29
29
  * Check JavaScript/TypeScript imports in a source file and add hallucinated findings.
30
30
  */
31
31
  export declare function checkJSImports(content: string, file: string, ctx: JsImportContext): Promise<void>;
32
+ /**
33
+ * Bundler conventions: a query on a relative import (`./logo.svg?raw`, `./worker.ts?worker`)
34
+ * names a transform of that file, so the file is what must exist; a `virtual:` module or a
35
+ * bare specifier with a query (`page.tsx?tsr-split=component`) is produced by a plugin at
36
+ * build time and cannot be checked. Returns the path to check, or null to skip.
37
+ */
38
+ export declare function bundlerSpecifier(importPath: string): string | null;
32
39
  /**
33
40
  * Collect all import statements from JavaScript/TypeScript file content.
34
41
  * Handles import, export, require(), and dynamic import() statements.
@@ -21,7 +21,9 @@ import { loadTsPathConfig } from './tsconfig-paths.js';
21
21
  export async function checkJSImports(content, file, ctx) {
22
22
  const depsForFile = await resolveJSDepsForFile(file, ctx.cwd, ctx.rootDeps, ctx.depCacheByDir);
23
23
  const kitRoot = await ctx.kitRoots.rootFor(file);
24
- for (const { importPath, line } of collectJSImportSpecs(content, file)) {
24
+ for (const spec of collectJSImportSpecs(content, file)) {
25
+ const { line } = spec;
26
+ const importPath = bundlerSpecifier(spec.importPath);
25
27
  if (!importPath || ctx.shouldIgnore(importPath))
26
28
  continue;
27
29
  if (kitRoot && isSvelteKitProvided(importPath))
@@ -38,6 +40,20 @@ export async function checkJSImports(content, file, ctx) {
38
40
  }
39
41
  }
40
42
  }
43
+ /**
44
+ * Bundler conventions: a query on a relative import (`./logo.svg?raw`, `./worker.ts?worker`)
45
+ * names a transform of that file, so the file is what must exist; a `virtual:` module or a
46
+ * bare specifier with a query (`page.tsx?tsr-split=component`) is produced by a plugin at
47
+ * build time and cannot be checked. Returns the path to check, or null to skip.
48
+ */
49
+ export function bundlerSpecifier(importPath) {
50
+ if (!importPath || importPath.startsWith('virtual:') || importPath.startsWith('\0'))
51
+ return null;
52
+ const query = importPath.search(/[?#]/);
53
+ if (query < 0)
54
+ return importPath;
55
+ return importPath.startsWith('.') ? importPath.slice(0, query) : null;
56
+ }
41
57
  async function checkRelativeImport(file, importPath, ctx) {
42
58
  if (ctx.resolveRelativeImport(file, importPath, ctx.projectFiles))
43
59
  return null;
@@ -106,3 +106,27 @@ describe('HallucinatedImportsGate — tsconfig inheritance and rootDirs', () =>
106
106
  expect(await missingImports(cwd)).toContain("'./nowhere'");
107
107
  });
108
108
  });
109
+ describe('HallucinatedImportsGate — bundler query and virtual imports', () => {
110
+ let cwd;
111
+ beforeEach(() => {
112
+ cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'bundler-imports-'));
113
+ write(cwd, 'package.json', JSON.stringify({ dependencies: { '@tanstack/react-router': '1.0.0' } }));
114
+ write(cwd, 'src/logo.svg', '<svg/>');
115
+ write(cwd, 'src/worker.ts', 'export {};\n');
116
+ });
117
+ afterEach(() => { fs.rmSync(cwd, { recursive: true, force: true }); });
118
+ it('checks the file behind a query and skips plugin-provided modules', async () => {
119
+ write(cwd, 'src/route.tsx', [
120
+ "import raw from './logo.svg?raw';",
121
+ "import Worker from './worker.ts?worker&inline';",
122
+ "const split = () => import('route.tsx?tsr-split=component');",
123
+ "import { registerSW } from 'virtual:pwa-register';",
124
+ 'export default { raw, Worker, split, registerSW };',
125
+ ].join('\n'));
126
+ expect(await missingImports(cwd)).toBe('');
127
+ });
128
+ it('still reports a missing module behind a query', async () => {
129
+ write(cwd, 'src/route.tsx', "import Worker from './missing-worker.ts?worker';\nexport default Worker;\n");
130
+ expect(await missingImports(cwd)).toContain('./missing-worker.ts');
131
+ });
132
+ });
@@ -4,6 +4,7 @@ import path from 'path';
4
4
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
5
  import { SemanticBugsGate } from './semantic-bugs.js';
6
6
  import { GateRunner } from './runner.js';
7
+ import { ConfigSchema } from '../types/index.js';
7
8
  const LEAKY = [
8
9
  'export async function notify(endpoint: string, signature: string) {',
9
10
  " return fetch(endpoint, { method: 'POST', headers: { 'x-hook-signature': signature } });",
@@ -59,12 +60,11 @@ describe('SemanticBugsGate', () => {
59
60
  expect(failure.title).toContain('[learned/require-option-redirect-abcd1234]');
60
61
  expect(failure.hint).toContain('fix abcdef01 in src/http.ts (post)');
61
62
  });
62
- it('is off unless enabled in rigour.yml', async () => {
63
+ it('is on by default and can be turned off in rigour.yml', async () => {
63
64
  write('src/notify.ts', LEAKY);
64
- const base = { version: 1, commands: {}, gates: { max_file_lines: 500 } };
65
- const off = await new GateRunner(base).run(cwd);
66
- expect(off.failures.some(f => f.id === 'semantic-bugs')).toBe(false);
67
- const on = await new GateRunner({ ...base, gates: { ...base.gates, semantic_bugs: { enabled: true } } }).run(cwd);
65
+ const on = await new GateRunner(ConfigSchema.parse({ version: 1 })).run(cwd);
68
66
  expect(on.failures.some(f => f.id === 'semantic-bugs')).toBe(true);
67
+ const off = await new GateRunner(ConfigSchema.parse({ version: 1, gates: { semantic_bugs: { enabled: false } } })).run(cwd);
68
+ expect(off.failures.some(f => f.id === 'semantic-bugs')).toBe(false);
69
69
  });
70
70
  });
@@ -12,3 +12,5 @@ export { recordDLPFeedback, allowLastDLPBlock, writeDLPBlockManifest, getLearned
12
12
  export type { DLPFeedbackEntry, DLPFeedbackStore, DLPBlockManifest } from './dlp-feedback.js';
13
13
  export { generateDLPHookFiles } from './dlp-templates.js';
14
14
  export type { GeneratedDLPHookFile } from './dlp-templates.js';
15
+ export { stopReview, stopMessage, blocksStop, STOP_MAX_ATTEMPTS } from './stop-review.js';
16
+ export type { StopDecision } from './stop-review.js';
@@ -9,3 +9,5 @@ export { DEFAULT_HOOK_CONFIG, FAST_GATE_IDS } from './types.js';
9
9
  export { scanInputForCredentials, formatDLPAlert, createDLPAuditEntry } from './input-validator.js';
10
10
  export { recordDLPFeedback, allowLastDLPBlock, writeDLPBlockManifest, getLearnedAllowCount, loadDLPFeedbackStore, loadDLPFeedbackStoreSync, fingerprintDetection, } from './dlp-feedback.js';
11
11
  export { generateDLPHookFiles } from './dlp-templates.js';
12
+ // Stop review ("before you say done")
13
+ export { stopReview, stopMessage, blocksStop, STOP_MAX_ATTEMPTS } from './stop-review.js';
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The "before you say done" review: when an agent is about to stop, review the
3
+ * uncommitted change and, if it introduced serious findings, tell the agent
4
+ * what to fix instead of letting it finish.
5
+ *
6
+ * Only findings that deserve it block: critical ones, and high ones that are
7
+ * proven (the semantic engine traced them) or security findings. A high
8
+ * heuristic (a regex that sees `fetch` without `.catch`) is not enough to hold
9
+ * an agent back; callers also cap the number of attempts.
10
+ */
11
+ import type { Config, Failure } from '../types/index.js';
12
+ export declare const STOP_MAX_ATTEMPTS = 3;
13
+ export interface StopDecision {
14
+ block: boolean;
15
+ /** What the agent is told when blocked; empty otherwise. */
16
+ message: string;
17
+ blocking: number;
18
+ /** Every finding on changed lines, and the files the review covered (for fix capture). */
19
+ findings: Failure[];
20
+ reviewedFiles: string[];
21
+ }
22
+ export declare function stopReview(cwd: string, config: Config, attempt: number): Promise<StopDecision>;
23
+ /** Critical, or high and either proven by the semantic engine or a security finding. */
24
+ export declare function blocksStop(finding: Failure): boolean;
25
+ export declare function stopMessage(findings: Failure[], attempt: number): string;
@@ -0,0 +1,31 @@
1
+ import { reviewChange } from '../review/review.js';
2
+ export const STOP_MAX_ATTEMPTS = 3;
3
+ const MAX_LISTED = 8;
4
+ export async function stopReview(cwd, config, attempt) {
5
+ const result = await reviewChange({ cwd, config, source: { mode: 'working' } });
6
+ const blocking = result.findings.filter(blocksStop);
7
+ const reviewed = { findings: result.findings, reviewedFiles: Object.keys(result.changedLines) };
8
+ if (blocking.length === 0)
9
+ return { block: false, message: '', blocking: 0, ...reviewed };
10
+ return { block: true, message: stopMessage(blocking, attempt), blocking: blocking.length, ...reviewed };
11
+ }
12
+ /** Critical, or high and either proven by the semantic engine or a security finding. */
13
+ export function blocksStop(finding) {
14
+ const severity = (finding.severity || 'medium');
15
+ if (severity === 'critical')
16
+ return true;
17
+ return severity === 'high' && (finding.verified === true || finding.provenance === 'security');
18
+ }
19
+ export function stopMessage(findings, attempt) {
20
+ const listed = findings.slice(0, MAX_LISTED).map(f => {
21
+ const where = `${f.files?.[0] ?? '?'}:${f.line ?? '?'}`;
22
+ return `- [${(f.severity || 'medium').toUpperCase()}] ${where} ${f.title}${f.hint ? `\n Fix: ${f.hint}` : ''}`;
23
+ });
24
+ const more = findings.length > MAX_LISTED ? [`- …and ${findings.length - MAX_LISTED} more (run \`rigour review\`).`] : [];
25
+ return [
26
+ `Rigour review found ${findings.length} serious issue(s) on the lines you changed (attempt ${attempt} of ${STOP_MAX_ATTEMPTS}).`,
27
+ 'Fix them before finishing, or explain why a finding is wrong:',
28
+ ...listed,
29
+ ...more,
30
+ ].join('\n');
31
+ }
package/dist/index.d.ts CHANGED
@@ -11,6 +11,11 @@ export { FrontendSecretExposureGate } from './gates/frontend-secret-exposure.js'
11
11
  export * from './utils/logger.js';
12
12
  export { normalizeScopePatterns, isScoped } from './utils/scope.js';
13
13
  export { deepAnalysisError } from './utils/deep-status.js';
14
+ export { reviewChange, toReviewFinding, type ReviewInput, type ReviewResult, type ReviewFinding } from './review/review.js';
15
+ export { splitByChangedLines, type ChangedLineSplit } from './review/changed-lines.js';
16
+ export { diffFromGit, GitDiffError, type DiffSource } from './review/git-diff.js';
17
+ export { recordReviewOutcome, listResolvedFixes, removeResolvedFix, openFindingCount, type ResolvedFix, type FixCapture } from './review/agent-fixes.js';
18
+ export { computeEffectiveness, readAgentEvents, appendAgentEvent, findingKeys, type AgentEvent, type ReviewEffectiveness } from './review/effectiveness.js';
14
19
  export { parseDiff, changedLinesByFile } from './utils/diff.js';
15
20
  export { FileScanner } from './utils/scanner.js';
16
21
  export * from './services/score-history.js';
@@ -51,7 +56,7 @@ export * from './services/system-health.js';
51
56
  export * from './services/agent-history.js';
52
57
  export * from './services/engineering-knowledge-graph.js';
53
58
  export * from './firewall/index.js';
54
- export { learnFromFix, type LearnReport, type Rejection } from './semantic/learn/learn.js';
59
+ export { learnFromFix, learnFromFileChange, type LearnReport, type Rejection } from './semantic/learn/learn.js';
55
60
  export { extractFixTrees, type FixTrees } from './semantic/learn/git-trees.js';
56
61
  export { saveLearnedRule, loadLearnedRules, LEARNED_RULES_DIR } from './semantic/learn/store.js';
57
62
  export type { LearnedRule, LearnedPattern } from './semantic/learn/types.js';
package/dist/index.js CHANGED
@@ -11,6 +11,11 @@ export { FrontendSecretExposureGate } from './gates/frontend-secret-exposure.js'
11
11
  export * from './utils/logger.js';
12
12
  export { normalizeScopePatterns, isScoped } from './utils/scope.js';
13
13
  export { deepAnalysisError } from './utils/deep-status.js';
14
+ export { reviewChange, toReviewFinding } from './review/review.js';
15
+ export { splitByChangedLines } from './review/changed-lines.js';
16
+ export { diffFromGit, GitDiffError } from './review/git-diff.js';
17
+ export { recordReviewOutcome, listResolvedFixes, removeResolvedFix, openFindingCount } from './review/agent-fixes.js';
18
+ export { computeEffectiveness, readAgentEvents, appendAgentEvent, findingKeys } from './review/effectiveness.js';
14
19
  export { parseDiff, changedLinesByFile } from './utils/diff.js';
15
20
  export { FileScanner } from './utils/scanner.js';
16
21
  export * from './services/score-history.js';
@@ -50,7 +55,7 @@ export * from './services/system-health.js';
50
55
  export * from './services/agent-history.js';
51
56
  export * from './services/engineering-knowledge-graph.js';
52
57
  export * from './firewall/index.js';
53
- export { learnFromFix } from './semantic/learn/learn.js';
58
+ export { learnFromFix, learnFromFileChange } from './semantic/learn/learn.js';
54
59
  export { extractFixTrees } from './semantic/learn/git-trees.js';
55
60
  export { saveLearnedRule, loadLearnedRules, LEARNED_RULES_DIR } from './semantic/learn/store.js';
56
61
  export { recordContextEvent, recordModelUsage, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics, } from './storage/index.js';
@@ -0,0 +1,18 @@
1
+ import type { Failure } from '../types/index.js';
2
+ export interface ResolvedFix {
3
+ id: string;
4
+ file: string;
5
+ rule: string;
6
+ before: string;
7
+ after: string;
8
+ resolvedAt: string;
9
+ }
10
+ export interface FixCapture {
11
+ opened: number;
12
+ resolved: number;
13
+ }
14
+ /** Record a review's findings: open new ones, resolve open ones the review no longer reports. */
15
+ export declare function recordReviewOutcome(cwd: string, findings: Failure[], reviewedFiles: string[]): FixCapture;
16
+ export declare function listResolvedFixes(cwd: string): ResolvedFix[];
17
+ export declare function removeResolvedFix(cwd: string, id: string): void;
18
+ export declare function openFindingCount(cwd: string): number;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Fixes agents make to Rigour findings, captured so they can become rules.
3
+ *
4
+ * Capture is cheap and happens in the review loop (MCP review, stop hook):
5
+ * a finding's file content is kept when the finding appears, and when a later
6
+ * review of that file no longer reports it, the before and after are stored as
7
+ * a resolved fix. Learning is expensive and happens later, outside the loop
8
+ * (`rigour learn --agent-fixes`), with the same validation as any learned rule.
9
+ *
10
+ * A finding only counts as resolved when its file was part of the review that
11
+ * no longer reports it: a file committed out of the working tree was not fixed.
12
+ */
13
+ import crypto from 'crypto';
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+ const DIR = path.join('.rigour', 'agent-fixes');
17
+ const MAX_FILE_BYTES = 200_000;
18
+ const OPEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
19
+ /** Record a review's findings: open new ones, resolve open ones the review no longer reports. */
20
+ export function recordReviewOutcome(cwd, findings, reviewedFiles) {
21
+ const open = readOpen(cwd);
22
+ const current = new Map(findings.flatMap(f => (f.files?.[0] ? [[`${f.id}:${f.files[0]}`, f]] : [])));
23
+ const capture = { resolved: resolveGone(cwd, open, current, new Set(reviewedFiles)), opened: openNew(cwd, open, current) };
24
+ writeOpen(cwd, open);
25
+ return capture;
26
+ }
27
+ /** Open findings this review no longer reports: resolved when their file was reviewed and changed. */
28
+ function resolveGone(cwd, open, current, reviewed) {
29
+ let resolved = 0;
30
+ for (const [key, entry] of Object.entries(open)) {
31
+ if (current.has(key))
32
+ continue;
33
+ if (!reviewed.has(entry.file)) {
34
+ if (Date.now() - Date.parse(entry.openedAt) > OPEN_TTL_MS)
35
+ delete open[key];
36
+ continue;
37
+ }
38
+ const after = readSmall(cwd, entry.file);
39
+ if (after !== null && after !== entry.before) {
40
+ writeResolved(cwd, entry, after);
41
+ resolved++;
42
+ }
43
+ delete open[key];
44
+ }
45
+ return resolved;
46
+ }
47
+ function openNew(cwd, open, current) {
48
+ let opened = 0;
49
+ for (const [key, finding] of current) {
50
+ if (open[key])
51
+ continue;
52
+ const file = finding.files[0];
53
+ const before = readSmall(cwd, file);
54
+ if (before === null)
55
+ continue;
56
+ open[key] = { file, rule: finding.id, before, openedAt: new Date().toISOString() };
57
+ opened++;
58
+ }
59
+ return opened;
60
+ }
61
+ export function listResolvedFixes(cwd) {
62
+ const dir = path.join(cwd, DIR, 'resolved');
63
+ if (!fs.existsSync(dir))
64
+ return [];
65
+ return fs.readdirSync(dir).filter(n => n.endsWith('.json')).sort().flatMap(name => {
66
+ try {
67
+ return [JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'))];
68
+ }
69
+ catch {
70
+ return [];
71
+ }
72
+ });
73
+ }
74
+ export function removeResolvedFix(cwd, id) {
75
+ fs.rmSync(path.join(cwd, DIR, 'resolved', `${id}.json`), { force: true });
76
+ }
77
+ export function openFindingCount(cwd) {
78
+ return Object.keys(readOpen(cwd)).length;
79
+ }
80
+ function readOpen(cwd) {
81
+ try {
82
+ return JSON.parse(fs.readFileSync(path.join(cwd, DIR, 'open.json'), 'utf8'));
83
+ }
84
+ catch {
85
+ return {};
86
+ }
87
+ }
88
+ function writeOpen(cwd, open) {
89
+ try {
90
+ fs.mkdirSync(path.join(cwd, DIR), { recursive: true });
91
+ fs.writeFileSync(path.join(cwd, DIR, 'open.json'), JSON.stringify(open));
92
+ }
93
+ catch {
94
+ // Capture is best-effort; it must never break a review.
95
+ }
96
+ }
97
+ function writeResolved(cwd, entry, after) {
98
+ const id = crypto.createHash('sha256').update(`${entry.rule}\0${entry.file}\0${entry.before}\0${after}`).digest('hex').slice(0, 16);
99
+ const fix = { id, file: entry.file, rule: entry.rule, before: entry.before, after, resolvedAt: new Date().toISOString() };
100
+ try {
101
+ fs.mkdirSync(path.join(cwd, DIR, 'resolved'), { recursive: true });
102
+ fs.writeFileSync(path.join(cwd, DIR, 'resolved', `${id}.json`), JSON.stringify(fix));
103
+ }
104
+ catch {
105
+ // Best-effort, as above.
106
+ }
107
+ }
108
+ function readSmall(cwd, file) {
109
+ try {
110
+ const full = path.join(cwd, file);
111
+ if (fs.statSync(full).size > MAX_FILE_BYTES)
112
+ return null;
113
+ return fs.readFileSync(full, 'utf8');
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
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 { listResolvedFixes, openFindingCount, recordReviewOutcome } from './agent-fixes.js';
6
+ const finding = (file) => ({ id: 'semantic-bugs', title: 't', details: 'd', files: [file], line: 2 });
7
+ describe('recordReviewOutcome', () => {
8
+ let cwd;
9
+ beforeEach(() => {
10
+ cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-fixes-'));
11
+ fs.writeFileSync(path.join(cwd, 'a.ts'), 'before\n');
12
+ });
13
+ afterEach(() => { fs.rmSync(cwd, { recursive: true, force: true }); });
14
+ it('stores the before and after once a reviewed file no longer has the finding', () => {
15
+ expect(recordReviewOutcome(cwd, [finding('a.ts')], ['a.ts'])).toEqual({ opened: 1, resolved: 0 });
16
+ fs.writeFileSync(path.join(cwd, 'a.ts'), 'after\n');
17
+ expect(recordReviewOutcome(cwd, [], ['a.ts'])).toEqual({ opened: 0, resolved: 1 });
18
+ const [fix] = listResolvedFixes(cwd);
19
+ expect(fix).toMatchObject({ file: 'a.ts', rule: 'semantic-bugs', before: 'before\n', after: 'after\n' });
20
+ expect(openFindingCount(cwd)).toBe(0);
21
+ });
22
+ it('keeps a finding open when its file was not part of the review (committed, not fixed)', () => {
23
+ recordReviewOutcome(cwd, [finding('a.ts')], ['a.ts']);
24
+ expect(recordReviewOutcome(cwd, [], ['other.ts'])).toEqual({ opened: 0, resolved: 0 });
25
+ expect(openFindingCount(cwd)).toBe(1);
26
+ });
27
+ it('does not count a finding that vanished without an edit as a fix', () => {
28
+ recordReviewOutcome(cwd, [finding('a.ts')], ['a.ts']);
29
+ expect(recordReviewOutcome(cwd, [], ['a.ts'])).toEqual({ opened: 0, resolved: 0 });
30
+ expect(listResolvedFixes(cwd)).toEqual([]);
31
+ });
32
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Which findings belong to a change.
3
+ *
4
+ * - findings: on a changed line; these decide PASS/FAIL.
5
+ * - fileFindings: about a changed file as a whole (no line); shown as context.
6
+ * - unlocated: no file at all; counted, never shown as a changed-line finding.
7
+ * - outside: on an unchanged line; counted.
8
+ *
9
+ * The CLI, the MCP tool and the GitHub integration all use this, so the same
10
+ * change gets the same verdict everywhere.
11
+ */
12
+ import type { Failure } from '../types/index.js';
13
+ export interface ChangedLineSplit {
14
+ findings: Failure[];
15
+ fileFindings: Failure[];
16
+ unlocated: number;
17
+ outside: number;
18
+ }
19
+ export declare function splitByChangedLines(failures: Failure[], changedLines: Record<string, Set<number>>): ChangedLineSplit;
@@ -0,0 +1,22 @@
1
+ export function splitByChangedLines(failures, changedLines) {
2
+ const split = { findings: [], fileFindings: [], unlocated: 0, outside: 0 };
3
+ for (const failure of failures) {
4
+ const files = failure.files ?? [];
5
+ if (files.length === 0) {
6
+ split.unlocated++;
7
+ }
8
+ else if (failure.line === undefined) {
9
+ if (files.some(file => changedLines[file]))
10
+ split.fileFindings.push(failure);
11
+ else
12
+ split.outside++;
13
+ }
14
+ else if (files.some(file => changedLines[file]?.has(failure.line))) {
15
+ split.findings.push(failure);
16
+ }
17
+ else {
18
+ split.outside++;
19
+ }
20
+ }
21
+ return split;
22
+ }
@@ -0,0 +1,34 @@
1
+ export interface AgentEvent {
2
+ type: string;
3
+ timestamp?: string;
4
+ tool?: string;
5
+ status?: string;
6
+ content?: Array<{
7
+ type: string;
8
+ text?: string;
9
+ }>;
10
+ session?: string;
11
+ blocked?: boolean;
12
+ /** Findings that blocked a stop. */
13
+ blocking?: number;
14
+ }
15
+ export interface ReviewEffectiveness {
16
+ toolCalls: Record<string, number>;
17
+ reviews: number;
18
+ failedReviews: number;
19
+ findingsReported: number;
20
+ findingsResolved: number;
21
+ stopChecks: number;
22
+ stopBlocks: number;
23
+ blockedStopsFollowedThrough: number;
24
+ stopsWithSelfReview: number;
25
+ }
26
+ export declare function readAgentEvents(cwd: string): AgentEvent[];
27
+ /** Append an event in the same format the MCP server writes; never throws. */
28
+ export declare function appendAgentEvent(cwd: string, event: AgentEvent): void;
29
+ /** `rule:file` keys, so a finding that moved lines is still the same finding. */
30
+ export declare function findingKeys(findings: Array<{
31
+ id?: string;
32
+ file?: string;
33
+ }>): string[];
34
+ export declare function computeEffectiveness(events: AgentEvent[]): ReviewEffectiveness;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Is the review loop working? Computed from the local event log
3
+ * (`.rigour/events.jsonl`, written by the MCP server and the stop hook), so
4
+ * nothing leaves the machine.
5
+ *
6
+ * - a finding is resolved when a later review no longer reports it (same rule, same file);
7
+ * - a blocked stop is followed through when the same session later stops cleanly;
8
+ * - a self-review is a rigour_review call made before an attempt to stop.
9
+ */
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import { randomUUID } from 'crypto';
13
+ const EVENTS = path.join('.rigour', 'events.jsonl');
14
+ export function readAgentEvents(cwd) {
15
+ let text = '';
16
+ try {
17
+ text = fs.readFileSync(path.join(cwd, EVENTS), 'utf8');
18
+ }
19
+ catch {
20
+ return [];
21
+ }
22
+ return text.split('\n').filter(Boolean).flatMap(line => {
23
+ try {
24
+ return [JSON.parse(line)];
25
+ }
26
+ catch {
27
+ return [];
28
+ }
29
+ });
30
+ }
31
+ /** Append an event in the same format the MCP server writes; never throws. */
32
+ export function appendAgentEvent(cwd, event) {
33
+ try {
34
+ fs.mkdirSync(path.join(cwd, '.rigour'), { recursive: true });
35
+ fs.appendFileSync(path.join(cwd, EVENTS), JSON.stringify({ id: randomUUID(), timestamp: new Date().toISOString(), ...event }) + '\n');
36
+ }
37
+ catch {
38
+ // The event log is best-effort; it must never break a hook or a tool call.
39
+ }
40
+ }
41
+ /** `rule:file` keys, so a finding that moved lines is still the same finding. */
42
+ export function findingKeys(findings) {
43
+ return findings.map(f => `${f.id ?? '?'}:${f.file ?? '?'}`);
44
+ }
45
+ export function computeEffectiveness(events) {
46
+ const stats = {
47
+ toolCalls: {}, reviews: 0, failedReviews: 0, findingsReported: 0, findingsResolved: 0,
48
+ stopChecks: 0, stopBlocks: 0, blockedStopsFollowedThrough: 0, stopsWithSelfReview: 0,
49
+ };
50
+ const open = new Set();
51
+ const blockedSessions = new Set();
52
+ let reviewedSinceStop = false;
53
+ for (const event of events) {
54
+ if (event.type === 'tool_call' && event.tool) {
55
+ stats.toolCalls[event.tool] = (stats.toolCalls[event.tool] ?? 0) + 1;
56
+ if (event.tool === 'rigour_review')
57
+ reviewedSinceStop = true;
58
+ }
59
+ else if (event.type === 'tool_response' && event.tool === 'rigour_review') {
60
+ countReview(stats, open, reviewKeys(event));
61
+ }
62
+ else if (event.type === 'stop_review') {
63
+ countStop(stats, blockedSessions, event, reviewedSinceStop);
64
+ reviewedSinceStop = false;
65
+ }
66
+ }
67
+ return stats;
68
+ }
69
+ function countReview(stats, open, keys) {
70
+ if (keys === null)
71
+ return;
72
+ stats.reviews++;
73
+ if (keys.length)
74
+ stats.failedReviews++;
75
+ const now = new Set(keys);
76
+ for (const key of [...open]) {
77
+ if (!now.has(key)) {
78
+ open.delete(key);
79
+ stats.findingsResolved++;
80
+ }
81
+ }
82
+ for (const key of now) {
83
+ if (!open.has(key)) {
84
+ open.add(key);
85
+ stats.findingsReported++;
86
+ }
87
+ }
88
+ }
89
+ function countStop(stats, blockedSessions, event, selfReviewed) {
90
+ const session = event.session ?? 'default';
91
+ stats.stopChecks++;
92
+ if (selfReviewed)
93
+ stats.stopsWithSelfReview++;
94
+ if (event.blocked) {
95
+ stats.stopBlocks++;
96
+ blockedSessions.add(session);
97
+ }
98
+ else if (blockedSessions.delete(session)) {
99
+ stats.blockedStopsFollowedThrough++;
100
+ }
101
+ }
102
+ /** Finding keys from a rigour_review response, or null when it is not a review result. */
103
+ function reviewKeys(event) {
104
+ const text = event.content?.find(c => c.type === 'text')?.text;
105
+ if (!text)
106
+ return null;
107
+ try {
108
+ const body = JSON.parse(text);
109
+ return Array.isArray(body.failures) ? findingKeys(body.failures) : null;
110
+ }
111
+ catch {
112
+ return null;
113
+ }
114
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { appendAgentEvent, computeEffectiveness, readAgentEvents } from './effectiveness.js';
6
+ const review = (failures) => [
7
+ { type: 'tool_call', tool: 'rigour_review' },
8
+ { type: 'tool_response', tool: 'rigour_review', content: [{ type: 'text', text: JSON.stringify({ status: failures.length ? 'FAIL' : 'PASS', failures }) }] },
9
+ ];
10
+ describe('computeEffectiveness', () => {
11
+ it('counts reviews, findings resolved by later reviews, and tool calls', () => {
12
+ const events = [
13
+ { type: 'tool_call', tool: 'rigour_recall' },
14
+ ...review([{ id: 'semantic-bugs', file: 'a.ts' }, { id: 'promise-safety', file: 'b.ts' }]),
15
+ ...review([{ id: 'promise-safety', file: 'b.ts' }]),
16
+ ...review([]),
17
+ ];
18
+ expect(computeEffectiveness(events)).toMatchObject({
19
+ reviews: 3, failedReviews: 2, findingsReported: 2, findingsResolved: 2,
20
+ toolCalls: { rigour_recall: 1, rigour_review: 3 },
21
+ });
22
+ });
23
+ it('follows a blocked stop through to a clean stop in the same session, and counts self-reviews', () => {
24
+ const events = [
25
+ { type: 'stop_review', session: 's1', blocked: true },
26
+ ...review([]),
27
+ { type: 'stop_review', session: 's1', blocked: false },
28
+ { type: 'stop_review', session: 's2', blocked: false },
29
+ ];
30
+ expect(computeEffectiveness(events)).toMatchObject({
31
+ stopChecks: 3, stopBlocks: 1, blockedStopsFollowedThrough: 1, stopsWithSelfReview: 1,
32
+ });
33
+ });
34
+ it('reads the log it writes and skips malformed lines', () => {
35
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'effectiveness-'));
36
+ appendAgentEvent(dir, { type: 'stop_review', blocked: true });
37
+ fs.appendFileSync(path.join(dir, '.rigour', 'events.jsonl'), 'not json\n');
38
+ expect(readAgentEvents(dir).map(e => e.type)).toEqual(['stop_review']);
39
+ expect(readAgentEvents(path.join(dir, 'missing'))).toEqual([]);
40
+ fs.rmSync(dir, { recursive: true, force: true });
41
+ });
42
+ });
@@ -0,0 +1,9 @@
1
+ export type DiffSource = {
2
+ mode: 'working';
3
+ } | {
4
+ mode: 'base';
5
+ base: string;
6
+ };
7
+ export declare class GitDiffError extends Error {
8
+ }
9
+ export declare function diffFromGit(cwd: string, source?: DiffSource): string;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The diff to review, taken from git so callers do not have to build one.
3
+ *
4
+ * - working: uncommitted changes against HEAD, staged or not, plus new files
5
+ * git does not track yet (what an agent has just written).
6
+ * - base: everything on this branch since it left `base` (the merge-base),
7
+ * including uncommitted changes (what a pull request contains).
8
+ */
9
+ import { spawnSync } from 'child_process';
10
+ export class GitDiffError extends Error {
11
+ }
12
+ export function diffFromGit(cwd, source = { mode: 'working' }) {
13
+ if (source.mode === 'base') {
14
+ const mergeBase = git(cwd, ['merge-base', source.base, 'HEAD']).trim();
15
+ return git(cwd, ['diff', '--no-color', '--no-ext-diff', mergeBase]);
16
+ }
17
+ const tracked = hasHead(cwd) ? git(cwd, ['diff', '--no-color', '--no-ext-diff', 'HEAD']) : '';
18
+ return tracked + untrackedFiles(cwd).map(file => newFileDiff(cwd, file)).join('');
19
+ }
20
+ function untrackedFiles(cwd) {
21
+ return git(cwd, ['ls-files', '--others', '--exclude-standard', '-z']).split('\0').filter(Boolean);
22
+ }
23
+ /** `git diff --no-index` exits 1 when the files differ, which for a new file is always. */
24
+ function newFileDiff(cwd, file) {
25
+ const result = spawnSync('git', ['diff', '--no-color', '--no-ext-diff', '--no-index', '--', '/dev/null', file], {
26
+ cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024,
27
+ });
28
+ if (result.status !== 0 && result.status !== 1)
29
+ throw new GitDiffError(`git diff for new file ${file} failed: ${result.stderr.trim()}`);
30
+ return result.stdout;
31
+ }
32
+ function hasHead(cwd) {
33
+ return spawnSync('git', ['rev-parse', '--verify', '--quiet', 'HEAD'], { cwd }).status === 0;
34
+ }
35
+ function git(cwd, args) {
36
+ const result = spawnSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
37
+ if (result.error)
38
+ throw new GitDiffError(`git is not available: ${result.error.message}`);
39
+ if (result.status !== 0)
40
+ throw new GitDiffError(`git ${args.join(' ')} failed: ${result.stderr.trim()}`);
41
+ return result.stdout;
42
+ }
@@ -0,0 +1,36 @@
1
+ import type { Config, DeepOptions, Failure, Report } from '../types/index.js';
2
+ import { type DiffSource } from './git-diff.js';
3
+ export interface ReviewInput {
4
+ cwd: string;
5
+ config: Config;
6
+ /** A unified diff; when omitted, it is taken from git (`source`). */
7
+ diff?: string;
8
+ source?: DiffSource;
9
+ /** Review exactly these files instead of the ones the diff touches. */
10
+ files?: string[];
11
+ /** Deep analysis; `focusLines` is filled from the diff. */
12
+ deep?: Omit<DeepOptions, 'focusLines'>;
13
+ }
14
+ export interface ReviewResult {
15
+ status: 'PASS' | 'FAIL' | 'ERROR';
16
+ findings: Failure[];
17
+ fileFindings: Failure[];
18
+ unlocated: number;
19
+ excludedOutsideChangedLines: number;
20
+ changedLines: Record<string, Set<number>>;
21
+ report: Report | null;
22
+ deepError?: string;
23
+ }
24
+ export interface ReviewFinding {
25
+ id: string;
26
+ gate: string;
27
+ severity: string;
28
+ provenance: string;
29
+ message: string;
30
+ file: string;
31
+ line: number | null;
32
+ suggestion?: string;
33
+ }
34
+ export declare function reviewChange(input: ReviewInput): Promise<ReviewResult>;
35
+ /** The JSON shape of a finding, shared by the CLI's --json and the MCP tool. */
36
+ export declare function toReviewFinding(failure: Failure): ReviewFinding;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * reviewChange: the one review engine behind `rigour review`, the MCP
3
+ * `rigour_review` tool and the GitHub integration.
4
+ *
5
+ * Runs the configured gates on the files a change touches and keeps what
6
+ * belongs to the change (see changed-lines.ts). A deep run that was asked for
7
+ * but could not analyze anything makes the result ERROR, never a clean PASS.
8
+ */
9
+ import { GateRunner } from '../gates/runner.js';
10
+ import { changedLinesByFile, parseDiff } from '../utils/diff.js';
11
+ import { normalizeScopePatterns } from '../utils/scope.js';
12
+ import { deepAnalysisError } from '../utils/deep-status.js';
13
+ import { splitByChangedLines } from './changed-lines.js';
14
+ import { diffFromGit } from './git-diff.js';
15
+ export async function reviewChange(input) {
16
+ const diff = input.diff ?? diffFromGit(input.cwd, input.source);
17
+ const changedLines = parseDiff(diff);
18
+ const targets = input.files?.length ? input.files : Object.keys(changedLines);
19
+ if (targets.length === 0) {
20
+ return { status: 'PASS', findings: [], fileFindings: [], unlocated: 0, excludedOutsideChangedLines: 0, changedLines, report: null };
21
+ }
22
+ const deep = input.deep ? { ...input.deep, focusLines: changedLinesByFile(changedLines) } : undefined;
23
+ const report = await new GateRunner(input.config).run(input.cwd, await normalizeScopePatterns(input.cwd, targets), deep);
24
+ const split = splitByChangedLines(report.failures, changedLines);
25
+ const deepError = deepAnalysisError(report);
26
+ return {
27
+ status: deepError ? 'ERROR' : split.findings.length > 0 ? 'FAIL' : 'PASS',
28
+ findings: split.findings,
29
+ fileFindings: split.fileFindings,
30
+ unlocated: split.unlocated,
31
+ excludedOutsideChangedLines: split.outside,
32
+ changedLines,
33
+ report,
34
+ ...(deepError ? { deepError } : {}),
35
+ };
36
+ }
37
+ /** The JSON shape of a finding, shared by the CLI's --json and the MCP tool. */
38
+ export function toReviewFinding(failure) {
39
+ return {
40
+ id: failure.id,
41
+ gate: failure.title,
42
+ severity: failure.severity || 'medium',
43
+ provenance: failure.provenance || 'traditional',
44
+ message: failure.details,
45
+ file: failure.files?.[0] || '',
46
+ line: failure.line ?? null,
47
+ ...(failure.hint ? { suggestion: failure.hint } : {}),
48
+ };
49
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,87 @@
1
+ import { execFileSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
+ import { ConfigSchema } from '../types/index.js';
7
+ import { parseDiff } from '../utils/diff.js';
8
+ import { splitByChangedLines } from './changed-lines.js';
9
+ import { diffFromGit } from './git-diff.js';
10
+ import { reviewChange, toReviewFinding } from './review.js';
11
+ const LEAKY = [
12
+ 'export async function notify(endpoint: string, signature: string) {',
13
+ " return fetch(endpoint, { method: 'POST', headers: { 'x-hook-signature': signature } });",
14
+ '}',
15
+ '',
16
+ ].join('\n');
17
+ function failure(file, line) {
18
+ return { id: 'x', title: 'X', details: 'd', files: file ? [file] : [], ...(line !== undefined ? { line } : {}) };
19
+ }
20
+ describe('splitByChangedLines', () => {
21
+ it('separates changed-line, file-level, unlocated and outside findings', () => {
22
+ const changed = { 'a.ts': new Set([3]) };
23
+ const split = splitByChangedLines([failure('a.ts', 3), failure('a.ts', 9), failure('a.ts'), failure('b.ts'), failure(undefined)], changed);
24
+ expect(split.findings.map(f => f.line)).toEqual([3]);
25
+ expect(split.fileFindings).toHaveLength(1);
26
+ expect(split.unlocated).toBe(1);
27
+ expect(split.outside).toBe(2);
28
+ });
29
+ });
30
+ describe('git-backed review', () => {
31
+ let repo;
32
+ const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' });
33
+ const write = (rel, body) => {
34
+ fs.mkdirSync(path.dirname(path.join(repo, rel)), { recursive: true });
35
+ fs.writeFileSync(path.join(repo, rel), body);
36
+ };
37
+ beforeEach(() => {
38
+ repo = fs.mkdtempSync(path.join(os.tmpdir(), 'review-core-'));
39
+ git('init', '-q', '-b', 'main');
40
+ git('config', 'user.email', 't@example.com');
41
+ git('config', 'user.name', 't');
42
+ git('config', 'commit.gpgsign', 'false');
43
+ });
44
+ afterEach(() => { fs.rmSync(repo, { recursive: true, force: true }); });
45
+ it('works in a repository with no commits yet', () => {
46
+ write('src/new.ts', 'export const a = 1;\n');
47
+ expect(parseDiff(diffFromGit(repo))).toEqual({ 'src/new.ts': new Set([1]) });
48
+ });
49
+ it('takes uncommitted edits and untracked files from the working tree', () => {
50
+ write('src/a.ts', 'export const a = 1;\nexport const b = 2;\n');
51
+ git('add', '-A');
52
+ git('commit', '-qm', 'init');
53
+ write('src/a.ts', 'export const a = 1;\nexport const b = 3;\n');
54
+ write('src/new.ts', 'export const c = 1;\nexport const d = 2;\n');
55
+ expect(parseDiff(diffFromGit(repo))).toEqual({ 'src/a.ts': new Set([2]), 'src/new.ts': new Set([1, 2]) });
56
+ });
57
+ it('takes a branch against its merge-base, committed and uncommitted', () => {
58
+ write('src/a.ts', 'export const a = 1;\n');
59
+ git('add', '-A');
60
+ git('commit', '-qm', 'init');
61
+ git('checkout', '-q', '-b', 'feature');
62
+ write('src/b.ts', 'export const b = 1;\n');
63
+ git('add', '-A');
64
+ git('commit', '-qm', 'feature');
65
+ write('src/a.ts', 'export const a = 1;\nexport const z = 2;\n');
66
+ expect(parseDiff(diffFromGit(repo, { mode: 'base', base: 'main' }))).toEqual({ 'src/a.ts': new Set([2]), 'src/b.ts': new Set([1]) });
67
+ });
68
+ it('fails a change that introduces a bug and ignores the same bug in untouched code', async () => {
69
+ write('src/old.ts', LEAKY);
70
+ git('add', '-A');
71
+ git('commit', '-qm', 'init');
72
+ write('src/notify.ts', LEAKY);
73
+ const config = ConfigSchema.parse({ version: 1, gates: { semantic_bugs: { enabled: true } } });
74
+ const result = await reviewChange({ cwd: repo, config });
75
+ const semantic = result.findings.filter(f => f.id === 'semantic-bugs');
76
+ expect(result.status).toBe('FAIL');
77
+ expect(semantic.map(f => [f.files?.[0], f.line])).toEqual([['src/notify.ts', 2]]);
78
+ expect(toReviewFinding(semantic[0])).toMatchObject({ id: 'semantic-bugs', file: 'src/notify.ts', line: 2, severity: 'high' });
79
+ });
80
+ it('passes when nothing changed', async () => {
81
+ write('src/a.ts', 'export const a = 1;\n');
82
+ git('add', '-A');
83
+ git('commit', '-qm', 'init');
84
+ const result = await reviewChange({ cwd: repo, config: ConfigSchema.parse({ version: 1 }) });
85
+ expect(result).toMatchObject({ status: 'PASS', findings: [], report: null });
86
+ });
87
+ });
@@ -23,3 +23,5 @@ export interface LearnReport {
23
23
  }>;
24
24
  }
25
25
  export declare function learnFromFix(input: LearnInput): Promise<LearnReport>;
26
+ /** Learn from one file's before and after (e.g. an agent's fix), validated against the repository at `repoDir`. */
27
+ export declare function learnFromFileChange(repoDir: string, file: string, before: string, after: string, maxHits?: number): Promise<LearnReport>;
@@ -8,6 +8,8 @@
8
8
  * reported with the reason the most specific one failed.
9
9
  */
10
10
  import crypto from 'crypto';
11
+ import fs from 'fs';
12
+ import os from 'os';
11
13
  import path from 'path';
12
14
  import { globby } from 'globby';
13
15
  import { analyzeFiles } from '../engine.js';
@@ -47,6 +49,22 @@ export async function learnFromFix(input) {
47
49
  }
48
50
  return report;
49
51
  }
52
+ /** Learn from one file's before and after (e.g. an agent's fix), validated against the repository at `repoDir`. */
53
+ export async function learnFromFileChange(repoDir, file, before, after, maxHits) {
54
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rigour-learn-'));
55
+ try {
56
+ const place = (tree, content) => {
57
+ const target = path.join(root, tree, file);
58
+ fs.mkdirSync(path.dirname(target), { recursive: true });
59
+ fs.writeFileSync(target, content);
60
+ return path.join(root, tree);
61
+ };
62
+ return await learnFromFix({ beforeDir: place('before', before), afterDir: place('after', after), repoDir, files: [file], maxHits });
63
+ }
64
+ finally {
65
+ fs.rmSync(root, { recursive: true, force: true });
66
+ }
67
+ }
50
68
  function openTree(cwd, file) {
51
69
  const root = path.join(cwd, file);
52
70
  const [program] = [...programBatches([root], loadProjectConfig(cwd).options)];
@@ -235,7 +235,7 @@ export const UNIVERSAL_CONFIG = {
235
235
  track_returns: true,
236
236
  },
237
237
  semantic_bugs: {
238
- enabled: false,
238
+ enabled: true,
239
239
  },
240
240
  side_effect_analysis: {
241
241
  enabled: true,
@@ -266,8 +266,9 @@ export const GatesSchema = z.object({
266
266
  audit_log: z.boolean().optional().default(true),
267
267
  }).optional().default({}),
268
268
  // v4.3+ Side-Effect Safety Analysis
269
+ // On by default: full scans of three real repositories reported no finding that was not a real defect.
269
270
  semantic_bugs: z.object({
270
- enabled: z.boolean().optional().default(false),
271
+ enabled: z.boolean().optional().default(true),
271
272
  rules: z.array(z.string()).optional(),
272
273
  }).optional().default({}),
273
274
  side_effect_analysis: z.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "6.3.1",
3
+ "version": "6.4.0",
4
4
  "description": "AI-native quality gate engine with local Bayesian learning. AST analysis, drift detection, Fix Packet generation, and agent self-healing across TypeScript, JavaScript, Python, Go, Ruby, and C#.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://rigour.run",
@@ -67,11 +67,11 @@
67
67
  "pg": "^8.16.3",
68
68
  "sqlite3": "^5.1.7",
69
69
  "openai": "^4.104.0",
70
- "@rigour-labs/brain-darwin-x64": "6.3.1",
71
- "@rigour-labs/brain-darwin-arm64": "6.3.1",
72
- "@rigour-labs/brain-linux-x64": "6.3.1",
73
- "@rigour-labs/brain-linux-arm64": "6.3.1",
74
- "@rigour-labs/brain-win-x64": "6.3.1"
70
+ "@rigour-labs/brain-darwin-arm64": "6.4.0",
71
+ "@rigour-labs/brain-linux-arm64": "6.4.0",
72
+ "@rigour-labs/brain-darwin-x64": "6.4.0",
73
+ "@rigour-labs/brain-linux-x64": "6.4.0",
74
+ "@rigour-labs/brain-win-x64": "6.4.0"
75
75
  },
76
76
  "devDependencies": {
77
77
  "@types/fs-extra": "^11.0.4",