@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.
- package/dist/gates/security-patterns-owasp.test.d.ts +1 -0
- package/dist/gates/security-patterns-owasp.test.js +171 -0
- package/dist/gates/security-patterns.d.ts +4 -0
- package/dist/gates/security-patterns.js +100 -0
- package/dist/hooks/checker.d.ts +23 -0
- package/dist/hooks/checker.js +222 -0
- package/dist/hooks/checker.test.d.ts +1 -0
- package/dist/hooks/checker.test.js +132 -0
- package/dist/hooks/index.d.ts +9 -0
- package/dist/hooks/index.js +8 -0
- package/dist/hooks/standalone-checker.d.ts +15 -0
- package/dist/hooks/standalone-checker.js +106 -0
- package/dist/hooks/templates.d.ts +22 -0
- package/dist/hooks/templates.js +232 -0
- package/dist/hooks/types.d.ts +34 -0
- package/dist/hooks/types.js +21 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/templates/index.js +7 -0
- package/dist/types/index.d.ts +54 -0
- package/dist/types/index.js +13 -0
- package/package.json +1 -1
|
@@ -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,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
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.
|
package/dist/templates/index.js
CHANGED
|
@@ -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
|
},
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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>;
|
package/dist/types/index.js
CHANGED
|
@@ -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.
|
|
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",
|