@rigour-labs/core 3.0.0 → 3.0.1

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Tests for OWASP-aligned security patterns added in v3.0.0.
3
+ * Covers: ReDoS, overly permissive code, unsafe output, missing input validation.
4
+ */
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { SecurityPatternsGate, checkSecurityPatterns } from './security-patterns.js';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import * as os from 'os';
10
+ describe('SecurityPatternsGate — OWASP extended patterns', () => {
11
+ let testDir;
12
+ beforeEach(() => {
13
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'owasp-test-'));
14
+ });
15
+ afterEach(() => {
16
+ fs.rmSync(testDir, { recursive: true, force: true });
17
+ });
18
+ describe('ReDoS detection (OWASP #7)', () => {
19
+ it('should detect dynamic regex from user input', async () => {
20
+ const filePath = path.join(testDir, 'search.ts');
21
+ fs.writeFileSync(filePath, `
22
+ const pattern = new RegExp(req.query.search);
23
+ const matches = text.match(pattern);
24
+ `);
25
+ const vulns = await checkSecurityPatterns(filePath);
26
+ expect(vulns.some(v => v.type === 'redos')).toBe(true);
27
+ });
28
+ it('should detect nested quantifiers', async () => {
29
+ const filePath = path.join(testDir, 'regex.ts');
30
+ fs.writeFileSync(filePath, `
31
+ const re = /(?:a+)+b/;
32
+ `);
33
+ const vulns = await checkSecurityPatterns(filePath);
34
+ expect(vulns.some(v => v.type === 'redos')).toBe(true);
35
+ });
36
+ it('should allow safe regex patterns', async () => {
37
+ const filePath = path.join(testDir, 'safe-regex.ts');
38
+ fs.writeFileSync(filePath, `
39
+ const re = /^[a-z]+$/;
40
+ `);
41
+ const vulns = await checkSecurityPatterns(filePath);
42
+ expect(vulns.filter(v => v.type === 'redos')).toHaveLength(0);
43
+ });
44
+ });
45
+ describe('Overly Permissive Code (OWASP #9)', () => {
46
+ it('should detect CORS wildcard origin', async () => {
47
+ const filePath = path.join(testDir, 'server.ts');
48
+ fs.writeFileSync(filePath, `
49
+ import cors from 'cors';
50
+ app.use(cors({ origin: '*' }));
51
+ `);
52
+ const vulns = await checkSecurityPatterns(filePath);
53
+ expect(vulns.some(v => v.type === 'overly_permissive')).toBe(true);
54
+ });
55
+ it('should detect CORS origin true', async () => {
56
+ const filePath = path.join(testDir, 'server2.ts');
57
+ fs.writeFileSync(filePath, `
58
+ app.use(cors({ origin: true }));
59
+ `);
60
+ const vulns = await checkSecurityPatterns(filePath);
61
+ expect(vulns.some(v => v.type === 'overly_permissive')).toBe(true);
62
+ });
63
+ it('should detect 0.0.0.0 binding', async () => {
64
+ const filePath = path.join(testDir, 'listen.ts');
65
+ fs.writeFileSync(filePath, `
66
+ app.listen(3000, '0.0.0.0');
67
+ `);
68
+ const vulns = await checkSecurityPatterns(filePath);
69
+ expect(vulns.some(v => v.type === 'overly_permissive')).toBe(true);
70
+ });
71
+ it('should detect chmod 777', async () => {
72
+ const filePath = path.join(testDir, 'perms.ts');
73
+ fs.writeFileSync(filePath, `
74
+ fs.chmod('/tmp/data', 0o777);
75
+ `);
76
+ const vulns = await checkSecurityPatterns(filePath);
77
+ expect(vulns.some(v => v.type === 'overly_permissive')).toBe(true);
78
+ });
79
+ it('should detect wildcard CORS header', async () => {
80
+ const filePath = path.join(testDir, 'headers.ts');
81
+ fs.writeFileSync(filePath, `
82
+ res.setHeader('Access-Control-Allow-Origin', '*');
83
+ `);
84
+ const vulns = await checkSecurityPatterns(filePath);
85
+ expect(vulns.some(v => v.type === 'overly_permissive')).toBe(true);
86
+ });
87
+ it('should allow specific CORS origin', async () => {
88
+ const filePath = path.join(testDir, 'safe-cors.ts');
89
+ fs.writeFileSync(filePath, `
90
+ app.use(cors({ origin: 'https://myapp.com' }));
91
+ `);
92
+ const vulns = await checkSecurityPatterns(filePath);
93
+ expect(vulns.filter(v => v.type === 'overly_permissive')).toHaveLength(0);
94
+ });
95
+ });
96
+ describe('Unsafe Output Handling (OWASP #6)', () => {
97
+ it('should detect response reflecting user input', async () => {
98
+ const filePath = path.join(testDir, 'handler.ts');
99
+ fs.writeFileSync(filePath, `
100
+ app.get('/echo', (req, res) => {
101
+ res.send(req.query.msg);
102
+ });
103
+ `);
104
+ const vulns = await checkSecurityPatterns(filePath);
105
+ expect(vulns.some(v => v.type === 'unsafe_output')).toBe(true);
106
+ });
107
+ it('should detect eval with user input', async () => {
108
+ const filePath = path.join(testDir, 'eval.ts');
109
+ fs.writeFileSync(filePath, `
110
+ eval(req.body.code);
111
+ `);
112
+ const vulns = await checkSecurityPatterns(filePath);
113
+ expect(vulns.some(v => v.type === 'unsafe_output')).toBe(true);
114
+ });
115
+ it('should allow safe response patterns', async () => {
116
+ const filePath = path.join(testDir, 'safe-res.ts');
117
+ fs.writeFileSync(filePath, `
118
+ res.json({ status: 'ok', data: processedData });
119
+ `);
120
+ const vulns = await checkSecurityPatterns(filePath);
121
+ expect(vulns.filter(v => v.type === 'unsafe_output')).toHaveLength(0);
122
+ });
123
+ });
124
+ describe('Missing Input Validation (OWASP #8)', () => {
125
+ it('should detect JSON.parse on raw body', async () => {
126
+ const filePath = path.join(testDir, 'parse.ts');
127
+ fs.writeFileSync(filePath, `
128
+ const data = JSON.parse(req.body);
129
+ `);
130
+ const vulns = await checkSecurityPatterns(filePath);
131
+ expect(vulns.some(v => v.type === 'missing_input_validation')).toBe(true);
132
+ });
133
+ it('should detect "as any" type assertion', async () => {
134
+ const filePath = path.join(testDir, 'assert.ts');
135
+ fs.writeFileSync(filePath, `
136
+ const user = payload as any;
137
+ `);
138
+ const vulns = await checkSecurityPatterns(filePath);
139
+ expect(vulns.some(v => v.type === 'missing_input_validation')).toBe(true);
140
+ });
141
+ it('should allow validated JSON parse', async () => {
142
+ const filePath = path.join(testDir, 'safe-parse.ts');
143
+ fs.writeFileSync(filePath, `
144
+ const data = JSON.parse(rawString);
145
+ const validated = schema.parse(data);
146
+ `);
147
+ const vulns = await checkSecurityPatterns(filePath);
148
+ expect(vulns.filter(v => v.type === 'missing_input_validation')).toHaveLength(0);
149
+ });
150
+ });
151
+ describe('config toggles for new patterns', () => {
152
+ it('should disable redos when configured', async () => {
153
+ const gate = new SecurityPatternsGate({ enabled: true, redos: false });
154
+ const filePath = path.join(testDir, 'regex.ts');
155
+ fs.writeFileSync(filePath, `
156
+ const pattern = new RegExp(req.query.search);
157
+ `);
158
+ const failures = await gate.run({ cwd: testDir });
159
+ expect(failures.filter(f => f.title?.includes('ReDoS') || f.title?.includes('regex'))).toHaveLength(0);
160
+ });
161
+ it('should disable overly_permissive when configured', async () => {
162
+ const gate = new SecurityPatternsGate({ enabled: true, overly_permissive: false });
163
+ const filePath = path.join(testDir, 'cors.ts');
164
+ fs.writeFileSync(filePath, `
165
+ app.use(cors({ origin: '*' }));
166
+ `);
167
+ const failures = await gate.run({ cwd: testDir });
168
+ expect(failures.filter(f => f.title?.includes('CORS') || f.title?.includes('permissive'))).toHaveLength(0);
169
+ });
170
+ });
171
+ });
@@ -33,6 +33,10 @@ export interface SecurityPatternsConfig {
33
33
  hardcoded_secrets?: boolean;
34
34
  insecure_randomness?: boolean;
35
35
  command_injection?: boolean;
36
+ redos?: boolean;
37
+ overly_permissive?: boolean;
38
+ unsafe_output?: boolean;
39
+ missing_input_validation?: boolean;
36
40
  block_on_severity?: 'critical' | 'high' | 'medium' | 'low';
37
41
  }
38
42
  export declare class SecurityPatternsGate extends Gate {
@@ -131,6 +131,98 @@ const VULNERABILITY_PATTERNS = [
131
131
  cwe: 'CWE-78',
132
132
  languages: ['ts', 'js']
133
133
  },
134
+ // ReDoS — Denial of Service via regex (OWASP #7)
135
+ {
136
+ type: 'redos',
137
+ regex: /new RegExp\s*\([^)]*(?:req\.|params|query|body|input|user)/g,
138
+ severity: 'high',
139
+ description: 'Dynamic regex from user input — potential ReDoS',
140
+ cwe: 'CWE-1333',
141
+ languages: ['ts', 'js']
142
+ },
143
+ {
144
+ type: 'redos',
145
+ regex: /\(\?:[^)]*\+[^)]*\)\+|\([^)]*\*[^)]*\)\+|\(\.\*\)\{/g,
146
+ severity: 'medium',
147
+ description: 'Regex with nested quantifiers — potential ReDoS',
148
+ cwe: 'CWE-1333',
149
+ languages: ['ts', 'js', 'py']
150
+ },
151
+ // Overly Permissive Code (OWASP #9)
152
+ {
153
+ type: 'overly_permissive',
154
+ regex: /cors\s*\(\s*\{[^}]*origin\s*:\s*(?:true|['"`]\*['"`])/g,
155
+ severity: 'high',
156
+ description: 'CORS wildcard origin — allows any domain',
157
+ cwe: 'CWE-942',
158
+ languages: ['ts', 'js']
159
+ },
160
+ {
161
+ type: 'overly_permissive',
162
+ regex: /(?:listen|bind)\s*\(\s*(?:\d+\s*,\s*)?['"`]0\.0\.0\.0['"`]/g,
163
+ severity: 'medium',
164
+ description: 'Binding to 0.0.0.0 exposes service to all interfaces',
165
+ cwe: 'CWE-668',
166
+ languages: ['ts', 'js', 'py', 'go']
167
+ },
168
+ {
169
+ type: 'overly_permissive',
170
+ regex: /chmod\s*\(\s*[^,]*,\s*['"`]?(?:0o?)?777['"`]?\s*\)/g,
171
+ severity: 'high',
172
+ description: 'chmod 777 — world-readable/writable permissions',
173
+ cwe: 'CWE-732',
174
+ languages: ['ts', 'js', 'py']
175
+ },
176
+ {
177
+ type: 'overly_permissive',
178
+ regex: /(?:Access-Control-Allow-Origin|x-powered-by)['"`,\s:]+\*/gi,
179
+ severity: 'high',
180
+ description: 'Wildcard Access-Control-Allow-Origin header',
181
+ cwe: 'CWE-942',
182
+ languages: ['ts', 'js', 'py']
183
+ },
184
+ // Unsafe Output Handling (OWASP #6)
185
+ {
186
+ type: 'unsafe_output',
187
+ regex: /res\.(?:send|write|end)\s*\(\s*(?:req\.|params|query|body|input|user)/g,
188
+ severity: 'high',
189
+ description: 'Reflecting user input in response without sanitization',
190
+ cwe: 'CWE-79',
191
+ languages: ['ts', 'js']
192
+ },
193
+ {
194
+ type: 'unsafe_output',
195
+ regex: /\$\{[^}]*(?:req\.|params|query|body|input|user)[^}]*\}.*(?:html|template|render)/gi,
196
+ severity: 'high',
197
+ description: 'User input interpolated into template/HTML output',
198
+ cwe: 'CWE-79',
199
+ languages: ['ts', 'js', 'py']
200
+ },
201
+ {
202
+ type: 'unsafe_output',
203
+ regex: /eval\s*\(\s*(?:req\.|params|query|body|input|user)/g,
204
+ severity: 'critical',
205
+ description: 'eval() with user input — code injection',
206
+ cwe: 'CWE-94',
207
+ languages: ['ts', 'js', 'py']
208
+ },
209
+ // Missing Input Validation (OWASP #8)
210
+ {
211
+ type: 'missing_input_validation',
212
+ regex: /JSON\.parse\s*\(\s*(?:req\.body|request\.body|body|data|input)\s*\)/g,
213
+ severity: 'medium',
214
+ description: 'JSON.parse on raw input without schema validation',
215
+ cwe: 'CWE-20',
216
+ languages: ['ts', 'js']
217
+ },
218
+ {
219
+ type: 'missing_input_validation',
220
+ regex: /(?:as\s+any|:\s*any)\s*(?:[;,)\]}])/g,
221
+ severity: 'medium',
222
+ description: 'Type assertion to "any" bypasses type safety',
223
+ cwe: 'CWE-20',
224
+ languages: ['ts']
225
+ },
134
226
  ];
135
227
  export class SecurityPatternsGate extends Gate {
136
228
  config;
@@ -145,6 +237,10 @@ export class SecurityPatternsGate extends Gate {
145
237
  hardcoded_secrets: config.hardcoded_secrets ?? true,
146
238
  insecure_randomness: config.insecure_randomness ?? true,
147
239
  command_injection: config.command_injection ?? true,
240
+ redos: config.redos ?? true,
241
+ overly_permissive: config.overly_permissive ?? true,
242
+ unsafe_output: config.unsafe_output ?? true,
243
+ missing_input_validation: config.missing_input_validation ?? true,
148
244
  block_on_severity: config.block_on_severity ?? 'high',
149
245
  };
150
246
  }
@@ -178,6 +274,10 @@ export class SecurityPatternsGate extends Gate {
178
274
  case 'hardcoded_secrets': return this.config.hardcoded_secrets;
179
275
  case 'insecure_randomness': return this.config.insecure_randomness;
180
276
  case 'command_injection': return this.config.command_injection;
277
+ case 'redos': return this.config.redos;
278
+ case 'overly_permissive': return this.config.overly_permissive;
279
+ case 'unsafe_output': return this.config.unsafe_output;
280
+ case 'missing_input_validation': return this.config.missing_input_validation;
181
281
  default: return true;
182
282
  }
183
283
  });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Lightweight per-file checker for hook integration.
3
+ *
4
+ * Runs a fast subset of Rigour gates on individual files,
5
+ * designed to complete in <200ms for real-time hook feedback.
6
+ *
7
+ * Used by all tool-specific hooks (Claude, Cursor, Cline, Windsurf).
8
+ *
9
+ * @since v3.0.0
10
+ */
11
+ import type { HookCheckerResult } from './types.js';
12
+ interface CheckerOptions {
13
+ cwd: string;
14
+ files: string[];
15
+ timeout_ms?: number;
16
+ block_on_failure?: boolean;
17
+ }
18
+ /**
19
+ * Run fast gates on a set of files.
20
+ * Returns structured JSON for hook consumers.
21
+ */
22
+ export declare function runHookChecker(options: CheckerOptions): Promise<HookCheckerResult>;
23
+ export {};
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Lightweight per-file checker for hook integration.
3
+ *
4
+ * Runs a fast subset of Rigour gates on individual files,
5
+ * designed to complete in <200ms for real-time hook feedback.
6
+ *
7
+ * Used by all tool-specific hooks (Claude, Cursor, Cline, Windsurf).
8
+ *
9
+ * @since v3.0.0
10
+ */
11
+ import fs from 'fs-extra';
12
+ import path from 'path';
13
+ import yaml from 'yaml';
14
+ import { ConfigSchema } from '../types/index.js';
15
+ const JS_TS_PATTERN = /\.(ts|tsx|js|jsx|mts|mjs)$/;
16
+ /**
17
+ * Load rigour config from cwd, falling back to defaults.
18
+ */
19
+ async function loadConfig(cwd) {
20
+ const configPath = path.join(cwd, 'rigour.yml');
21
+ if (await fs.pathExists(configPath)) {
22
+ const raw = yaml.parse(await fs.readFile(configPath, 'utf-8'));
23
+ return ConfigSchema.parse(raw);
24
+ }
25
+ return ConfigSchema.parse({ version: 1 });
26
+ }
27
+ /**
28
+ * Resolve a file path to absolute, read its content, and return metadata.
29
+ */
30
+ async function resolveFile(filePath, cwd) {
31
+ const absPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
32
+ if (!(await fs.pathExists(absPath))) {
33
+ return null;
34
+ }
35
+ const content = await fs.readFile(absPath, 'utf-8');
36
+ const relPath = path.relative(cwd, absPath);
37
+ return { absPath, relPath, content };
38
+ }
39
+ /**
40
+ * Run all fast gates on a single file's content.
41
+ */
42
+ function checkFile(content, relPath, cwd, config) {
43
+ const failures = [];
44
+ const lines = content.split('\n');
45
+ // Gate 1: File size
46
+ const maxLines = config.gates.max_file_lines ?? 500;
47
+ if (lines.length > maxLines) {
48
+ failures.push({
49
+ gate: 'file-size',
50
+ file: relPath,
51
+ message: `File has ${lines.length} lines (max: ${maxLines})`,
52
+ severity: 'medium',
53
+ });
54
+ }
55
+ const isJsTs = JS_TS_PATTERN.test(relPath);
56
+ // Gate 2: Hallucinated imports (JS/TS only)
57
+ if (isJsTs) {
58
+ checkHallucinatedImports(content, relPath, cwd, failures);
59
+ }
60
+ // Gate 3: Promise safety (JS/TS only)
61
+ if (isJsTs) {
62
+ checkPromiseSafety(lines, relPath, failures);
63
+ }
64
+ // Gate 4: Security patterns (all languages)
65
+ checkSecurityPatterns(lines, relPath, failures);
66
+ return failures;
67
+ }
68
+ /**
69
+ * Run fast gates on a set of files.
70
+ * Returns structured JSON for hook consumers.
71
+ */
72
+ export async function runHookChecker(options) {
73
+ const start = Date.now();
74
+ const { cwd, files, timeout_ms = 5000 } = options;
75
+ const failures = [];
76
+ try {
77
+ const config = await loadConfig(cwd);
78
+ const deadline = start + timeout_ms;
79
+ for (const filePath of files) {
80
+ if (Date.now() > deadline) {
81
+ break;
82
+ }
83
+ const resolved = await resolveFile(filePath, cwd);
84
+ if (!resolved) {
85
+ continue;
86
+ }
87
+ const fileFailures = checkFile(resolved.content, resolved.relPath, cwd, config);
88
+ failures.push(...fileFailures);
89
+ }
90
+ return {
91
+ status: failures.length > 0 ? 'fail' : 'pass',
92
+ failures,
93
+ duration_ms: Date.now() - start,
94
+ };
95
+ }
96
+ catch (error) {
97
+ const msg = error instanceof Error ? error.message : String(error);
98
+ return {
99
+ status: 'error',
100
+ failures: [{
101
+ gate: 'hook-checker',
102
+ file: '',
103
+ message: `Hook checker error: ${msg}`,
104
+ severity: 'medium',
105
+ }],
106
+ duration_ms: Date.now() - start,
107
+ };
108
+ }
109
+ }
110
+ /**
111
+ * Check for imports of non-existent relative files.
112
+ */
113
+ function checkHallucinatedImports(content, relPath, cwd, failures) {
114
+ const importRegex = /(?:import\s+.*\s+from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))/g;
115
+ let match;
116
+ while ((match = importRegex.exec(content)) !== null) {
117
+ const specifier = match[1] || match[2];
118
+ if (!specifier || !specifier.startsWith('.')) {
119
+ continue;
120
+ }
121
+ const dir = path.dirname(path.join(cwd, relPath));
122
+ const resolved = path.resolve(dir, specifier);
123
+ const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs', '/index.ts', '/index.js'];
124
+ const exists = extensions.some(ext => {
125
+ try {
126
+ return fs.existsSync(resolved + ext);
127
+ }
128
+ catch {
129
+ return false;
130
+ }
131
+ });
132
+ if (!exists) {
133
+ const lineNum = content.substring(0, match.index).split('\n').length;
134
+ failures.push({
135
+ gate: 'hallucinated-imports',
136
+ file: relPath,
137
+ message: `Import '${specifier}' does not resolve to an existing file`,
138
+ severity: 'high',
139
+ line: lineNum,
140
+ });
141
+ }
142
+ }
143
+ }
144
+ /**
145
+ * Check for common async/promise safety issues.
146
+ */
147
+ function checkPromiseSafety(lines, relPath, failures) {
148
+ for (let i = 0; i < lines.length; i++) {
149
+ const line = lines[i];
150
+ checkUnsafeJsonParse(line, lines, i, relPath, failures);
151
+ checkUnhandledFetch(line, lines, i, relPath, failures);
152
+ }
153
+ }
154
+ function checkUnsafeJsonParse(line, lines, i, relPath, failures) {
155
+ if (!/JSON\.parse\s*\(/.test(line)) {
156
+ return;
157
+ }
158
+ const contextStart = Math.max(0, i - 5);
159
+ const context = lines.slice(contextStart, i + 1).join('\n');
160
+ if (!/try\s*\{/.test(context)) {
161
+ failures.push({
162
+ gate: 'promise-safety',
163
+ file: relPath,
164
+ message: 'JSON.parse() without try/catch — crashes on malformed input',
165
+ severity: 'medium',
166
+ line: i + 1,
167
+ });
168
+ }
169
+ }
170
+ function checkUnhandledFetch(line, lines, i, relPath, failures) {
171
+ if (!/\bfetch\s*\(/.test(line) || /\.catch\b/.test(line) || /await/.test(line)) {
172
+ return;
173
+ }
174
+ const contextEnd = Math.min(lines.length, i + 3);
175
+ const afterContext = lines.slice(i, contextEnd).join('\n');
176
+ const beforeContext = lines.slice(Math.max(0, i - 5), i + 1).join('\n');
177
+ if (!/\.catch\b/.test(afterContext) && !/try\s*\{/.test(beforeContext)) {
178
+ failures.push({
179
+ gate: 'promise-safety',
180
+ file: relPath,
181
+ message: 'fetch() without error handling',
182
+ severity: 'medium',
183
+ line: i + 1,
184
+ });
185
+ }
186
+ }
187
+ /**
188
+ * Check for critical security patterns.
189
+ */
190
+ function checkSecurityPatterns(lines, relPath, failures) {
191
+ const isTestFile = /\.(test|spec|example|mock)\./i.test(relPath);
192
+ for (let i = 0; i < lines.length; i++) {
193
+ const line = lines[i];
194
+ checkHardcodedSecrets(line, i, relPath, isTestFile, failures);
195
+ checkCommandInjection(line, i, relPath, failures);
196
+ }
197
+ }
198
+ function checkHardcodedSecrets(line, i, relPath, isTestFile, failures) {
199
+ if (isTestFile) {
200
+ return;
201
+ }
202
+ if (/(?:api[_-]?key|secret|password|token)\s*[:=]\s*['"][A-Za-z0-9+/=]{20,}['"]/i.test(line)) {
203
+ failures.push({
204
+ gate: 'security-patterns',
205
+ file: relPath,
206
+ message: 'Possible hardcoded secret or API key',
207
+ severity: 'critical',
208
+ line: i + 1,
209
+ });
210
+ }
211
+ }
212
+ function checkCommandInjection(line, i, relPath, failures) {
213
+ if (/(?:exec|spawn|execSync|spawnSync)\s*\(.*\$\{/.test(line)) {
214
+ failures.push({
215
+ gate: 'security-patterns',
216
+ file: relPath,
217
+ message: 'Potential command injection: user input in shell command',
218
+ severity: 'critical',
219
+ line: i + 1,
220
+ });
221
+ }
222
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Tests for the hooks fast-checker module.
3
+ * Verifies all 4 fast gates: file-size, hallucinated-imports, promise-safety, security-patterns.
4
+ */
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { runHookChecker } from './checker.js';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import * as os from 'os';
10
+ import yaml from 'yaml';
11
+ describe('runHookChecker', () => {
12
+ let testDir;
13
+ beforeEach(() => {
14
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hook-checker-test-'));
15
+ // Write minimal rigour.yml
16
+ fs.writeFileSync(path.join(testDir, 'rigour.yml'), yaml.stringify({
17
+ version: 1,
18
+ gates: { max_file_lines: 50 },
19
+ }));
20
+ // Write package.json for import resolution
21
+ fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({
22
+ name: 'test-proj',
23
+ dependencies: { express: '^4.0.0' },
24
+ }));
25
+ });
26
+ afterEach(() => {
27
+ fs.rmSync(testDir, { recursive: true, force: true });
28
+ });
29
+ it('should return pass for clean files', async () => {
30
+ const filePath = path.join(testDir, 'clean.ts');
31
+ fs.writeFileSync(filePath, 'export const x = 1;\n');
32
+ const result = await runHookChecker({ cwd: testDir, files: [filePath] });
33
+ expect(result.status).toBe('pass');
34
+ expect(result.failures).toHaveLength(0);
35
+ expect(result.duration_ms).toBeGreaterThanOrEqual(0);
36
+ });
37
+ it('should detect file size violations', async () => {
38
+ const filePath = path.join(testDir, 'big.ts');
39
+ const lines = Array.from({ length: 100 }, (_, i) => `export const v${i} = ${i};`);
40
+ fs.writeFileSync(filePath, lines.join('\n'));
41
+ const result = await runHookChecker({ cwd: testDir, files: [filePath] });
42
+ expect(result.status).toBe('fail');
43
+ expect(result.failures.some(f => f.gate === 'file-size')).toBe(true);
44
+ });
45
+ it('should detect hardcoded secrets', async () => {
46
+ const filePath = path.join(testDir, 'auth.ts');
47
+ fs.writeFileSync(filePath, `
48
+ const api_key = "abcdefghijklmnopqrstuvwxyz123456";
49
+ `);
50
+ const result = await runHookChecker({ cwd: testDir, files: [filePath] });
51
+ expect(result.status).toBe('fail');
52
+ expect(result.failures.some(f => f.gate === 'security-patterns')).toBe(true);
53
+ });
54
+ it('should detect command injection patterns', async () => {
55
+ const filePath = path.join(testDir, 'cmd.ts');
56
+ fs.writeFileSync(filePath, `
57
+ import { exec } from 'child_process';
58
+ exec(\`rm -rf \${userInput}\`);
59
+ `);
60
+ const result = await runHookChecker({ cwd: testDir, files: [filePath] });
61
+ expect(result.status).toBe('fail');
62
+ expect(result.failures.some(f => f.gate === 'security-patterns' && f.message.includes('command injection'))).toBe(true);
63
+ });
64
+ it('should detect JSON.parse without try/catch', async () => {
65
+ const filePath = path.join(testDir, 'parse.ts');
66
+ fs.writeFileSync(filePath, `
67
+ const data = JSON.parse(input);
68
+ console.log(data);
69
+ `);
70
+ const result = await runHookChecker({ cwd: testDir, files: [filePath] });
71
+ expect(result.status).toBe('fail');
72
+ expect(result.failures.some(f => f.gate === 'promise-safety')).toBe(true);
73
+ });
74
+ it('should skip non-existent files gracefully', async () => {
75
+ const result = await runHookChecker({
76
+ cwd: testDir,
77
+ files: ['/does/not/exist.ts'],
78
+ });
79
+ expect(result.status).toBe('pass');
80
+ expect(result.failures).toHaveLength(0);
81
+ });
82
+ it('should handle multiple files', async () => {
83
+ const cleanFile = path.join(testDir, 'clean.ts');
84
+ fs.writeFileSync(cleanFile, 'export const x = 1;\n');
85
+ const badFile = path.join(testDir, 'bad.ts');
86
+ fs.writeFileSync(badFile, `const password = "supersecretpassword123456";`);
87
+ const result = await runHookChecker({
88
+ cwd: testDir,
89
+ files: [cleanFile, badFile],
90
+ });
91
+ expect(result.status).toBe('fail');
92
+ expect(result.failures.length).toBeGreaterThan(0);
93
+ });
94
+ it('should handle missing config gracefully', async () => {
95
+ // Remove rigour.yml
96
+ fs.unlinkSync(path.join(testDir, 'rigour.yml'));
97
+ const filePath = path.join(testDir, 'test.ts');
98
+ fs.writeFileSync(filePath, 'export const x = 1;\n');
99
+ const result = await runHookChecker({ cwd: testDir, files: [filePath] });
100
+ expect(result.status).toBe('pass');
101
+ });
102
+ it('should complete within timeout', async () => {
103
+ const filePath = path.join(testDir, 'test.ts');
104
+ fs.writeFileSync(filePath, 'export const x = 1;\n');
105
+ const result = await runHookChecker({
106
+ cwd: testDir,
107
+ files: [filePath],
108
+ timeout_ms: 5000,
109
+ });
110
+ expect(result.duration_ms).toBeLessThan(5000);
111
+ });
112
+ it('should detect hallucinated relative imports', async () => {
113
+ const filePath = path.join(testDir, 'app.ts');
114
+ fs.writeFileSync(filePath, `
115
+ import { helper } from './nonexistent-module';
116
+ `);
117
+ const result = await runHookChecker({ cwd: testDir, files: [filePath] });
118
+ expect(result.status).toBe('fail');
119
+ expect(result.failures.some(f => f.gate === 'hallucinated-imports')).toBe(true);
120
+ });
121
+ it('should not flag existing relative imports', async () => {
122
+ const helperPath = path.join(testDir, 'helper.ts');
123
+ fs.writeFileSync(helperPath, 'export const help = true;\n');
124
+ const filePath = path.join(testDir, 'app.ts');
125
+ fs.writeFileSync(filePath, `
126
+ import { help } from './helper';
127
+ `);
128
+ const result = await runHookChecker({ cwd: testDir, files: [filePath] });
129
+ const importFailures = result.failures.filter(f => f.gate === 'hallucinated-imports');
130
+ expect(importFailures).toHaveLength(0);
131
+ });
132
+ });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Hooks module — multi-tool hook integration for Rigour.
3
+ *
4
+ * @since v3.0.0
5
+ */
6
+ export { runHookChecker } from './checker.js';
7
+ export { generateHookFiles } from './templates.js';
8
+ export type { HookTool, HookConfig, HookCheckerResult } from './types.js';
9
+ export { DEFAULT_HOOK_CONFIG, FAST_GATE_IDS } from './types.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Hooks module — multi-tool hook integration for Rigour.
3
+ *
4
+ * @since v3.0.0
5
+ */
6
+ export { runHookChecker } from './checker.js';
7
+ export { generateHookFiles } from './templates.js';
8
+ export { DEFAULT_HOOK_CONFIG, FAST_GATE_IDS } from './types.js';
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone hook checker entry point.
4
+ *
5
+ * Can be invoked two ways:
6
+ * 1. CLI args: node rigour-hook-checker.js --files path/to/file.ts
7
+ * 2. Stdin: echo '{"file_path":"x.ts"}' | node rigour-hook-checker.js --stdin
8
+ *
9
+ * Exit codes:
10
+ * 0 = pass (or warn-only mode)
11
+ * 2 = block (fail + block_on_failure enabled)
12
+ *
13
+ * @since v3.0.0
14
+ */
15
+ export {};
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone hook checker entry point.
4
+ *
5
+ * Can be invoked two ways:
6
+ * 1. CLI args: node rigour-hook-checker.js --files path/to/file.ts
7
+ * 2. Stdin: echo '{"file_path":"x.ts"}' | node rigour-hook-checker.js --stdin
8
+ *
9
+ * Exit codes:
10
+ * 0 = pass (or warn-only mode)
11
+ * 2 = block (fail + block_on_failure enabled)
12
+ *
13
+ * @since v3.0.0
14
+ */
15
+ import { runHookChecker } from './checker.js';
16
+ const EMPTY_RESULT = JSON.stringify({ status: 'pass', failures: [], duration_ms: 0 });
17
+ /**
18
+ * Read all of stdin as a string.
19
+ */
20
+ async function readStdin() {
21
+ const chunks = [];
22
+ for await (const chunk of process.stdin) {
23
+ chunks.push(chunk);
24
+ }
25
+ return Buffer.concat(chunks).toString('utf-8').trim();
26
+ }
27
+ /**
28
+ * Parse file paths from stdin JSON payload.
29
+ *
30
+ * Supports multiple formats from different tools:
31
+ * Cursor: { file_path, old_content, new_content }
32
+ * Windsurf: { file_path, content }
33
+ * Cline: { toolName, toolInput: { path } }
34
+ * Array: { files: ["a.ts", "b.ts"] }
35
+ */
36
+ function parseStdinFiles(input) {
37
+ if (!input) {
38
+ return [];
39
+ }
40
+ try {
41
+ const payload = JSON.parse(input);
42
+ // Array format
43
+ if (Array.isArray(payload.files)) {
44
+ return payload.files;
45
+ }
46
+ // Direct file_path (Cursor/Windsurf)
47
+ if (payload.file_path) {
48
+ return [payload.file_path];
49
+ }
50
+ // Cline format: { toolInput: { path } }
51
+ if (payload.toolInput?.path) {
52
+ return [payload.toolInput.path];
53
+ }
54
+ if (payload.toolInput?.file_path) {
55
+ return [payload.toolInput.file_path];
56
+ }
57
+ return [];
58
+ }
59
+ catch {
60
+ // Not JSON — treat each line as a file path
61
+ return input.split('\n').map(l => l.trim()).filter(Boolean);
62
+ }
63
+ }
64
+ /**
65
+ * Parse file paths from CLI --files argument.
66
+ */
67
+ function parseCliFiles(args) {
68
+ const filesIdx = args.indexOf('--files');
69
+ if (filesIdx === -1 || !args[filesIdx + 1]) {
70
+ return [];
71
+ }
72
+ return args[filesIdx + 1].split(',').map(f => f.trim()).filter(Boolean);
73
+ }
74
+ /**
75
+ * Log failures to stderr in human-readable format.
76
+ */
77
+ function logFailures(failures) {
78
+ for (const f of failures) {
79
+ const loc = f.line ? `:${f.line}` : '';
80
+ process.stderr.write(`[rigour/${f.gate}] ${f.file}${loc}: ${f.message}\n`);
81
+ }
82
+ }
83
+ async function main() {
84
+ const args = process.argv.slice(2);
85
+ const cwd = process.cwd();
86
+ const files = args.includes('--stdin')
87
+ ? parseStdinFiles(await readStdin())
88
+ : parseCliFiles(args);
89
+ if (files.length === 0) {
90
+ process.stdout.write(EMPTY_RESULT);
91
+ return;
92
+ }
93
+ const result = await runHookChecker({ cwd, files });
94
+ process.stdout.write(JSON.stringify(result));
95
+ if (result.status === 'fail') {
96
+ logFailures(result.failures);
97
+ }
98
+ // Exit 2 = block signal for tools that respect it
99
+ if (result.status === 'fail' && args.includes('--block')) {
100
+ process.exit(2);
101
+ }
102
+ }
103
+ main().catch((err) => {
104
+ process.stderr.write(`Rigour hook checker fatal error: ${err.message}\n`);
105
+ process.exit(1);
106
+ });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Hook configuration templates for each AI coding tool.
3
+ *
4
+ * Each template generates the tool-native config format:
5
+ * - Claude Code: .claude/settings.json (PostToolUse matcher)
6
+ * - Cursor: .cursor/hooks.json (afterFileEdit event)
7
+ * - Cline: .clinerules/hooks/PostToolUse (executable script)
8
+ * - Windsurf: .windsurf/hooks.json (post_write_code event)
9
+ *
10
+ * @since v3.0.0
11
+ */
12
+ import type { HookTool } from './types.js';
13
+ export interface GeneratedHookFile {
14
+ path: string;
15
+ content: string;
16
+ executable?: boolean;
17
+ description: string;
18
+ }
19
+ /**
20
+ * Generate hook config files for a specific tool.
21
+ */
22
+ export declare function generateHookFiles(tool: HookTool, checkerPath: string): GeneratedHookFile[];
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Hook configuration templates for each AI coding tool.
3
+ *
4
+ * Each template generates the tool-native config format:
5
+ * - Claude Code: .claude/settings.json (PostToolUse matcher)
6
+ * - Cursor: .cursor/hooks.json (afterFileEdit event)
7
+ * - Cline: .clinerules/hooks/PostToolUse (executable script)
8
+ * - Windsurf: .windsurf/hooks.json (post_write_code event)
9
+ *
10
+ * @since v3.0.0
11
+ */
12
+ /**
13
+ * Generate hook config files for a specific tool.
14
+ */
15
+ export function generateHookFiles(tool, checkerPath) {
16
+ switch (tool) {
17
+ case 'claude':
18
+ return generateClaudeHooks(checkerPath);
19
+ case 'cursor':
20
+ return generateCursorHooks(checkerPath);
21
+ case 'cline':
22
+ return generateClineHooks(checkerPath);
23
+ case 'windsurf':
24
+ return generateWindsurfHooks(checkerPath);
25
+ default:
26
+ return [];
27
+ }
28
+ }
29
+ function generateClaudeHooks(checkerPath) {
30
+ const settings = {
31
+ hooks: {
32
+ PostToolUse: [
33
+ {
34
+ matcher: "Write|Edit|MultiEdit",
35
+ hooks: [
36
+ {
37
+ type: "command",
38
+ command: `node ${checkerPath} --files "$TOOL_INPUT_file_path"`,
39
+ }
40
+ ]
41
+ }
42
+ ]
43
+ }
44
+ };
45
+ return [
46
+ {
47
+ path: '.claude/settings.json',
48
+ content: JSON.stringify(settings, null, 4),
49
+ description: 'Claude Code PostToolUse hook — runs Rigour fast-check after every Write/Edit',
50
+ },
51
+ ];
52
+ }
53
+ function generateCursorHooks(checkerPath) {
54
+ const hooks = {
55
+ version: 1,
56
+ hooks: {
57
+ afterFileEdit: [
58
+ {
59
+ command: `node ${checkerPath} --stdin`,
60
+ }
61
+ ]
62
+ }
63
+ };
64
+ const wrapper = `#!/usr/bin/env node
65
+ /**
66
+ * Cursor afterFileEdit hook wrapper for Rigour.
67
+ * Receives { file_path, old_content, new_content } on stdin.
68
+ * Runs Rigour fast-check on the edited file.
69
+ */
70
+ const { runHookChecker } = require('./node_modules/@rigour-labs/core/dist/hooks/checker.js');
71
+
72
+ let data = '';
73
+ process.stdin.on('data', chunk => { data += chunk; });
74
+ process.stdin.on('end', async () => {
75
+ try {
76
+ const payload = JSON.parse(data);
77
+ const result = await runHookChecker({
78
+ cwd: process.cwd(),
79
+ files: [payload.file_path],
80
+ });
81
+
82
+ // Write result to stdout for Cursor to consume
83
+ process.stdout.write(JSON.stringify({ status: 'ok' }));
84
+
85
+ // Log failures to stderr (visible in Cursor Hooks panel)
86
+ if (result.status === 'fail') {
87
+ for (const f of result.failures) {
88
+ const loc = f.line ? \`:\${f.line}\` : '';
89
+ process.stderr.write(\`[rigour/\${f.gate}] \${f.file}\${loc}: \${f.message}\\n\`);
90
+ }
91
+ }
92
+ } catch (err) {
93
+ process.stderr.write(\`Rigour hook error: \${err.message}\\n\`);
94
+ process.stdout.write(JSON.stringify({ status: 'ok' }));
95
+ }
96
+ });
97
+ `;
98
+ return [
99
+ {
100
+ path: '.cursor/hooks.json',
101
+ content: JSON.stringify(hooks, null, 4),
102
+ description: 'Cursor afterFileEdit hook config',
103
+ },
104
+ {
105
+ path: '.cursor/rigour-hook.js',
106
+ content: wrapper,
107
+ executable: true,
108
+ description: 'Cursor hook wrapper that reads stdin and runs Rigour checker',
109
+ },
110
+ ];
111
+ }
112
+ function generateClineHooks(checkerPath) {
113
+ const script = `#!/usr/bin/env node
114
+ /**
115
+ * Cline PostToolUse hook for Rigour.
116
+ * Receives JSON on stdin with { toolName, toolInput, toolOutput }.
117
+ * Only triggers on write_to_file and replace_in_file tools.
118
+ */
119
+ const { runHookChecker } = require('./node_modules/@rigour-labs/core/dist/hooks/checker.js');
120
+
121
+ const WRITE_TOOLS = ['write_to_file', 'replace_in_file'];
122
+
123
+ let data = '';
124
+ process.stdin.on('data', chunk => { data += chunk; });
125
+ process.stdin.on('end', async () => {
126
+ try {
127
+ const payload = JSON.parse(data);
128
+
129
+ if (!WRITE_TOOLS.includes(payload.toolName)) {
130
+ // Not a write tool, pass through
131
+ process.stdout.write(JSON.stringify({}));
132
+ process.exit(0);
133
+ return;
134
+ }
135
+
136
+ const filePath = payload.toolInput?.path || payload.toolInput?.file_path;
137
+ if (!filePath) {
138
+ process.stdout.write(JSON.stringify({}));
139
+ process.exit(0);
140
+ return;
141
+ }
142
+
143
+ const result = await runHookChecker({
144
+ cwd: process.cwd(),
145
+ files: [filePath],
146
+ });
147
+
148
+ if (result.status === 'fail') {
149
+ const messages = result.failures
150
+ .map(f => {
151
+ const loc = f.line ? \`:\${f.line}\` : '';
152
+ return \`[rigour/\${f.gate}] \${f.file}\${loc}: \${f.message}\`;
153
+ })
154
+ .join('\\n');
155
+
156
+ process.stdout.write(JSON.stringify({
157
+ contextModification: \`\\n[Rigour Quality Gate] Found \${result.failures.length} issue(s):\\n\${messages}\\nPlease fix before continuing.\`,
158
+ }));
159
+ } else {
160
+ process.stdout.write(JSON.stringify({}));
161
+ }
162
+ } catch (err) {
163
+ process.stderr.write(\`Rigour hook error: \${err.message}\\n\`);
164
+ process.stdout.write(JSON.stringify({}));
165
+ }
166
+ });
167
+ `;
168
+ return [
169
+ {
170
+ path: '.clinerules/hooks/PostToolUse',
171
+ content: script,
172
+ executable: true,
173
+ description: 'Cline PostToolUse hook — runs Rigour fast-check after file writes',
174
+ },
175
+ ];
176
+ }
177
+ function generateWindsurfHooks(checkerPath) {
178
+ const hooks = {
179
+ version: 1,
180
+ hooks: {
181
+ post_write_code: [
182
+ {
183
+ command: `node ${checkerPath} --stdin`,
184
+ }
185
+ ]
186
+ }
187
+ };
188
+ const wrapper = `#!/usr/bin/env node
189
+ /**
190
+ * Windsurf post_write_code hook wrapper for Rigour.
191
+ * Receives { file_path, content } on stdin from Cascade agent.
192
+ * Runs Rigour fast-check on the written file.
193
+ */
194
+ const { runHookChecker } = require('./node_modules/@rigour-labs/core/dist/hooks/checker.js');
195
+
196
+ let data = '';
197
+ process.stdin.on('data', chunk => { data += chunk; });
198
+ process.stdin.on('end', async () => {
199
+ try {
200
+ const payload = JSON.parse(data);
201
+ const result = await runHookChecker({
202
+ cwd: process.cwd(),
203
+ files: [payload.file_path],
204
+ });
205
+
206
+ if (result.status === 'fail') {
207
+ for (const f of result.failures) {
208
+ const loc = f.line ? \`:\${f.line}\` : '';
209
+ process.stderr.write(\`[rigour/\${f.gate}] \${f.file}\${loc}: \${f.message}\\n\`);
210
+ }
211
+ // Exit 2 = block (if configured), exit 0 = warn only
212
+ process.exit(0);
213
+ }
214
+ } catch (err) {
215
+ process.stderr.write(\`Rigour hook error: \${err.message}\\n\`);
216
+ }
217
+ });
218
+ `;
219
+ return [
220
+ {
221
+ path: '.windsurf/hooks.json',
222
+ content: JSON.stringify(hooks, null, 4),
223
+ description: 'Windsurf post_write_code hook config',
224
+ },
225
+ {
226
+ path: '.windsurf/rigour-hook.js',
227
+ content: wrapper,
228
+ executable: true,
229
+ description: 'Windsurf hook wrapper that reads stdin and runs Rigour checker',
230
+ },
231
+ ];
232
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Hook system types for multi-tool integration.
3
+ *
4
+ * Each AI coding tool (Claude Code, Cursor, Cline, Windsurf)
5
+ * has its own hook format. These types unify the config generation.
6
+ *
7
+ * @since v3.0.0
8
+ */
9
+ export type HookTool = 'claude' | 'cursor' | 'cline' | 'windsurf';
10
+ export interface HookConfig {
11
+ /** Which tools to generate hooks for */
12
+ tools: HookTool[];
13
+ /** Gates to run in the hook checker (fast subset) */
14
+ fast_gates: string[];
15
+ /** Max execution time in ms before the checker aborts */
16
+ timeout_ms: number;
17
+ /** Whether to block the tool on failure (exit 2) or just warn */
18
+ block_on_failure: boolean;
19
+ }
20
+ /** The fast gates that can run per-file in <200ms */
21
+ export declare const FAST_GATE_IDS: readonly ["hallucinated-imports", "promise-safety", "security-patterns", "file-size"];
22
+ export declare const DEFAULT_HOOK_CONFIG: HookConfig;
23
+ export type FastGateId = typeof FAST_GATE_IDS[number];
24
+ export interface HookCheckerResult {
25
+ status: 'pass' | 'fail' | 'error';
26
+ failures: Array<{
27
+ gate: string;
28
+ file: string;
29
+ message: string;
30
+ severity: string;
31
+ line?: number;
32
+ }>;
33
+ duration_ms: number;
34
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Hook system types for multi-tool integration.
3
+ *
4
+ * Each AI coding tool (Claude Code, Cursor, Cline, Windsurf)
5
+ * has its own hook format. These types unify the config generation.
6
+ *
7
+ * @since v3.0.0
8
+ */
9
+ /** The fast gates that can run per-file in <200ms */
10
+ export const FAST_GATE_IDS = [
11
+ 'hallucinated-imports',
12
+ 'promise-safety',
13
+ 'security-patterns',
14
+ 'file-size',
15
+ ];
16
+ export const DEFAULT_HOOK_CONFIG = {
17
+ tools: ['claude'],
18
+ fast_gates: [...FAST_GATE_IDS],
19
+ timeout_ms: 5000,
20
+ block_on_failure: false,
21
+ };
package/dist/index.d.ts CHANGED
@@ -8,3 +8,4 @@ export { Gate, GateContext } from './gates/base.js';
8
8
  export { RetryLoopBreakerGate } from './gates/retry-loop-breaker.js';
9
9
  export * from './utils/logger.js';
10
10
  export * from './services/score-history.js';
11
+ export * from './hooks/index.js';
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ export { Gate } from './gates/base.js';
8
8
  export { RetryLoopBreakerGate } from './gates/retry-loop-breaker.js';
9
9
  export * from './utils/logger.js';
10
10
  export * from './services/score-history.js';
11
+ export * from './hooks/index.js';
11
12
  // Pattern Index is intentionally NOT exported here to prevent
12
13
  // native dependency issues (sharp/transformers) from leaking into
13
14
  // non-AI parts of the system.
@@ -482,6 +482,13 @@ export const UNIVERSAL_CONFIG = {
482
482
  ignore_patterns: [],
483
483
  },
484
484
  },
485
+ hooks: {
486
+ enabled: false,
487
+ tools: [],
488
+ fast_gates: ['hallucinated-imports', 'promise-safety', 'security-patterns', 'file-size'],
489
+ timeout_ms: 5000,
490
+ block_on_failure: false,
491
+ },
485
492
  output: {
486
493
  report_path: 'rigour-report.json',
487
494
  },
@@ -568,6 +568,25 @@ export declare const CommandsSchema: z.ZodObject<{
568
568
  typecheck?: string | undefined;
569
569
  test?: string | undefined;
570
570
  }>;
571
+ export declare const HooksSchema: z.ZodDefault<z.ZodOptional<z.ZodObject<{
572
+ enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
573
+ tools: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodEnum<["claude", "cursor", "cline", "windsurf"]>, "many">>>;
574
+ fast_gates: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
575
+ timeout_ms: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
576
+ block_on_failure: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
577
+ }, "strip", z.ZodTypeAny, {
578
+ enabled: boolean;
579
+ tools: ("claude" | "cursor" | "cline" | "windsurf")[];
580
+ fast_gates: string[];
581
+ timeout_ms: number;
582
+ block_on_failure: boolean;
583
+ }, {
584
+ enabled?: boolean | undefined;
585
+ tools?: ("claude" | "cursor" | "cline" | "windsurf")[] | undefined;
586
+ fast_gates?: string[] | undefined;
587
+ timeout_ms?: number | undefined;
588
+ block_on_failure?: boolean | undefined;
589
+ }>>>;
571
590
  export declare const ConfigSchema: z.ZodObject<{
572
591
  version: z.ZodDefault<z.ZodNumber>;
573
592
  preset: z.ZodOptional<z.ZodString>;
@@ -1141,6 +1160,25 @@ export declare const ConfigSchema: z.ZodObject<{
1141
1160
  check_unsafe_fetch?: boolean | undefined;
1142
1161
  } | undefined;
1143
1162
  }>>>;
1163
+ hooks: z.ZodDefault<z.ZodOptional<z.ZodObject<{
1164
+ enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
1165
+ tools: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodEnum<["claude", "cursor", "cline", "windsurf"]>, "many">>>;
1166
+ fast_gates: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
1167
+ timeout_ms: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
1168
+ block_on_failure: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
1169
+ }, "strip", z.ZodTypeAny, {
1170
+ enabled: boolean;
1171
+ tools: ("claude" | "cursor" | "cline" | "windsurf")[];
1172
+ fast_gates: string[];
1173
+ timeout_ms: number;
1174
+ block_on_failure: boolean;
1175
+ }, {
1176
+ enabled?: boolean | undefined;
1177
+ tools?: ("claude" | "cursor" | "cline" | "windsurf")[] | undefined;
1178
+ fast_gates?: string[] | undefined;
1179
+ timeout_ms?: number | undefined;
1180
+ block_on_failure?: boolean | undefined;
1181
+ }>>>;
1144
1182
  output: z.ZodDefault<z.ZodOptional<z.ZodObject<{
1145
1183
  report_path: z.ZodDefault<z.ZodString>;
1146
1184
  }, "strip", z.ZodTypeAny, {
@@ -1278,6 +1316,13 @@ export declare const ConfigSchema: z.ZodObject<{
1278
1316
  check_unsafe_fetch: boolean;
1279
1317
  };
1280
1318
  };
1319
+ hooks: {
1320
+ enabled: boolean;
1321
+ tools: ("claude" | "cursor" | "cline" | "windsurf")[];
1322
+ fast_gates: string[];
1323
+ timeout_ms: number;
1324
+ block_on_failure: boolean;
1325
+ };
1281
1326
  output: {
1282
1327
  report_path: string;
1283
1328
  };
@@ -1414,6 +1459,13 @@ export declare const ConfigSchema: z.ZodObject<{
1414
1459
  check_unsafe_fetch?: boolean | undefined;
1415
1460
  } | undefined;
1416
1461
  } | undefined;
1462
+ hooks?: {
1463
+ enabled?: boolean | undefined;
1464
+ tools?: ("claude" | "cursor" | "cline" | "windsurf")[] | undefined;
1465
+ fast_gates?: string[] | undefined;
1466
+ timeout_ms?: number | undefined;
1467
+ block_on_failure?: boolean | undefined;
1468
+ } | undefined;
1417
1469
  output?: {
1418
1470
  report_path?: string | undefined;
1419
1471
  } | undefined;
@@ -1421,9 +1473,11 @@ export declare const ConfigSchema: z.ZodObject<{
1421
1473
  }>;
1422
1474
  export type Gates = z.infer<typeof GatesSchema>;
1423
1475
  export type Commands = z.infer<typeof CommandsSchema>;
1476
+ export type Hooks = z.infer<typeof HooksSchema>;
1424
1477
  export type Config = z.infer<typeof ConfigSchema>;
1425
1478
  export type RawGates = z.input<typeof GatesSchema>;
1426
1479
  export type RawCommands = z.input<typeof CommandsSchema>;
1480
+ export type RawHooks = z.input<typeof HooksSchema>;
1427
1481
  export type RawConfig = z.input<typeof ConfigSchema>;
1428
1482
  export declare const StatusSchema: z.ZodEnum<["PASS", "FAIL", "SKIP", "ERROR"]>;
1429
1483
  export type Status = z.infer<typeof StatusSchema>;
@@ -144,12 +144,25 @@ export const CommandsSchema = z.object({
144
144
  typecheck: z.string().optional(),
145
145
  test: z.string().optional(),
146
146
  });
147
+ export const HooksSchema = z.object({
148
+ enabled: z.boolean().optional().default(false),
149
+ tools: z.array(z.enum(['claude', 'cursor', 'cline', 'windsurf'])).optional().default([]),
150
+ fast_gates: z.array(z.string()).optional().default([
151
+ 'hallucinated-imports',
152
+ 'promise-safety',
153
+ 'security-patterns',
154
+ 'file-size',
155
+ ]),
156
+ timeout_ms: z.number().optional().default(5000),
157
+ block_on_failure: z.boolean().optional().default(false),
158
+ }).optional().default({});
147
159
  export const ConfigSchema = z.object({
148
160
  version: z.number().default(1),
149
161
  preset: z.string().optional(),
150
162
  paradigm: z.string().optional(),
151
163
  commands: CommandsSchema.optional().default({}),
152
164
  gates: GatesSchema.optional().default({}),
165
+ hooks: HooksSchema,
153
166
  output: z.object({
154
167
  report_path: z.string().default('rigour-report.json'),
155
168
  }).optional().default({}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Deterministic quality gate engine for AI-generated code. AST analysis, drift detection, and Fix Packet generation across TypeScript, JavaScript, Python, Go, Ruby, and C#.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://rigour.run",