@rigour-labs/core 2.22.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/README.md +58 -0
- package/dist/context.test.js +2 -3
- package/dist/environment.test.js +2 -1
- package/dist/gates/agent-team.d.ts +2 -1
- package/dist/gates/agent-team.js +1 -0
- package/dist/gates/base.d.ts +3 -1
- package/dist/gates/base.js +3 -0
- package/dist/gates/checkpoint.d.ts +2 -1
- package/dist/gates/checkpoint.js +3 -2
- package/dist/gates/context-window-artifacts.d.ts +2 -1
- package/dist/gates/context-window-artifacts.js +6 -3
- package/dist/gates/context.d.ts +2 -1
- package/dist/gates/context.js +1 -0
- package/dist/gates/coverage.js +3 -1
- package/dist/gates/dependency.js +5 -5
- package/dist/gates/duplication-drift.d.ts +2 -1
- package/dist/gates/duplication-drift.js +4 -1
- package/dist/gates/environment.js +4 -4
- package/dist/gates/hallucinated-imports.d.ts +21 -2
- package/dist/gates/hallucinated-imports.js +116 -2
- package/dist/gates/inconsistent-error-handling.d.ts +2 -1
- package/dist/gates/inconsistent-error-handling.js +21 -7
- package/dist/gates/promise-safety.d.ts +68 -0
- package/dist/gates/promise-safety.js +509 -0
- package/dist/gates/retry-loop-breaker.d.ts +2 -1
- package/dist/gates/retry-loop-breaker.js +2 -1
- package/dist/gates/runner.js +34 -1
- package/dist/gates/safety.d.ts +2 -1
- package/dist/gates/safety.js +2 -1
- 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 +6 -1
- package/dist/gates/security-patterns.js +101 -0
- package/dist/gates/structure.js +1 -1
- 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 +2 -0
- package/dist/index.js +2 -0
- package/dist/services/fix-packet-service.d.ts +0 -1
- package/dist/services/fix-packet-service.js +9 -14
- package/dist/services/score-history.d.ts +54 -0
- package/dist/services/score-history.js +122 -0
- package/dist/templates/index.js +176 -0
- package/dist/types/fix-packet.d.ts +5 -5
- package/dist/types/fix-packet.js +1 -1
- package/dist/types/index.d.ts +207 -0
- package/dist/types/index.js +32 -0
- package/package.json +21 -1
- package/src/context.test.ts +0 -256
- package/src/discovery.test.ts +0 -88
- package/src/discovery.ts +0 -112
- package/src/environment.test.ts +0 -115
- package/src/gates/agent-team.test.ts +0 -134
- package/src/gates/agent-team.ts +0 -210
- package/src/gates/ast-handlers/base.ts +0 -13
- package/src/gates/ast-handlers/python.ts +0 -145
- package/src/gates/ast-handlers/python_parser.py +0 -181
- package/src/gates/ast-handlers/typescript.ts +0 -264
- package/src/gates/ast-handlers/universal.ts +0 -184
- package/src/gates/ast.ts +0 -54
- package/src/gates/base.ts +0 -28
- package/src/gates/checkpoint.test.ts +0 -135
- package/src/gates/checkpoint.ts +0 -311
- package/src/gates/content.ts +0 -51
- package/src/gates/context-window-artifacts.ts +0 -277
- package/src/gates/context.ts +0 -270
- package/src/gates/coverage.ts +0 -74
- package/src/gates/dependency.ts +0 -108
- package/src/gates/duplication-drift.ts +0 -231
- package/src/gates/environment.ts +0 -94
- package/src/gates/file.ts +0 -46
- package/src/gates/hallucinated-imports.ts +0 -361
- package/src/gates/inconsistent-error-handling.ts +0 -254
- package/src/gates/retry-loop-breaker.ts +0 -151
- package/src/gates/runner.ts +0 -188
- package/src/gates/safety.ts +0 -56
- package/src/gates/security-patterns.test.ts +0 -162
- package/src/gates/security-patterns.ts +0 -306
- package/src/gates/structure.ts +0 -36
- package/src/index.ts +0 -13
- package/src/pattern-index/embeddings.ts +0 -84
- package/src/pattern-index/index.ts +0 -59
- package/src/pattern-index/indexer.test.ts +0 -276
- package/src/pattern-index/indexer.ts +0 -1023
- package/src/pattern-index/matcher.test.ts +0 -293
- package/src/pattern-index/matcher.ts +0 -493
- package/src/pattern-index/overrides.ts +0 -235
- package/src/pattern-index/security.ts +0 -151
- package/src/pattern-index/staleness.test.ts +0 -313
- package/src/pattern-index/staleness.ts +0 -568
- package/src/pattern-index/types.ts +0 -339
- package/src/safety.test.ts +0 -53
- package/src/services/adaptive-thresholds.test.ts +0 -189
- package/src/services/adaptive-thresholds.ts +0 -275
- package/src/services/context-engine.ts +0 -104
- package/src/services/fix-packet-service.ts +0 -42
- package/src/services/state-service.ts +0 -138
- package/src/smoke.test.ts +0 -18
- package/src/templates/index.ts +0 -338
- package/src/types/fix-packet.ts +0 -32
- package/src/types/index.ts +0 -200
- package/src/utils/logger.ts +0 -43
- package/src/utils/scanner.test.ts +0 -37
- package/src/utils/scanner.ts +0 -43
- package/tsconfig.json +0 -10
- package/vitest.config.ts +0 -7
- package/vitest.setup.ts +0 -30
package/package.json
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://rigour.run",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"quality-gates",
|
|
9
|
+
"ai-code-quality",
|
|
10
|
+
"static-analysis",
|
|
11
|
+
"ast",
|
|
12
|
+
"drift-detection",
|
|
13
|
+
"code-review",
|
|
14
|
+
"linter",
|
|
15
|
+
"typescript",
|
|
16
|
+
"python",
|
|
17
|
+
"golang",
|
|
18
|
+
"csharp"
|
|
19
|
+
],
|
|
4
20
|
"type": "module",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
5
25
|
"main": "dist/index.js",
|
|
6
26
|
"types": "dist/index.d.ts",
|
|
7
27
|
"exports": {
|
package/src/context.test.ts
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
-
import { GateRunner } from '../src/gates/runner.js';
|
|
3
|
-
import fs from 'fs-extra';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
|
|
7
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const TEST_CWD = path.join(__dirname, '../temp-test-context');
|
|
9
|
-
|
|
10
|
-
describe('Context Awareness Engine', () => {
|
|
11
|
-
beforeAll(async () => {
|
|
12
|
-
await fs.ensureDir(TEST_CWD);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
afterAll(async () => {
|
|
16
|
-
await fs.remove(TEST_CWD);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('should detect context drift for redundant env suffixes (Golden Example)', async () => {
|
|
20
|
-
// Setup: Define standard GCP_PROJECT_ID
|
|
21
|
-
await fs.writeFile(path.join(TEST_CWD, '.env.example'), 'GCP_PROJECT_ID=my-project\n');
|
|
22
|
-
|
|
23
|
-
// Setup: Use drifted GCP_PROJECT_ID_PRODUCTION
|
|
24
|
-
await fs.writeFile(path.join(TEST_CWD, 'feature.js'), `
|
|
25
|
-
const id = process.env.GCP_PROJECT_ID_PRODUCTION;
|
|
26
|
-
console.log(id);
|
|
27
|
-
`);
|
|
28
|
-
|
|
29
|
-
const config = {
|
|
30
|
-
version: 1,
|
|
31
|
-
commands: {},
|
|
32
|
-
gates: {
|
|
33
|
-
context: {
|
|
34
|
-
enabled: true,
|
|
35
|
-
sensitivity: 0.8,
|
|
36
|
-
mining_depth: 10,
|
|
37
|
-
ignored_patterns: [],
|
|
38
|
-
cross_file_patterns: true,
|
|
39
|
-
naming_consistency: true,
|
|
40
|
-
import_relationships: true,
|
|
41
|
-
max_cross_file_depth: 50,
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
output: { report_path: 'rigour-report.json' }
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const runner = new GateRunner(config as any);
|
|
48
|
-
const report = await runner.run(TEST_CWD);
|
|
49
|
-
|
|
50
|
-
const driftFailures = report.failures.filter(f => f.id === 'context-drift');
|
|
51
|
-
expect(driftFailures.length).toBeGreaterThan(0);
|
|
52
|
-
expect(driftFailures[0].details).toContain('GCP_PROJECT_ID_PRODUCTION');
|
|
53
|
-
expect(driftFailures[0].hint).toContain('GCP_PROJECT_ID');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('should not flag valid environment variables', async () => {
|
|
57
|
-
await fs.writeFile(path.join(TEST_CWD, 'valid.js'), `
|
|
58
|
-
const id = process.env.GCP_PROJECT_ID;
|
|
59
|
-
`);
|
|
60
|
-
|
|
61
|
-
const config = {
|
|
62
|
-
version: 1,
|
|
63
|
-
commands: {},
|
|
64
|
-
gates: {
|
|
65
|
-
context: {
|
|
66
|
-
enabled: true,
|
|
67
|
-
sensitivity: 0.8,
|
|
68
|
-
mining_depth: 100,
|
|
69
|
-
ignored_patterns: [],
|
|
70
|
-
cross_file_patterns: true,
|
|
71
|
-
naming_consistency: true,
|
|
72
|
-
import_relationships: true,
|
|
73
|
-
max_cross_file_depth: 50,
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
output: { report_path: 'rigour-report.json' }
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const runner = new GateRunner(config as any);
|
|
80
|
-
const report = await runner.run(TEST_CWD);
|
|
81
|
-
|
|
82
|
-
const driftFailures = report.failures.filter(f => f.id === 'context-drift');
|
|
83
|
-
// Filter out failures from other files if they still exist in TEST_CWD
|
|
84
|
-
const specificFailures = driftFailures.filter(f => f.files?.includes('valid.js'));
|
|
85
|
-
expect(specificFailures.length).toBe(0);
|
|
86
|
-
});
|
|
87
|
-
it('should classify arrow function exports as camelCase, not unknown', async () => {
|
|
88
|
-
// Create files with arrow function patterns that previously returned 'unknown'
|
|
89
|
-
await fs.writeFile(path.join(TEST_CWD, 'api.ts'), `
|
|
90
|
-
export const fetchData = async () => { return []; };
|
|
91
|
-
export const getUserProfile = async (id: string) => { return {}; };
|
|
92
|
-
export const use = () => {};
|
|
93
|
-
export const get = async () => {};
|
|
94
|
-
const handleClick = (e: Event) => {};
|
|
95
|
-
let processItem = async (item: any) => {};
|
|
96
|
-
`);
|
|
97
|
-
|
|
98
|
-
// Create a second file with consistent arrow function naming
|
|
99
|
-
await fs.writeFile(path.join(TEST_CWD, 'service.ts'), `
|
|
100
|
-
export const createUser = async (data: any) => {};
|
|
101
|
-
export const deleteUser = async (id: string) => {};
|
|
102
|
-
export const updateUser = async (id: string, data: any) => {};
|
|
103
|
-
`);
|
|
104
|
-
|
|
105
|
-
const config = {
|
|
106
|
-
version: 1,
|
|
107
|
-
commands: {},
|
|
108
|
-
gates: {
|
|
109
|
-
context: {
|
|
110
|
-
enabled: true,
|
|
111
|
-
sensitivity: 0.8,
|
|
112
|
-
mining_depth: 10,
|
|
113
|
-
ignored_patterns: [],
|
|
114
|
-
cross_file_patterns: true,
|
|
115
|
-
naming_consistency: true,
|
|
116
|
-
import_relationships: true,
|
|
117
|
-
max_cross_file_depth: 50,
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
output: { report_path: 'rigour-report.json' }
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
const runner = new GateRunner(config as any);
|
|
124
|
-
const report = await runner.run(TEST_CWD);
|
|
125
|
-
|
|
126
|
-
// Should NOT have any "unknown" naming convention failures
|
|
127
|
-
const namingFailures = report.failures.filter(f =>
|
|
128
|
-
f.id === 'context-drift' && f.details?.includes('unknown')
|
|
129
|
-
);
|
|
130
|
-
expect(namingFailures.length).toBe(0);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('should not classify plain variable declarations as function patterns', async () => {
|
|
134
|
-
// Create file with non-function const declarations
|
|
135
|
-
await fs.writeFile(path.join(TEST_CWD, 'constants.ts'), `
|
|
136
|
-
export const API_URL = 'https://api.example.com';
|
|
137
|
-
export const MAX_RETRIES = 3;
|
|
138
|
-
const config = { timeout: 5000 };
|
|
139
|
-
let count = 0;
|
|
140
|
-
`);
|
|
141
|
-
|
|
142
|
-
// Create file with actual functions for a dominant pattern
|
|
143
|
-
await fs.writeFile(path.join(TEST_CWD, 'utils.ts'), `
|
|
144
|
-
function getData() { return []; }
|
|
145
|
-
function setData(d: any) { return d; }
|
|
146
|
-
function processRequest(req: any) { return req; }
|
|
147
|
-
`);
|
|
148
|
-
|
|
149
|
-
const config = {
|
|
150
|
-
version: 1,
|
|
151
|
-
commands: {},
|
|
152
|
-
gates: {
|
|
153
|
-
context: {
|
|
154
|
-
enabled: true,
|
|
155
|
-
sensitivity: 0.8,
|
|
156
|
-
mining_depth: 10,
|
|
157
|
-
ignored_patterns: [],
|
|
158
|
-
cross_file_patterns: true,
|
|
159
|
-
naming_consistency: true,
|
|
160
|
-
import_relationships: true,
|
|
161
|
-
max_cross_file_depth: 50,
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
output: { report_path: 'rigour-report.json' }
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const runner = new GateRunner(config as any);
|
|
168
|
-
const report = await runner.run(TEST_CWD);
|
|
169
|
-
|
|
170
|
-
// SCREAMING_SNAKE constants should NOT create naming drift failures
|
|
171
|
-
// because they should not be in the 'function' pattern bucket at all
|
|
172
|
-
const namingFailures = report.failures.filter(f =>
|
|
173
|
-
f.id === 'context-drift' && f.details?.includes('SCREAMING_SNAKE')
|
|
174
|
-
);
|
|
175
|
-
expect(namingFailures.length).toBe(0);
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Direct unit tests for detectCasing logic
|
|
181
|
-
*/
|
|
182
|
-
describe('detectCasing classification', () => {
|
|
183
|
-
// We test the regex rules directly since detectCasing is private
|
|
184
|
-
function detectCasing(name: string): string {
|
|
185
|
-
if (/^[A-Z][a-z]/.test(name) && /[a-z][A-Z]/.test(name)) return 'PascalCase';
|
|
186
|
-
if (/^[a-z]/.test(name) && /[a-z][A-Z]/.test(name)) return 'camelCase';
|
|
187
|
-
if (/^[a-z][a-zA-Z0-9]*$/.test(name)) return 'camelCase'; // single-word lowercase
|
|
188
|
-
if (/^[a-z]+(_[a-z]+)+$/.test(name)) return 'snake_case';
|
|
189
|
-
if (/^[A-Z]+(_[A-Z]+)*$/.test(name)) return 'SCREAMING_SNAKE';
|
|
190
|
-
if (/^[A-Z][a-zA-Z]*$/.test(name)) return 'PascalCase';
|
|
191
|
-
return 'unknown';
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Multi-word camelCase
|
|
195
|
-
it('classifies multi-word camelCase', () => {
|
|
196
|
-
expect(detectCasing('fetchData')).toBe('camelCase');
|
|
197
|
-
expect(detectCasing('getUserProfile')).toBe('camelCase');
|
|
198
|
-
expect(detectCasing('handleClick')).toBe('camelCase');
|
|
199
|
-
expect(detectCasing('processItem')).toBe('camelCase');
|
|
200
|
-
expect(detectCasing('createNewUser')).toBe('camelCase');
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// Single-word lowercase (the bug fix)
|
|
204
|
-
it('classifies single-word lowercase as camelCase', () => {
|
|
205
|
-
expect(detectCasing('fetch')).toBe('camelCase');
|
|
206
|
-
expect(detectCasing('use')).toBe('camelCase');
|
|
207
|
-
expect(detectCasing('get')).toBe('camelCase');
|
|
208
|
-
expect(detectCasing('set')).toBe('camelCase');
|
|
209
|
-
expect(detectCasing('run')).toBe('camelCase');
|
|
210
|
-
expect(detectCasing('a')).toBe('camelCase');
|
|
211
|
-
expect(detectCasing('x')).toBe('camelCase');
|
|
212
|
-
expect(detectCasing('id')).toBe('camelCase');
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
// Single-word lowercase with digits
|
|
216
|
-
it('classifies lowercase with digits as camelCase', () => {
|
|
217
|
-
expect(detectCasing('handler2')).toBe('camelCase');
|
|
218
|
-
expect(detectCasing('config3')).toBe('camelCase');
|
|
219
|
-
expect(detectCasing('v2')).toBe('camelCase');
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// PascalCase
|
|
223
|
-
it('classifies PascalCase', () => {
|
|
224
|
-
expect(detectCasing('MyComponent')).toBe('PascalCase');
|
|
225
|
-
expect(detectCasing('UserService')).toBe('PascalCase');
|
|
226
|
-
expect(detectCasing('App')).toBe('PascalCase');
|
|
227
|
-
expect(detectCasing('A')).toBe('SCREAMING_SNAKE'); // single uppercase letter
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// snake_case
|
|
231
|
-
it('classifies snake_case', () => {
|
|
232
|
-
expect(detectCasing('my_func')).toBe('snake_case');
|
|
233
|
-
expect(detectCasing('get_data')).toBe('snake_case');
|
|
234
|
-
expect(detectCasing('process_all_items')).toBe('snake_case');
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
// SCREAMING_SNAKE
|
|
238
|
-
it('classifies SCREAMING_SNAKE_CASE', () => {
|
|
239
|
-
expect(detectCasing('API_URL')).toBe('SCREAMING_SNAKE');
|
|
240
|
-
expect(detectCasing('MAX_RETRIES')).toBe('SCREAMING_SNAKE');
|
|
241
|
-
expect(detectCasing('A')).toBe('SCREAMING_SNAKE');
|
|
242
|
-
expect(detectCasing('DB')).toBe('SCREAMING_SNAKE');
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
// Edge cases that should NOT be unknown
|
|
246
|
-
it('does not return unknown for valid identifiers', () => {
|
|
247
|
-
const validIdentifiers = [
|
|
248
|
-
'fetch', 'getData', 'MyClass', 'my_func', 'API_KEY',
|
|
249
|
-
'use', 'run', 'a', 'x', 'id', 'App', 'handler2',
|
|
250
|
-
'processItem', 'UserProfile', 'get_all_data', 'MAX_SIZE',
|
|
251
|
-
];
|
|
252
|
-
for (const name of validIdentifiers) {
|
|
253
|
-
expect(detectCasing(name)).not.toBe('unknown');
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
});
|
package/src/discovery.test.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { DiscoveryService } from './discovery.js';
|
|
3
|
-
import fs from 'fs-extra';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
|
|
6
|
-
vi.mock('fs-extra');
|
|
7
|
-
|
|
8
|
-
describe('DiscoveryService', () => {
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
vi.resetAllMocks();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('should discover project marker in root directory', async () => {
|
|
14
|
-
const service = new DiscoveryService();
|
|
15
|
-
vi.mocked(fs.pathExists).mockImplementation(async (p: string) => p.includes('package.json'));
|
|
16
|
-
vi.mocked(fs.readdir).mockResolvedValue(['package.json'] as any);
|
|
17
|
-
vi.mocked(fs.readFile).mockResolvedValue('{}' as any);
|
|
18
|
-
|
|
19
|
-
const result = await service.discover('/test');
|
|
20
|
-
// If package.json doesn't match a specific role marker, it stays Universal.
|
|
21
|
-
// Let's mock a specific one like 'express'
|
|
22
|
-
vi.mocked(fs.pathExists).mockImplementation(async (p: string) => p.includes('express'));
|
|
23
|
-
const result2 = await service.discover('/test');
|
|
24
|
-
expect(result2.matches.preset?.name).toBe('api');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('should discover project marker in src/ directory (Deep Detection)', async () => {
|
|
28
|
-
const service = new DiscoveryService();
|
|
29
|
-
vi.mocked(fs.pathExists).mockImplementation((async (p: string) => {
|
|
30
|
-
if (p.endsWith('src')) return true;
|
|
31
|
-
if (p.includes('src/index.ts')) return true;
|
|
32
|
-
return false;
|
|
33
|
-
}) as any);
|
|
34
|
-
vi.mocked(fs.readdir).mockImplementation((async (p: string) => {
|
|
35
|
-
if (p.toString().endsWith('/test')) return ['src'] as any;
|
|
36
|
-
if (p.toString().endsWith('src')) return ['index.ts'] as any;
|
|
37
|
-
return [] as any;
|
|
38
|
-
}) as any);
|
|
39
|
-
vi.mocked(fs.readFile).mockResolvedValue('export const x = 1;' as any);
|
|
40
|
-
|
|
41
|
-
const result = await service.discover('/test');
|
|
42
|
-
// Since UNIVERSAL_CONFIG has a default, we check if it found something extra or matches expectation
|
|
43
|
-
// Default is universal, but detecting .ts should tilt it towards node or similar if configured
|
|
44
|
-
// In our current templates, package.json is the node marker.
|
|
45
|
-
// Let's check for paradigm detection which uses content
|
|
46
|
-
expect(result.config).toBeDefined();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should identify OOP paradigm from content in subfolder', async () => {
|
|
50
|
-
const service = new DiscoveryService();
|
|
51
|
-
vi.mocked(fs.pathExists).mockImplementation((async (p: string) => p.endsWith('src') || p.endsWith('src/Service.ts')) as any);
|
|
52
|
-
vi.mocked(fs.readdir).mockImplementation((async (p: string) => {
|
|
53
|
-
if (p.toString().endsWith('src')) return ['Service.ts'] as any;
|
|
54
|
-
return ['src'] as any;
|
|
55
|
-
}) as any);
|
|
56
|
-
vi.mocked(fs.readFile).mockResolvedValue('class MyService {}' as any);
|
|
57
|
-
|
|
58
|
-
const result = await service.discover('/test');
|
|
59
|
-
expect(result.matches.paradigm?.name).toBe('oop');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should include project-type-aware ignore patterns for API preset', async () => {
|
|
63
|
-
const service = new DiscoveryService();
|
|
64
|
-
// Mock finding requirements.txt (Python API marker)
|
|
65
|
-
vi.mocked(fs.pathExists).mockImplementation(async (p: string) => p.includes('requirements.txt'));
|
|
66
|
-
vi.mocked(fs.readdir).mockResolvedValue(['requirements.txt'] as any);
|
|
67
|
-
vi.mocked(fs.readFile).mockResolvedValue('flask==2.0.0' as any);
|
|
68
|
-
|
|
69
|
-
const result = await service.discover('/test');
|
|
70
|
-
expect(result.matches.preset?.name).toBe('api');
|
|
71
|
-
expect(result.config.ignore).toContain('venv/**');
|
|
72
|
-
expect(result.config.ignore).toContain('__pycache__/**');
|
|
73
|
-
expect(result.config.ignore).toContain('*.pyc');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('should include project-type-aware ignore patterns for UI preset', async () => {
|
|
77
|
-
const service = new DiscoveryService();
|
|
78
|
-
// Mock finding next.config.js (UI marker)
|
|
79
|
-
vi.mocked(fs.pathExists).mockImplementation(async (p: string) => p.includes('next.config.js'));
|
|
80
|
-
vi.mocked(fs.readdir).mockResolvedValue(['next.config.js'] as any);
|
|
81
|
-
vi.mocked(fs.readFile).mockResolvedValue('module.exports = {}' as any);
|
|
82
|
-
|
|
83
|
-
const result = await service.discover('/test');
|
|
84
|
-
expect(result.matches.preset?.name).toBe('ui');
|
|
85
|
-
expect(result.config.ignore).toContain('node_modules/**');
|
|
86
|
-
expect(result.config.ignore).toContain('.next/**');
|
|
87
|
-
});
|
|
88
|
-
});
|
package/src/discovery.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { Config, Gates } from './types/index.js';
|
|
4
|
-
import { TEMPLATES, PARADIGM_TEMPLATES, UNIVERSAL_CONFIG } from './templates/index.js';
|
|
5
|
-
|
|
6
|
-
export interface DiscoveryResult {
|
|
7
|
-
config: Config;
|
|
8
|
-
matches: {
|
|
9
|
-
preset?: { name: string; marker: string };
|
|
10
|
-
paradigm?: { name: string; marker: string };
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class DiscoveryService {
|
|
15
|
-
async discover(cwd: string): Promise<DiscoveryResult> {
|
|
16
|
-
let config = { ...UNIVERSAL_CONFIG };
|
|
17
|
-
const matches: DiscoveryResult['matches'] = {};
|
|
18
|
-
|
|
19
|
-
// 1. Detect Role (ui, api, infra, data)
|
|
20
|
-
for (const template of TEMPLATES) {
|
|
21
|
-
const marker = await this.findFirstMarker(cwd, template.markers, true); // Search content for roles too
|
|
22
|
-
if (marker) {
|
|
23
|
-
config = this.mergeConfig(config, template.config);
|
|
24
|
-
matches.preset = { name: template.name, marker };
|
|
25
|
-
break; // Only one role for now
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// 2. Detect Paradigm (oop, functional)
|
|
30
|
-
for (const template of PARADIGM_TEMPLATES) {
|
|
31
|
-
const marker = await this.findFirstMarker(cwd, template.markers, true); // Search content
|
|
32
|
-
if (marker) {
|
|
33
|
-
config = this.mergeConfig(config, template.config);
|
|
34
|
-
matches.paradigm = { name: template.name, marker };
|
|
35
|
-
break;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return { config, matches };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
private mergeConfig(base: Config, extension: any): Config {
|
|
43
|
-
// Deep merge for gates to preserve defaults when overrides are partial
|
|
44
|
-
const mergedGates = { ...base.gates };
|
|
45
|
-
if (extension.gates) {
|
|
46
|
-
for (const [key, value] of Object.entries(extension.gates)) {
|
|
47
|
-
if (typeof value === 'object' && value !== null && !Array.isArray(value) && (mergedGates as any)[key]) {
|
|
48
|
-
(mergedGates as any)[key] = { ...(mergedGates as any)[key], ...value };
|
|
49
|
-
} else {
|
|
50
|
-
(mergedGates as any)[key] = value;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
...base,
|
|
57
|
-
preset: extension.preset || base.preset,
|
|
58
|
-
paradigm: extension.paradigm || base.paradigm,
|
|
59
|
-
commands: { ...base.commands, ...extension.commands },
|
|
60
|
-
gates: mergedGates as Gates,
|
|
61
|
-
ignore: [...new Set([...(base.ignore || []), ...(extension.ignore || [])])],
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
private async findFirstMarker(cwd: string, markers: string[], searchContent: boolean = false): Promise<string | null> {
|
|
66
|
-
for (const marker of markers) {
|
|
67
|
-
const fullPath = path.join(cwd, marker);
|
|
68
|
-
|
|
69
|
-
// File/Directory existence check
|
|
70
|
-
if (await fs.pathExists(fullPath)) {
|
|
71
|
-
return marker;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Deep content check for paradigms
|
|
75
|
-
if (searchContent) {
|
|
76
|
-
const match = await this.existsInContent(cwd, marker);
|
|
77
|
-
if (match) return `content:${marker}`;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private async existsInContent(cwd: string, pattern: string): Promise<boolean> {
|
|
84
|
-
// Simple heuristic: search in top 5 source files
|
|
85
|
-
const files = await this.findSourceFiles(cwd);
|
|
86
|
-
for (const file of files) {
|
|
87
|
-
const content = await fs.readFile(file, 'utf-8');
|
|
88
|
-
if (content.includes(pattern)) return true;
|
|
89
|
-
}
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
private async findSourceFiles(cwd: string): Promise<string[]> {
|
|
94
|
-
const extensions = ['.ts', '.js', '.py', '.go', '.java', '.tf', 'package.json'];
|
|
95
|
-
const samples: string[] = [];
|
|
96
|
-
const commonDirs = ['.', 'src', 'app', 'lib', 'api', 'pkg'];
|
|
97
|
-
|
|
98
|
-
for (const dir of commonDirs) {
|
|
99
|
-
const fullDir = path.join(cwd, dir);
|
|
100
|
-
if (!(await fs.pathExists(fullDir))) continue;
|
|
101
|
-
|
|
102
|
-
const files = await fs.readdir(fullDir);
|
|
103
|
-
for (const file of files) {
|
|
104
|
-
if (extensions.some(ext => file.endsWith(ext))) {
|
|
105
|
-
samples.push(path.join(fullDir, file));
|
|
106
|
-
if (samples.length >= 5) return samples;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return samples;
|
|
111
|
-
}
|
|
112
|
-
}
|
package/src/environment.test.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { GateRunner } from './gates/runner.js';
|
|
3
|
-
import { Config, RawConfig, ConfigSchema } from './types/index.js';
|
|
4
|
-
import fs from 'fs-extra';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
|
|
7
|
-
describe('Environment Alignment Gate', () => {
|
|
8
|
-
const testDir = path.join(process.cwd(), 'temp-test-env');
|
|
9
|
-
|
|
10
|
-
beforeEach(async () => {
|
|
11
|
-
await fs.ensureDir(testDir);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
afterEach(async () => {
|
|
15
|
-
await fs.remove(testDir);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('should detect tool version mismatch (Explicit)', async () => {
|
|
19
|
-
const rawConfig: RawConfig = {
|
|
20
|
-
version: 1,
|
|
21
|
-
gates: {
|
|
22
|
-
environment: {
|
|
23
|
-
enabled: true,
|
|
24
|
-
enforce_contracts: false,
|
|
25
|
-
tools: {
|
|
26
|
-
node: ">=99.0.0" // Guaranteed to fail
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const config = ConfigSchema.parse(rawConfig);
|
|
33
|
-
const runner = new GateRunner(config);
|
|
34
|
-
const report = await runner.run(testDir);
|
|
35
|
-
|
|
36
|
-
expect(report.status).toBe('FAIL');
|
|
37
|
-
const envFailure = report.failures.find(f => f.id === 'environment-alignment');
|
|
38
|
-
expect(envFailure).toBeDefined();
|
|
39
|
-
expect(envFailure?.details).toContain('node');
|
|
40
|
-
expect(envFailure?.details).toContain('version mismatch');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('should detect missing environment variables', async () => {
|
|
44
|
-
const rawConfig: RawConfig = {
|
|
45
|
-
version: 1,
|
|
46
|
-
gates: {
|
|
47
|
-
environment: {
|
|
48
|
-
enabled: true,
|
|
49
|
-
required_env: ["RIGOUR_TEST_VAR_MISSING"]
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const config = ConfigSchema.parse(rawConfig);
|
|
55
|
-
const runner = new GateRunner(config);
|
|
56
|
-
const report = await runner.run(testDir);
|
|
57
|
-
|
|
58
|
-
expect(report.status).toBe('FAIL');
|
|
59
|
-
expect(report.failures[0].details).toContain('RIGOUR_TEST_VAR_MISSING');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should discover contracts from pyproject.toml', async () => {
|
|
63
|
-
// Create mock pyproject.toml with a version that will surely fail
|
|
64
|
-
await fs.writeFile(path.join(testDir, 'pyproject.toml'), `
|
|
65
|
-
[tool.ruff]
|
|
66
|
-
ruff = ">=99.14.0"
|
|
67
|
-
`);
|
|
68
|
-
|
|
69
|
-
const rawConfig: RawConfig = {
|
|
70
|
-
version: 1,
|
|
71
|
-
gates: {
|
|
72
|
-
environment: {
|
|
73
|
-
enabled: true,
|
|
74
|
-
enforce_contracts: true,
|
|
75
|
-
tools: {} // Should discover ruff from file
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const config = ConfigSchema.parse(rawConfig);
|
|
81
|
-
const runner = new GateRunner(config);
|
|
82
|
-
const report = await runner.run(testDir);
|
|
83
|
-
|
|
84
|
-
// This might pass or fail depending on the local ruff version,
|
|
85
|
-
// but we want to check if the gate attempted to check ruff.
|
|
86
|
-
// If ruff is missing, it will fail with "is missing".
|
|
87
|
-
const ruffFailure = report.failures.find(f => f.details.includes('ruff'));
|
|
88
|
-
expect(ruffFailure).toBeDefined();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should prioritize environment gate and run it first', async () => {
|
|
92
|
-
const rawConfig: RawConfig = {
|
|
93
|
-
version: 1,
|
|
94
|
-
gates: {
|
|
95
|
-
max_file_lines: 1,
|
|
96
|
-
environment: {
|
|
97
|
-
enabled: true,
|
|
98
|
-
required_env: ["MANDATORY_SECRET_MISSING"]
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const config = ConfigSchema.parse(rawConfig);
|
|
104
|
-
|
|
105
|
-
// Create a file that would fail max_file_lines
|
|
106
|
-
await fs.writeFile(path.join(testDir, 'large.js'), 'line1\nline2\nline3');
|
|
107
|
-
|
|
108
|
-
const runner = new GateRunner(config);
|
|
109
|
-
const report = await runner.run(testDir);
|
|
110
|
-
|
|
111
|
-
// In a real priority system, we might want to stop after environment failure.
|
|
112
|
-
// Currently GateRunner runs all, but environment-alignment has been unshifted.
|
|
113
|
-
expect(Object.keys(report.summary)[0]).toBe('environment-alignment');
|
|
114
|
-
});
|
|
115
|
-
});
|