@rigour-labs/core 6.4.1 → 6.4.2

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.
@@ -55,3 +55,6 @@ export declare class DeepAnalysisGate extends Gate {
55
55
  private createCloudAgent;
56
56
  private toFailure;
57
57
  }
58
+ /** Per-call options; unset config falls back to per-provider defaults. */
59
+ /** Per-call inference timeout: a local 7B reads a file with its reference pack far slower than the small tiers. */
60
+ export declare function defaultTimeout(options: DeepOptions): number;
@@ -175,12 +175,18 @@ function isCloud(options) {
175
175
  return !!options.apiKey && !!options.provider && options.provider !== 'local';
176
176
  }
177
177
  /** Per-call options; unset config falls back to per-provider defaults. */
178
+ /** Per-call inference timeout: a local 7B reads a file with its reference pack far slower than the small tiers. */
179
+ export function defaultTimeout(options) {
180
+ if (isCloud(options))
181
+ return 120_000;
182
+ return options.max ? 240_000 : 60_000;
183
+ }
178
184
  function inferenceOptions(config) {
179
185
  const cloud = isCloud(config.options);
180
186
  return {
181
187
  maxTokens: config.maxTokens ?? (cloud ? 4096 : 1024),
182
188
  temperature: config.temperature ?? 0.1,
183
- timeout: config.timeoutMs ?? (cloud ? 120_000 : 60_000),
189
+ timeout: config.timeoutMs ?? defaultTimeout(config.options),
184
190
  jsonMode: true,
185
191
  };
186
192
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { defaultTimeout } from './deep-analysis.js';
3
+ describe('deep inference timeout', () => {
4
+ it('gives the local 7B tier four minutes a call, the small tiers one, and cloud two', () => {
5
+ expect(defaultTimeout({ enabled: true })).toBe(60_000);
6
+ expect(defaultTimeout({ enabled: true, pro: true })).toBe(60_000);
7
+ expect(defaultTimeout({ enabled: true, max: true })).toBe(240_000);
8
+ expect(defaultTimeout({ enabled: true, apiKey: 'k', provider: 'claude' })).toBe(120_000);
9
+ });
10
+ });
@@ -3,9 +3,14 @@
3
3
  *
4
4
  * Detects when AI subtly changes business logic in functions:
5
5
  * - Comparison operator mutations: >= became > (off-by-one)
6
- * - Return statement additions/removals
7
- * - Branch count changes (new if/else added or removed)
8
- * - Call sequence changes (function calls reordered)
6
+ * - Return statement additions/removals (opt-in: track_returns)
7
+ * - Branch count changes, new if/else added or removed (opt-in: track_branches)
8
+ *
9
+ * Return and branch counts are off by default: a change that edits a function
10
+ * usually changes them on purpose, so they fired on almost every edited function
11
+ * without saying what was wrong. Judged against review comments developers acted
12
+ * on, 0 of 7 such findings raised the reviewer's issue. An operator mutation is
13
+ * specific and actionable, so it stays on.
9
14
  *
10
15
  * This is the HARDEST drift to catch because:
11
16
  * - Code still compiles
@@ -3,9 +3,14 @@
3
3
  *
4
4
  * Detects when AI subtly changes business logic in functions:
5
5
  * - Comparison operator mutations: >= became > (off-by-one)
6
- * - Return statement additions/removals
7
- * - Branch count changes (new if/else added or removed)
8
- * - Call sequence changes (function calls reordered)
6
+ * - Return statement additions/removals (opt-in: track_returns)
7
+ * - Branch count changes, new if/else added or removed (opt-in: track_branches)
8
+ *
9
+ * Return and branch counts are off by default: a change that edits a function
10
+ * usually changes them on purpose, so they fired on almost every edited function
11
+ * without saying what was wrong. Judged against review comments developers acted
12
+ * on, 0 of 7 such findings raised the reviewer's issue. An operator mutation is
13
+ * specific and actionable, so it stays on.
9
14
  *
10
15
  * This is the HARDEST drift to catch because:
11
16
  * - Code still compiles
@@ -34,8 +39,8 @@ export class LogicDriftGate extends Gate {
34
39
  enabled: config.enabled ?? true,
35
40
  baseline_path: config.baseline_path ?? '.rigour/logic-baseline.json',
36
41
  track_operators: config.track_operators ?? true,
37
- track_branches: config.track_branches ?? true,
38
- track_returns: config.track_returns ?? true,
42
+ track_branches: config.track_branches ?? false,
43
+ track_returns: config.track_returns ?? false,
39
44
  };
40
45
  }
41
46
  get provenance() { return 'ai-drift'; }
@@ -35,6 +35,13 @@ describe('LogicDriftGate Git baseline', () => {
35
35
  git('merge', '-q', '--ff-only', 'feature');
36
36
  expect(await gate.run({ cwd })).toEqual([]);
37
37
  }, 30_000);
38
+ it('reports return and branch count changes only when opted in', async () => {
39
+ git('switch', '-q', '-c', 'feature');
40
+ fs.writeFileSync(path.join(cwd, 'src/rules.ts'), 'export function eligible(score: number) {\n if (score < 0) return false;\n if (score > 100) return false;\n return score >= 10;\n}\n');
41
+ expect(await new LogicDriftGate().run({ cwd })).toEqual([]);
42
+ const opted = await new LogicDriftGate({ track_returns: true, track_branches: true }).run({ cwd });
43
+ expect(opted.map(f => f.title).sort()).toEqual(['Logic Drift: Branch Change', 'Logic Drift: Return Change']);
44
+ }, 30_000);
38
45
  it('does not use a moving local snapshot when Git has no main reference', async () => {
39
46
  git('switch', '-q', '-c', 'feature');
40
47
  git('branch', '-D', 'main');
@@ -104,7 +104,9 @@ export class PromiseSafetyGate extends Gate {
104
104
  // Also count .then().then().catch() chains — the catch at the end covers all
105
105
  if (j > i && braceDepth < 0)
106
106
  break; // Exited enclosing scope
107
- if (j > i && /^(?:const|let|var|function|class|export|import|if|for|while|return)\b/.test(lookahead.trim()))
107
+ // A statement keyword ends the chain only at the chain's own depth; inside the
108
+ // .then callback's body (depth > 0) `if`/`return` belong to the callback.
109
+ if (j > i && braceDepth <= 0 && /^(?:const|let|var|function|class|export|import|if|for|while|return)\b/.test(lookahead.trim()))
108
110
  break;
109
111
  }
110
112
  if (!hasCatch && !isInsideTryBlock(lines, i) && !/(?:const|let|var)\s+\w+\s*=/.test(line)) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
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 { PromiseSafetyGate } from './promise-safety.js';
6
+ describe('PromiseSafetyGate unhandled .then', () => {
7
+ let cwd;
8
+ beforeEach(() => { cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'promise-safety-')); });
9
+ afterEach(() => { fs.rmSync(cwd, { recursive: true, force: true }); });
10
+ async function thenFindings(code) {
11
+ fs.writeFileSync(path.join(cwd, 'session.ts'), code);
12
+ const failures = await new PromiseSafetyGate().run({ cwd });
13
+ return failures.filter(f => f.details.includes('unhandled-then')).length;
14
+ }
15
+ it('flags a .then chain with no .catch', async () => {
16
+ expect(await thenFindings('export function wake(p: Promise<number>) {\n p.then(n => {\n if (n > 1) log(n);\n });\n}\ndeclare function log(n: number): void;\n')).toBe(1);
17
+ });
18
+ it('finds the .catch that closes a chain whose callback body has if and return statements', async () => {
19
+ const code = [
20
+ 'export function wake(accepted: Promise<{ action: string }>) {',
21
+ ' void accepted',
22
+ ' .then(result => {',
23
+ " if (result.action === 'wake') {",
24
+ ' return run();',
25
+ ' }',
26
+ ' return undefined;',
27
+ ' })',
28
+ ' .catch(() => {});',
29
+ '}',
30
+ 'declare function run(): Promise<void>;',
31
+ ].join('\n');
32
+ expect(await thenFindings(code)).toBe(0);
33
+ });
34
+ });
@@ -67,7 +67,8 @@ export const VULNERABILITY_PATTERNS = [
67
67
  // Hardcoded Secrets
68
68
  {
69
69
  type: 'hardcoded_secrets',
70
- regex: /(?:password|secret|api_key|apikey|auth_token|access_token|private_key)\s*[:=]\s*['"][^'"]{8,}['"]/gi,
70
+ // A secret literal has no whitespace: `hidePassword: 'Hide password'` is a UI label, not a credential.
71
+ regex: /(?:password|secret|api_key|apikey|auth_token|access_token|private_key)\s*[:=]\s*['"][^'"\s]{8,}['"]/gi,
71
72
  severity: 'critical',
72
73
  description: 'Hardcoded secret detected in code',
73
74
  cwe: 'CWE-798',
@@ -85,6 +85,14 @@ describe('SecurityPatternsGate', () => {
85
85
  const vulns = await checkSecurityPatterns(filePath);
86
86
  expect(vulns.some(v => v.type === 'hardcoded_secrets')).toBe(true);
87
87
  });
88
+ it('does not take a UI label that mentions a password for a secret', async () => {
89
+ const filePath = path.join(testDir, 'be.ts');
90
+ fs.writeFileSync(filePath, `
91
+ export default { authForm: { hidePassword: 'Схаваць пароль', showPassword: 'Show the password' } };
92
+ `);
93
+ const vulns = await checkSecurityPatterns(filePath);
94
+ expect(vulns.some(v => v.type === 'hardcoded_secrets')).toBe(false);
95
+ });
88
96
  it('should detect password assignments', async () => {
89
97
  const filePath = path.join(testDir, 'auth.ts');
90
98
  fs.writeFileSync(filePath, `
@@ -231,8 +231,8 @@ export const UNIVERSAL_CONFIG = {
231
231
  enabled: true,
232
232
  baseline_path: '.rigour/logic-baseline.json',
233
233
  track_operators: true,
234
- track_branches: true,
235
- track_returns: true,
234
+ track_branches: false,
235
+ track_returns: false,
236
236
  },
237
237
  semantic_bugs: {
238
238
  enabled: true,
@@ -295,8 +295,8 @@ export const GatesSchema = z.object({
295
295
  enabled: z.boolean().optional().default(true),
296
296
  baseline_path: z.string().optional().default('.rigour/logic-baseline.json'),
297
297
  track_operators: z.boolean().optional().default(true),
298
- track_branches: z.boolean().optional().default(true),
299
- track_returns: z.boolean().optional().default(true),
298
+ track_branches: z.boolean().optional().default(false), // opt-in: counts change on purpose in most edits
299
+ track_returns: z.boolean().optional().default(false), // opt-in: same
300
300
  }).optional().default({}),
301
301
  // v4.0+ Deep Analysis (LLM-powered)
302
302
  deep: z.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "6.4.1",
3
+ "version": "6.4.2",
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-arm64": "6.4.1",
71
- "@rigour-labs/brain-darwin-x64": "6.4.1",
72
- "@rigour-labs/brain-linux-x64": "6.4.1",
73
- "@rigour-labs/brain-win-x64": "6.4.1",
74
- "@rigour-labs/brain-linux-arm64": "6.4.1"
70
+ "@rigour-labs/brain-darwin-arm64": "6.4.2",
71
+ "@rigour-labs/brain-darwin-x64": "6.4.2",
72
+ "@rigour-labs/brain-linux-arm64": "6.4.2",
73
+ "@rigour-labs/brain-win-x64": "6.4.2",
74
+ "@rigour-labs/brain-linux-x64": "6.4.2"
75
75
  },
76
76
  "devDependencies": {
77
77
  "@types/fs-extra": "^11.0.4",