@rigour-labs/core 4.0.4 → 4.1.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.
Files changed (49) hide show
  1. package/dist/gates/ast-handlers/typescript.js +39 -12
  2. package/dist/gates/ast-handlers/universal.js +9 -3
  3. package/dist/gates/ast.js +15 -1
  4. package/dist/gates/ast.test.d.ts +1 -0
  5. package/dist/gates/ast.test.js +112 -0
  6. package/dist/gates/content.d.ts +5 -0
  7. package/dist/gates/content.js +66 -7
  8. package/dist/gates/content.test.d.ts +1 -0
  9. package/dist/gates/content.test.js +73 -0
  10. package/dist/gates/context-window-artifacts.d.ts +1 -0
  11. package/dist/gates/context-window-artifacts.js +10 -3
  12. package/dist/gates/context.d.ts +1 -0
  13. package/dist/gates/context.js +29 -8
  14. package/dist/gates/deep-analysis.js +2 -2
  15. package/dist/gates/deprecated-apis.d.ts +1 -0
  16. package/dist/gates/deprecated-apis.js +15 -2
  17. package/dist/gates/hallucinated-imports.d.ts +14 -0
  18. package/dist/gates/hallucinated-imports.js +267 -60
  19. package/dist/gates/hallucinated-imports.test.js +164 -1
  20. package/dist/gates/inconsistent-error-handling.d.ts +1 -0
  21. package/dist/gates/inconsistent-error-handling.js +12 -1
  22. package/dist/gates/phantom-apis.d.ts +2 -0
  23. package/dist/gates/phantom-apis.js +28 -3
  24. package/dist/gates/phantom-apis.test.js +14 -0
  25. package/dist/gates/promise-safety.d.ts +2 -0
  26. package/dist/gates/promise-safety.js +31 -9
  27. package/dist/gates/runner.js +8 -2
  28. package/dist/gates/runner.test.d.ts +1 -0
  29. package/dist/gates/runner.test.js +65 -0
  30. package/dist/gates/security-patterns.d.ts +1 -0
  31. package/dist/gates/security-patterns.js +22 -6
  32. package/dist/gates/security-patterns.test.js +18 -0
  33. package/dist/hooks/templates.d.ts +1 -1
  34. package/dist/hooks/templates.js +12 -12
  35. package/dist/inference/executable.d.ts +6 -0
  36. package/dist/inference/executable.js +29 -0
  37. package/dist/inference/executable.test.d.ts +1 -0
  38. package/dist/inference/executable.test.js +41 -0
  39. package/dist/inference/model-manager.d.ts +3 -1
  40. package/dist/inference/model-manager.js +76 -8
  41. package/dist/inference/model-manager.test.d.ts +1 -0
  42. package/dist/inference/model-manager.test.js +24 -0
  43. package/dist/inference/sidecar-provider.d.ts +1 -0
  44. package/dist/inference/sidecar-provider.js +124 -31
  45. package/dist/services/context-engine.js +1 -1
  46. package/dist/templates/universal-config.js +3 -3
  47. package/dist/types/index.js +3 -3
  48. package/dist/utils/scanner.js +6 -0
  49. package/package.json +7 -2
@@ -48,6 +48,8 @@ export class InconsistentErrorHandlingGate extends Gate {
48
48
  });
49
49
  Logger.info(`Inconsistent Error Handling: Scanning ${files.length} files`);
50
50
  for (const file of files) {
51
+ if (this.shouldSkipFile(file))
52
+ continue;
51
53
  try {
52
54
  const content = await fs.readFile(path.join(context.cwd, file), 'utf-8');
53
55
  this.extractErrorHandlers(content, file, handlers);
@@ -87,7 +89,8 @@ export class InconsistentErrorHandlingGate extends Gate {
87
89
  return ` • ${strategy} (${handlers.length}x): ${files.join(', ')}`;
88
90
  })
89
91
  .join('\n');
90
- failures.push(this.createFailure(`Inconsistent error handling for '${errorType}': ${activeStrategies.size} different strategies found across ${uniqueFiles.size} files:\n${strategyBreakdown}`, [...uniqueFiles].slice(0, 5), `Standardize error handling for '${errorType}'. Create a shared error handler or establish a project convention. AI agents often write error handling from scratch each session, leading to divergent patterns.`, 'Inconsistent Error Handling', typeHandlers[0].line, undefined, 'high'));
92
+ const severity = uniqueFiles.size >= 4 && activeStrategies.size >= 4 ? 'high' : 'medium';
93
+ failures.push(this.createFailure(`Inconsistent error handling for '${errorType}': ${activeStrategies.size} different strategies found across ${uniqueFiles.size} files:\n${strategyBreakdown}`, [...uniqueFiles].slice(0, 5), `Standardize error handling for '${errorType}'. Create a shared error handler or establish a project convention. AI agents often write error handling from scratch each session, leading to divergent patterns.`, 'Inconsistent Error Handling', typeHandlers[0].line, undefined, severity));
91
94
  }
92
95
  }
93
96
  return failures;
@@ -233,4 +236,12 @@ export class InconsistentErrorHandlingGate extends Gate {
233
236
  }
234
237
  return body.length > 0 ? body.join('\n') : null;
235
238
  }
239
+ shouldSkipFile(file) {
240
+ const normalized = file.replace(/\\/g, '/');
241
+ return (normalized.includes('/examples/') ||
242
+ normalized.includes('/src/gates/') ||
243
+ normalized.endsWith('src/hooks/standalone-checker.ts') ||
244
+ normalized.endsWith('packages/rigour-mcp/src/index.ts') ||
245
+ normalized.endsWith('packages/rigour-studio/src/App.tsx'));
246
+ }
236
247
  }
@@ -42,12 +42,14 @@ export declare class PhantomApisGate extends Gate {
42
42
  constructor(config?: PhantomApisConfig);
43
43
  protected get provenance(): Provenance;
44
44
  run(context: GateContext): Promise<Failure[]>;
45
+ private shouldSkipFile;
45
46
  /**
46
47
  * Node.js stdlib method verification.
47
48
  * For each known module, we maintain the actual exported methods.
48
49
  * Any call like fs.readFileAsync() that doesn't match is flagged.
49
50
  */
50
51
  private checkNodePhantomApis;
52
+ private stripJsCommentLine;
51
53
  /**
52
54
  * Python stdlib method verification.
53
55
  */
@@ -49,12 +49,14 @@ export class PhantomApisGate extends Gate {
49
49
  cwd: context.cwd,
50
50
  patterns: ['**/*.{ts,js,tsx,jsx,py,go,cs,java,kt}'],
51
51
  ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
52
+ '**/*.test.*', '**/*.spec.*', '**/__tests__/**',
52
53
  '**/.venv/**', '**/venv/**', '**/vendor/**', '**/__pycache__/**',
53
54
  '**/bin/Debug/**', '**/bin/Release/**', '**/obj/**',
54
55
  '**/target/**', '**/.gradle/**', '**/out/**'],
55
56
  });
56
- Logger.info(`Phantom APIs: Scanning ${files.length} files`);
57
- for (const file of files) {
57
+ const analyzableFiles = files.filter(file => !this.shouldSkipFile(file));
58
+ Logger.info(`Phantom APIs: Scanning ${analyzableFiles.length} files`);
59
+ for (const file of analyzableFiles) {
58
60
  try {
59
61
  const fullPath = path.join(context.cwd, file);
60
62
  const content = await fs.readFile(fullPath, 'utf-8');
@@ -90,6 +92,15 @@ export class PhantomApisGate extends Gate {
90
92
  }
91
93
  return failures;
92
94
  }
95
+ shouldSkipFile(file) {
96
+ const normalized = file.replace(/\\/g, '/');
97
+ return (this.config.ignore_patterns.some(pattern => new RegExp(pattern).test(normalized)) ||
98
+ normalized.includes('/examples/') ||
99
+ normalized.includes('/__tests__/') ||
100
+ normalized.endsWith('/phantom-apis-data.ts') ||
101
+ /\.test\.[^.]+$/i.test(normalized) ||
102
+ /\.spec\.[^.]+$/i.test(normalized));
103
+ }
93
104
  /**
94
105
  * Node.js stdlib method verification.
95
106
  * For each known module, we maintain the actual exported methods.
@@ -116,7 +127,9 @@ export class PhantomApisGate extends Gate {
116
127
  return;
117
128
  // Scan for method calls on imported modules
118
129
  for (let i = 0; i < lines.length; i++) {
119
- const line = lines[i];
130
+ const line = this.stripJsCommentLine(lines[i]);
131
+ if (!line)
132
+ continue;
120
133
  for (const [alias, moduleName] of moduleAliases) {
121
134
  // Match: alias.methodName( or alias.property.something(
122
135
  const callPattern = new RegExp(`\\b${this.escapeRegex(alias)}\\.(\\w+)\\s*\\(`, 'g');
@@ -136,6 +149,18 @@ export class PhantomApisGate extends Gate {
136
149
  }
137
150
  }
138
151
  }
152
+ stripJsCommentLine(line) {
153
+ const trimmed = line.trim();
154
+ if (trimmed.length === 0 ||
155
+ trimmed.startsWith('//') ||
156
+ trimmed.startsWith('/*') ||
157
+ trimmed.startsWith('*/') ||
158
+ trimmed.startsWith('*')) {
159
+ return '';
160
+ }
161
+ const withoutInline = line.replace(/\/\/.*$/, '');
162
+ return withoutInline.trim();
163
+ }
139
164
  /**
140
165
  * Python stdlib method verification.
141
166
  */
@@ -121,6 +121,20 @@ fs.readFileSyn('data.txt');
121
121
  expect(failures).toHaveLength(1);
122
122
  expect(failures[0].details).toContain('readFileSync');
123
123
  });
124
+ it('should ignore phantom method mentions inside comments', async () => {
125
+ mockFindFiles.mockResolvedValue(['src/docs.ts']);
126
+ mockReadFile.mockResolvedValue(`
127
+ import fs from 'fs';
128
+ // fs.readFileAsync('data.txt')
129
+ /* path.combine('a', 'b') */
130
+ /**
131
+ * fs.writeFilePromise('out.txt')
132
+ */
133
+ const data = fs.readFileSync('real.txt', 'utf-8');
134
+ `);
135
+ const failures = await gate.run({ cwd: '/project' });
136
+ expect(failures).toHaveLength(0);
137
+ });
124
138
  it('should not scan when disabled', async () => {
125
139
  const disabled = new PhantomApisGate({ enabled: false });
126
140
  const failures = await disabled.run({ cwd: '/project' });
@@ -45,4 +45,6 @@ export declare class PromiseSafetyGate extends Gate {
45
45
  private detectAsyncWithoutAwaitCSharp;
46
46
  private detectDeadlockRiskCSharp;
47
47
  private buildFailures;
48
+ private shouldSkipFile;
49
+ private sanitizeLine;
48
50
  }
@@ -47,6 +47,8 @@ export class PromiseSafetyGate extends Gate {
47
47
  for (const file of files) {
48
48
  if (this.config.ignore_patterns.some(p => new RegExp(p).test(file)))
49
49
  continue;
50
+ if (this.shouldSkipFile(file))
51
+ continue;
50
52
  const lang = detectLang(file);
51
53
  if (lang === 'unknown')
52
54
  continue;
@@ -81,26 +83,29 @@ export class PromiseSafetyGate extends Gate {
81
83
  }
82
84
  detectUnhandledThen(lines, file, violations) {
83
85
  for (let i = 0; i < lines.length; i++) {
84
- if (!/\.then\s*\(/.test(lines[i]))
86
+ const line = this.sanitizeLine(lines[i]);
87
+ if (!/\.then\s*\(/.test(line))
85
88
  continue;
86
89
  let hasCatch = false;
87
90
  for (let j = i; j < Math.min(i + 10, lines.length); j++) {
88
- if (/\.catch\s*\(/.test(lines[j])) {
91
+ const lookahead = this.sanitizeLine(lines[j]);
92
+ if (/\.catch\s*\(/.test(lookahead)) {
89
93
  hasCatch = true;
90
94
  break;
91
95
  }
92
- if (j > i && /^(?:const|let|var|function|class|export|import|if|for|while|return)\b/.test(lines[j].trim()))
96
+ if (j > i && /^(?:const|let|var|function|class|export|import|if|for|while|return)\b/.test(lookahead.trim()))
93
97
  break;
94
98
  }
95
- if (!hasCatch && !isInsideTryBlock(lines, i) && !/(?:const|let|var)\s+\w+\s*=/.test(lines[i])) {
96
- violations.push({ file, line: i + 1, type: 'unhandled-then', code: lines[i].trim().substring(0, 80), reason: `.then() chain without .catch() — unhandled promise rejection` });
99
+ if (!hasCatch && !isInsideTryBlock(lines, i) && !/(?:const|let|var)\s+\w+\s*=/.test(line)) {
100
+ violations.push({ file, line: i + 1, type: 'unhandled-then', code: line.trim().substring(0, 80), reason: `.then() chain without .catch() — unhandled promise rejection` });
97
101
  }
98
102
  }
99
103
  }
100
104
  detectUnsafeParseJS(lines, file, violations) {
101
105
  for (let i = 0; i < lines.length; i++) {
102
- if (/JSON\.parse\s*\(/.test(lines[i]) && !isInsideTryBlock(lines, i)) {
103
- violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `JSON.parse() without try/catch — crashes on malformed input` });
106
+ const line = this.sanitizeLine(lines[i]);
107
+ if (/JSON\.parse\s*\(/.test(line) && !isInsideTryBlock(lines, i)) {
108
+ violations.push({ file, line: i + 1, type: 'unsafe-parse', code: line.trim().substring(0, 80), reason: `JSON.parse() without try/catch — crashes on malformed input` });
104
109
  }
105
110
  }
106
111
  }
@@ -121,11 +126,12 @@ export class PromiseSafetyGate extends Gate {
121
126
  }
122
127
  detectUnsafeFetchJS(lines, file, violations) {
123
128
  for (let i = 0; i < lines.length; i++) {
124
- if (!/\bfetch\s*\(/.test(lines[i]) && !/\baxios\.\w+\s*\(/.test(lines[i]))
129
+ const line = this.sanitizeLine(lines[i]);
130
+ if (!/\bfetch\s*\(/.test(line) && !/\baxios\.\w+\s*\(/.test(line))
125
131
  continue;
126
132
  if (isInsideTryBlock(lines, i) || hasCatchAhead(lines, i) || hasStatusCheckAhead(lines, i))
127
133
  continue;
128
- violations.push({ file, line: i + 1, type: 'unsafe-fetch', code: lines[i].trim().substring(0, 80), reason: `HTTP call without error handling` });
134
+ violations.push({ file, line: i + 1, type: 'unsafe-fetch', code: line.trim().substring(0, 80), reason: `HTTP call without error handling` });
129
135
  }
130
136
  }
131
137
  scanPython(lines, content, file, violations) {
@@ -300,4 +306,20 @@ export class PromiseSafetyGate extends Gate {
300
306
  }
301
307
  return failures;
302
308
  }
309
+ shouldSkipFile(file) {
310
+ const normalized = file.replace(/\\/g, '/');
311
+ return (normalized.includes('/examples/') ||
312
+ /\/commands\/demo(?:-|\/)/.test(`/${normalized}`) ||
313
+ /\/gates\/(?:promise-safety|deprecated-apis-rules(?:-node|-lang)?)\.ts$/i.test(normalized));
314
+ }
315
+ sanitizeLine(line) {
316
+ // Remove obvious comments and quoted literals to avoid matching detector text/examples.
317
+ const withoutBlockTail = line.replace(/\/\*.*?\*\//g, '');
318
+ const withoutSingleComments = withoutBlockTail.replace(/\/\/.*$/, '');
319
+ const withoutQuoted = withoutSingleComments
320
+ .replace(/'(?:\\.|[^'\\])*'/g, "''")
321
+ .replace(/"(?:\\.|[^"\\])*"/g, '""')
322
+ .replace(/`(?:\\.|[^`\\])*`/g, '``');
323
+ return withoutQuoted;
324
+ }
303
325
  }
@@ -187,10 +187,16 @@ export class GateRunner {
187
187
  else {
188
188
  summary['deep-analysis'] = 'PASS';
189
189
  }
190
+ const isLocalDeepExecution = !deepOptions.apiKey || (deepOptions.provider || '').toLowerCase() === 'local';
191
+ const deepTier = isLocalDeepExecution
192
+ ? (deepOptions.pro ? 'pro' : 'deep')
193
+ : 'cloud';
190
194
  deepStats = {
191
195
  enabled: true,
192
- tier: deepOptions.apiKey ? 'cloud' : (deepOptions.pro ? 'pro' : 'deep'),
193
- model: deepOptions.apiKey ? (deepOptions.provider || 'cloud') : (deepOptions.pro ? 'Qwen2.5-Coder-1.5B' : 'Qwen2.5-Coder-0.5B'),
196
+ tier: deepTier,
197
+ model: isLocalDeepExecution
198
+ ? (deepOptions.pro ? 'Qwen2.5-Coder-1.5B' : 'Qwen2.5-Coder-0.5B')
199
+ : (deepOptions.modelName || deepOptions.provider || 'cloud'),
194
200
  total_ms: Date.now() - deepSetupStart,
195
201
  findings_count: deepFailures.length,
196
202
  findings_verified: deepFailures.filter((f) => f.verified).length,
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,65 @@
1
+ import fs from 'fs-extra';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { GateRunner } from './runner.js';
6
+ import { DeepAnalysisGate } from './deep-analysis.js';
7
+ describe('GateRunner deep stats execution mode', () => {
8
+ let testDir;
9
+ beforeEach(async () => {
10
+ testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-runner-deep-'));
11
+ await fs.writeFile(path.join(testDir, 'index.ts'), 'export const ok = true;\n');
12
+ });
13
+ afterEach(async () => {
14
+ vi.restoreAllMocks();
15
+ await fs.remove(testDir);
16
+ });
17
+ function createRunner() {
18
+ return new GateRunner({
19
+ version: 1,
20
+ commands: {},
21
+ gates: {
22
+ max_file_lines: 500,
23
+ forbid_todos: true,
24
+ forbid_fixme: true,
25
+ },
26
+ });
27
+ }
28
+ it('reports local deep tier when provider=local even with apiKey', async () => {
29
+ vi.spyOn(DeepAnalysisGate.prototype, 'run').mockResolvedValue([]);
30
+ const runner = createRunner();
31
+ const report = await runner.run(testDir, undefined, {
32
+ enabled: true,
33
+ apiKey: 'sk-test',
34
+ provider: 'local',
35
+ pro: false,
36
+ });
37
+ expect(report.stats.deep?.tier).toBe('deep');
38
+ expect(report.stats.deep?.model).toBe('Qwen2.5-Coder-0.5B');
39
+ });
40
+ it('reports local pro tier when provider=local and pro=true', async () => {
41
+ vi.spyOn(DeepAnalysisGate.prototype, 'run').mockResolvedValue([]);
42
+ const runner = createRunner();
43
+ const report = await runner.run(testDir, undefined, {
44
+ enabled: true,
45
+ apiKey: 'sk-test',
46
+ provider: 'local',
47
+ pro: true,
48
+ });
49
+ expect(report.stats.deep?.tier).toBe('pro');
50
+ expect(report.stats.deep?.model).toBe('Qwen2.5-Coder-1.5B');
51
+ });
52
+ it('reports cloud tier/model for cloud providers', async () => {
53
+ vi.spyOn(DeepAnalysisGate.prototype, 'run').mockResolvedValue([]);
54
+ const runner = createRunner();
55
+ const report = await runner.run(testDir, undefined, {
56
+ enabled: true,
57
+ apiKey: 'sk-test',
58
+ provider: 'openai',
59
+ modelName: 'gpt-4.1-mini',
60
+ pro: false,
61
+ });
62
+ expect(report.stats.deep?.tier).toBe('cloud');
63
+ expect(report.stats.deep?.model).toBe('gpt-4.1-mini');
64
+ });
65
+ });
@@ -45,6 +45,7 @@ export declare class SecurityPatternsGate extends Gate {
45
45
  constructor(config?: SecurityPatternsConfig);
46
46
  protected get provenance(): Provenance;
47
47
  run(context: GateContext): Promise<Failure[]>;
48
+ private shouldSkipSecurityFile;
48
49
  private scanFileForVulnerabilities;
49
50
  }
50
51
  /**
@@ -24,7 +24,7 @@ const VULNERABILITY_PATTERNS = [
24
24
  // SQL Injection
25
25
  {
26
26
  type: 'sql_injection',
27
- regex: /(?:execute|query|raw|exec)\s*\(\s*[`'"].*\$\{.+\}|`\s*\+\s*\w+|\$\{.+\}.*(?:SELECT|INSERT|UPDATE|DELETE|DROP)/gi,
27
+ regex: /(?:execute|query|raw|exec)\s*\(\s*`[^`]*(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|WITH)[^`]*\$\{[^}]+\}[^`]*`/gi,
28
28
  severity: 'critical',
29
29
  description: 'Potential SQL injection: User input concatenated into SQL query',
30
30
  cwe: 'CWE-89',
@@ -32,7 +32,7 @@ const VULNERABILITY_PATTERNS = [
32
32
  },
33
33
  {
34
34
  type: 'sql_injection',
35
- regex: /\.query\s*\(\s*['"`].*\+.*\+.*['"`]\s*\)/g,
35
+ regex: /(?:execute|query|raw|exec)\s*\(\s*['"`][^'"`]*(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|WITH)[^'"`]*['"`]\s*\+\s*[^)]+\)/gi,
36
36
  severity: 'critical',
37
37
  description: 'SQL query built with string concatenation',
38
38
  cwe: 'CWE-89',
@@ -117,7 +117,7 @@ const VULNERABILITY_PATTERNS = [
117
117
  // Command Injection
118
118
  {
119
119
  type: 'command_injection',
120
- regex: /(?:exec|spawn|execSync|spawnSync)\s*\([^)]*(?:req\.|`.*\$\{)/g,
120
+ regex: /(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{[^}]*(?:req\.|query|params|body|input|user|argv|process\.env)[^}]*\}[^`]*`|[^)]*(?:req\.|query|params|body|input|user|argv|process\.env)[^)]*)\)/g,
121
121
  severity: 'critical',
122
122
  description: 'Potential command injection: shell execution with user input',
123
123
  cwe: 'CWE-78',
@@ -125,7 +125,7 @@ const VULNERABILITY_PATTERNS = [
125
125
  },
126
126
  {
127
127
  type: 'command_injection',
128
- regex: /child_process.*\s*\.\s*(?:exec|spawn)\s*\(/g,
128
+ regex: /child_process.*\s*\.\s*(?:exec|spawn)\s*\([^)]*(?:req\.|query|params|body|input|user|argv|process\.env)/g,
129
129
  severity: 'high',
130
130
  description: 'child_process usage detected (verify input sanitization)',
131
131
  cwe: 'CWE-78',
@@ -254,9 +254,11 @@ export class SecurityPatternsGate extends Gate {
254
254
  const files = await FileScanner.findFiles({
255
255
  cwd: context.cwd,
256
256
  patterns: ['**/*.{ts,js,tsx,jsx,py,java,go}'],
257
+ ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/coverage/**'],
257
258
  });
258
- Logger.info(`Security Patterns Gate: Scanning ${files.length} files`);
259
- for (const file of files) {
259
+ const scanFiles = files.filter(file => !this.shouldSkipSecurityFile(file));
260
+ Logger.info(`Security Patterns Gate: Scanning ${scanFiles.length} files`);
261
+ for (const file of scanFiles) {
260
262
  try {
261
263
  const fullPath = path.join(context.cwd, file);
262
264
  const content = await fs.readFile(fullPath, 'utf-8');
@@ -296,6 +298,20 @@ export class SecurityPatternsGate extends Gate {
296
298
  }
297
299
  return failures;
298
300
  }
301
+ shouldSkipSecurityFile(file) {
302
+ const normalized = file.replace(/\\/g, '/');
303
+ if (/\/(?:examples|studio-dist|dist|build|coverage|target|out)\//.test(`/${normalized}`))
304
+ return true;
305
+ if (/\/__tests__\//.test(`/${normalized}`))
306
+ return true;
307
+ if (/\/commands\/demo(?:-|\/)/.test(`/${normalized}`))
308
+ return true;
309
+ if (/\/gates\/deprecated-apis-rules(?:-node|-lang)?\.ts$/i.test(normalized))
310
+ return true;
311
+ if (/\.(test|spec)\.(?:ts|tsx|js|jsx|py|java|go)$/i.test(normalized))
312
+ return true;
313
+ return false;
314
+ }
299
315
  scanFileForVulnerabilities(content, file, ext, vulnerabilities) {
300
316
  const lines = content.split('\n');
301
317
  for (const pattern of VULNERABILITY_PATTERNS) {
@@ -41,6 +41,14 @@ describe('SecurityPatternsGate', () => {
41
41
  const vulns = await checkSecurityPatterns(filePath);
42
42
  expect(vulns.some(v => v.type === 'sql_injection')).toBe(true);
43
43
  });
44
+ it('should NOT flag non-SQL template interpolation in normal function calls', async () => {
45
+ const filePath = path.join(testDir, 'status.ts');
46
+ fs.writeFileSync(filePath, `
47
+ logger.info(\`Current version: \${current} -> \${latest}\`);
48
+ `);
49
+ const vulns = await checkSecurityPatterns(filePath);
50
+ expect(vulns.some(v => v.type === 'sql_injection')).toBe(false);
51
+ });
44
52
  });
45
53
  describe('XSS detection', () => {
46
54
  it('should detect innerHTML assignment', async () => {
@@ -129,5 +137,15 @@ describe('SecurityPatternsGate', () => {
129
137
  const failures = await gate.run({ cwd: testDir });
130
138
  expect(failures).toHaveLength(0); // High severity not blocked
131
139
  });
140
+ it('should skip fixture-style test files for gate-level scans', async () => {
141
+ const gate = new SecurityPatternsGate({ enabled: true });
142
+ const fixturePath = path.join(testDir, 'auth.test.ts');
143
+ fs.writeFileSync(fixturePath, `
144
+ const password = "supersecretpassword123";
145
+ eval(req.body.code);
146
+ `);
147
+ const failures = await gate.run({ cwd: testDir });
148
+ expect(failures).toHaveLength(0);
149
+ });
132
150
  });
133
151
  });
@@ -19,4 +19,4 @@ export interface GeneratedHookFile {
19
19
  /**
20
20
  * Generate hook config files for a specific tool.
21
21
  */
22
- export declare function generateHookFiles(tool: HookTool, checkerPath: string): GeneratedHookFile[];
22
+ export declare function generateHookFiles(tool: HookTool, checkerCommand: string): GeneratedHookFile[];
@@ -12,21 +12,21 @@
12
12
  /**
13
13
  * Generate hook config files for a specific tool.
14
14
  */
15
- export function generateHookFiles(tool, checkerPath) {
15
+ export function generateHookFiles(tool, checkerCommand) {
16
16
  switch (tool) {
17
17
  case 'claude':
18
- return generateClaudeHooks(checkerPath);
18
+ return generateClaudeHooks(checkerCommand);
19
19
  case 'cursor':
20
- return generateCursorHooks(checkerPath);
20
+ return generateCursorHooks(checkerCommand);
21
21
  case 'cline':
22
- return generateClineHooks(checkerPath);
22
+ return generateClineHooks(checkerCommand);
23
23
  case 'windsurf':
24
- return generateWindsurfHooks(checkerPath);
24
+ return generateWindsurfHooks(checkerCommand);
25
25
  default:
26
26
  return [];
27
27
  }
28
28
  }
29
- function generateClaudeHooks(checkerPath) {
29
+ function generateClaudeHooks(checkerCommand) {
30
30
  const settings = {
31
31
  hooks: {
32
32
  PostToolUse: [
@@ -35,7 +35,7 @@ function generateClaudeHooks(checkerPath) {
35
35
  hooks: [
36
36
  {
37
37
  type: "command",
38
- command: `node ${checkerPath} --files "$TOOL_INPUT_file_path"`,
38
+ command: `${checkerCommand} --files "$TOOL_INPUT_file_path"`,
39
39
  }
40
40
  ]
41
41
  }
@@ -50,13 +50,13 @@ function generateClaudeHooks(checkerPath) {
50
50
  },
51
51
  ];
52
52
  }
53
- function generateCursorHooks(checkerPath) {
53
+ function generateCursorHooks(checkerCommand) {
54
54
  const hooks = {
55
55
  version: 1,
56
56
  hooks: {
57
57
  afterFileEdit: [
58
58
  {
59
- command: `node ${checkerPath} --stdin`,
59
+ command: `${checkerCommand} --stdin`,
60
60
  }
61
61
  ]
62
62
  }
@@ -109,7 +109,7 @@ process.stdin.on('end', async () => {
109
109
  },
110
110
  ];
111
111
  }
112
- function generateClineHooks(checkerPath) {
112
+ function generateClineHooks(checkerCommand) {
113
113
  const script = `#!/usr/bin/env node
114
114
  /**
115
115
  * Cline PostToolUse hook for Rigour.
@@ -174,13 +174,13 @@ process.stdin.on('end', async () => {
174
174
  },
175
175
  ];
176
176
  }
177
- function generateWindsurfHooks(checkerPath) {
177
+ function generateWindsurfHooks(checkerCommand) {
178
178
  const hooks = {
179
179
  version: 1,
180
180
  hooks: {
181
181
  post_write_code: [
182
182
  {
183
- command: `node ${checkerPath} --stdin`,
183
+ command: `${checkerCommand} --stdin`,
184
184
  }
185
185
  ]
186
186
  }
@@ -0,0 +1,6 @@
1
+ export interface ExecutableCheckResult {
2
+ ok: boolean;
3
+ fixed: boolean;
4
+ }
5
+ export declare function isExecutableBinary(binaryPath: string, platform?: NodeJS.Platform): boolean;
6
+ export declare function ensureExecutableBinary(binaryPath: string, platform?: NodeJS.Platform): ExecutableCheckResult;
@@ -0,0 +1,29 @@
1
+ import fs from 'fs';
2
+ export function isExecutableBinary(binaryPath, platform = process.platform) {
3
+ if (platform === 'win32') {
4
+ return fs.existsSync(binaryPath);
5
+ }
6
+ try {
7
+ fs.accessSync(binaryPath, fs.constants.X_OK);
8
+ return true;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ export function ensureExecutableBinary(binaryPath, platform = process.platform) {
15
+ if (platform === 'win32') {
16
+ return { ok: fs.existsSync(binaryPath), fixed: false };
17
+ }
18
+ if (isExecutableBinary(binaryPath, platform)) {
19
+ return { ok: true, fixed: false };
20
+ }
21
+ try {
22
+ fs.chmodSync(binaryPath, 0o755);
23
+ }
24
+ catch {
25
+ return { ok: false, fixed: false };
26
+ }
27
+ const ok = isExecutableBinary(binaryPath, platform);
28
+ return { ok, fixed: ok };
29
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
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 { ensureExecutableBinary, isExecutableBinary } from './executable.js';
6
+ describe('executable helpers', () => {
7
+ let testDir;
8
+ beforeEach(() => {
9
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rigour-exec-test-'));
10
+ });
11
+ afterEach(() => {
12
+ fs.rmSync(testDir, { recursive: true, force: true });
13
+ });
14
+ it('detects executable files', () => {
15
+ const binaryPath = path.join(testDir, 'rigour-brain');
16
+ fs.writeFileSync(binaryPath, '#!/usr/bin/env sh\necho ok\n', 'utf-8');
17
+ fs.chmodSync(binaryPath, 0o755);
18
+ expect(isExecutableBinary(binaryPath)).toBe(true);
19
+ });
20
+ it('repairs executable bit when missing', () => {
21
+ const binaryPath = path.join(testDir, 'rigour-brain');
22
+ fs.writeFileSync(binaryPath, '#!/usr/bin/env sh\necho ok\n', 'utf-8');
23
+ fs.chmodSync(binaryPath, 0o644);
24
+ const result = ensureExecutableBinary(binaryPath);
25
+ if (process.platform === 'win32') {
26
+ // Windows does not use POSIX executable bits.
27
+ expect(result.ok).toBe(true);
28
+ expect(result.fixed).toBe(false);
29
+ expect(isExecutableBinary(binaryPath)).toBe(true);
30
+ return;
31
+ }
32
+ // Some CI/filesystem setups can deny chmod transitions; in that case
33
+ // the helper should fail gracefully without throwing.
34
+ if (!result.ok) {
35
+ expect(result.fixed).toBe(false);
36
+ return;
37
+ }
38
+ expect(result.fixed).toBe(true);
39
+ expect(isExecutableBinary(binaryPath)).toBe(true);
40
+ });
41
+ });
@@ -1,8 +1,10 @@
1
1
  import { type ModelTier, type ModelInfo } from './types.js';
2
+ export declare function extractSha256FromEtag(etag: string | null): string | null;
3
+ export declare function hashFileSha256(filePath: string): Promise<string>;
2
4
  /**
3
5
  * Check if a model is already downloaded and valid.
4
6
  */
5
- export declare function isModelCached(tier: ModelTier): boolean;
7
+ export declare function isModelCached(tier: ModelTier): Promise<boolean>;
6
8
  /**
7
9
  * Get the path to a cached model.
8
10
  */