@rigour-labs/core 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context.test.d.ts +1 -0
- package/dist/context.test.js +61 -0
- package/dist/discovery.js +12 -7
- package/dist/discovery.test.d.ts +1 -0
- package/dist/discovery.test.js +57 -0
- package/dist/environment.test.d.ts +1 -0
- package/dist/environment.test.js +97 -0
- package/dist/gates/ast-handlers/python.js +15 -1
- package/dist/gates/ast.js +6 -3
- package/dist/gates/base.d.ts +5 -1
- package/dist/gates/base.js +2 -2
- package/dist/gates/content.js +5 -1
- package/dist/gates/context.d.ts +8 -0
- package/dist/gates/context.js +43 -0
- package/dist/gates/coverage.js +6 -1
- package/dist/gates/dependency.js +40 -1
- package/dist/gates/environment.d.ts +8 -0
- package/dist/gates/environment.js +73 -0
- package/dist/gates/file.js +5 -1
- package/dist/gates/runner.d.ts +1 -1
- package/dist/gates/runner.js +19 -2
- package/dist/gates/safety.js +9 -3
- package/dist/safety.test.d.ts +1 -0
- package/dist/safety.test.js +42 -0
- package/dist/services/context-engine.d.ts +22 -0
- package/dist/services/context-engine.js +78 -0
- package/dist/templates/index.js +13 -0
- package/dist/types/index.d.ts +147 -5
- package/dist/types/index.js +13 -0
- package/dist/utils/scanner.js +9 -4
- package/dist/utils/scanner.test.d.ts +1 -0
- package/dist/utils/scanner.test.js +29 -0
- package/package.json +4 -2
- package/src/context.test.ts +73 -0
- package/src/discovery.test.ts +61 -0
- package/src/discovery.ts +11 -6
- package/src/environment.test.ts +115 -0
- package/src/gates/ast-handlers/python.ts +14 -1
- package/src/gates/ast.ts +7 -3
- package/src/gates/base.ts +6 -2
- package/src/gates/content.ts +5 -1
- package/src/gates/context.ts +55 -0
- package/src/gates/coverage.ts +5 -1
- package/src/gates/dependency.ts +65 -1
- package/src/gates/environment.ts +94 -0
- package/src/gates/file.ts +5 -1
- package/src/gates/runner.ts +23 -2
- package/src/gates/safety.ts +11 -4
- package/src/safety.test.ts +53 -0
- package/src/services/context-engine.ts +104 -0
- package/src/templates/index.ts +13 -0
- package/src/types/index.ts +17 -0
- package/src/utils/scanner.test.ts +37 -0
- package/src/utils/scanner.ts +10 -4
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { SafetyGate } from './gates/safety.js';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
vi.mock('execa');
|
|
5
|
+
describe('SafetyGate', () => {
|
|
6
|
+
const config = {
|
|
7
|
+
safety: {
|
|
8
|
+
protected_paths: ['docs/'],
|
|
9
|
+
max_files_changed_per_cycle: 10
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
it('should flag modified (M) protected files', async () => {
|
|
13
|
+
const gate = new SafetyGate(config);
|
|
14
|
+
vi.mocked(execa).mockResolvedValueOnce({ stdout: ' M docs/SPEC.md\n' });
|
|
15
|
+
const failures = await gate.run({ cwd: '/test', record: {} });
|
|
16
|
+
expect(failures).toHaveLength(1);
|
|
17
|
+
expect(failures[0].title).toContain("Protected file 'docs/SPEC.md' was modified.");
|
|
18
|
+
});
|
|
19
|
+
it('should flag added (A) protected files', async () => {
|
|
20
|
+
const gate = new SafetyGate(config);
|
|
21
|
+
vi.mocked(execa).mockResolvedValueOnce({ stdout: 'A docs/NEW.md\n' });
|
|
22
|
+
const failures = await gate.run({ cwd: '/test', record: {} });
|
|
23
|
+
expect(failures).toHaveLength(1);
|
|
24
|
+
expect(failures[0].title).toContain("Protected file 'docs/NEW.md' was modified.");
|
|
25
|
+
});
|
|
26
|
+
it('should NOT flag untracked (??) protected files', async () => {
|
|
27
|
+
const gate = new SafetyGate(config);
|
|
28
|
+
vi.mocked(execa).mockResolvedValueOnce({ stdout: '?? docs/UNTRAKED.md\n' });
|
|
29
|
+
const failures = await gate.run({ cwd: '/test', record: {} });
|
|
30
|
+
expect(failures).toHaveLength(0);
|
|
31
|
+
});
|
|
32
|
+
it('should correctly handle multiple mixed statuses', async () => {
|
|
33
|
+
const gate = new SafetyGate(config);
|
|
34
|
+
vi.mocked(execa).mockResolvedValueOnce({
|
|
35
|
+
stdout: ' M docs/MODIFIED.md\n?? docs/NEW_UNTRACKED.md\n D docs/DELETED.md\n'
|
|
36
|
+
});
|
|
37
|
+
const failures = await gate.run({ cwd: '/test', record: {} });
|
|
38
|
+
expect(failures).toHaveLength(2);
|
|
39
|
+
expect(failures.map(f => f.title)).toContain("Protected file 'docs/MODIFIED.md' was modified.");
|
|
40
|
+
expect(failures.map(f => f.title)).toContain("Protected file 'docs/DELETED.md' was modified.");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Config } from '../types/index.js';
|
|
2
|
+
export interface ProjectAnchor {
|
|
3
|
+
id: string;
|
|
4
|
+
type: 'env' | 'naming' | 'import';
|
|
5
|
+
pattern: string;
|
|
6
|
+
confidence: number;
|
|
7
|
+
occurrences: number;
|
|
8
|
+
}
|
|
9
|
+
export interface GoldenRecord {
|
|
10
|
+
anchors: ProjectAnchor[];
|
|
11
|
+
metadata: {
|
|
12
|
+
scannedFiles: number;
|
|
13
|
+
detectedCasing: 'camelCase' | 'snake_case' | 'PascalCase' | 'unknown';
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export declare class ContextEngine {
|
|
17
|
+
private config;
|
|
18
|
+
constructor(config: Config);
|
|
19
|
+
discover(cwd: string): Promise<GoldenRecord>;
|
|
20
|
+
private mineEnvVars;
|
|
21
|
+
private incrementRegistry;
|
|
22
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { FileScanner } from '../utils/scanner.js';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
export class ContextEngine {
|
|
5
|
+
config;
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
}
|
|
9
|
+
async discover(cwd) {
|
|
10
|
+
const anchors = [];
|
|
11
|
+
const files = await FileScanner.findFiles({
|
|
12
|
+
cwd,
|
|
13
|
+
patterns: [
|
|
14
|
+
'**/*.{ts,js,py,yaml,yml,json}',
|
|
15
|
+
'.env*',
|
|
16
|
+
'**/.env*',
|
|
17
|
+
'**/package.json',
|
|
18
|
+
'**/Dockerfile',
|
|
19
|
+
'**/*.tf'
|
|
20
|
+
]
|
|
21
|
+
});
|
|
22
|
+
const limit = this.config.gates.context?.mining_depth || 100;
|
|
23
|
+
const samples = files.slice(0, limit);
|
|
24
|
+
const envVars = new Map();
|
|
25
|
+
let scannedFiles = 0;
|
|
26
|
+
for (const file of samples) {
|
|
27
|
+
try {
|
|
28
|
+
const content = await fs.readFile(path.join(cwd, file), 'utf-8');
|
|
29
|
+
scannedFiles++;
|
|
30
|
+
this.mineEnvVars(content, file, envVars);
|
|
31
|
+
}
|
|
32
|
+
catch (e) { }
|
|
33
|
+
}
|
|
34
|
+
console.log(`[ContextEngine] Discovered ${envVars.size} env anchors`);
|
|
35
|
+
// Convert envVars to anchors
|
|
36
|
+
for (const [name, count] of envVars.entries()) {
|
|
37
|
+
const confidence = count >= 2 ? 1 : 0.5;
|
|
38
|
+
anchors.push({
|
|
39
|
+
id: name,
|
|
40
|
+
type: 'env',
|
|
41
|
+
pattern: name,
|
|
42
|
+
occurrences: count,
|
|
43
|
+
confidence
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
anchors,
|
|
48
|
+
metadata: {
|
|
49
|
+
scannedFiles,
|
|
50
|
+
detectedCasing: 'unknown', // TODO: Implement casing discovery
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
mineEnvVars(content, file, registry) {
|
|
55
|
+
const isAnchorSource = file.includes('.env') || file.includes('yml') || file.includes('yaml');
|
|
56
|
+
if (isAnchorSource) {
|
|
57
|
+
const matches = content.matchAll(/^\s*([A-Z0-9_]+)\s*=/gm);
|
|
58
|
+
for (const match of matches) {
|
|
59
|
+
// Anchors from .env count for more initially
|
|
60
|
+
registry.set(match[1], (registry.get(match[1]) || 0) + 2);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Source code matches (process.env.VAR or process.env['VAR'])
|
|
64
|
+
const tsJsMatches = content.matchAll(/process\.env(?:\.([A-Z0-9_]+)|\[['"]([A-Z0-9_]+)['"]\])/g);
|
|
65
|
+
for (const match of tsJsMatches) {
|
|
66
|
+
const name = match[1] || match[2];
|
|
67
|
+
this.incrementRegistry(registry, name);
|
|
68
|
+
}
|
|
69
|
+
// Python matches (os.environ.get('VAR') or os.environ['VAR'])
|
|
70
|
+
const pyMatches = content.matchAll(/os\.environ(?:\.get\(|\[)['"]([A-Z0-9_]+)['"]/g);
|
|
71
|
+
for (const match of pyMatches) {
|
|
72
|
+
this.incrementRegistry(registry, match[1]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
incrementRegistry(registry, key) {
|
|
76
|
+
registry.set(key, (registry.get(key) || 0) + 1);
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/templates/index.js
CHANGED
|
@@ -174,9 +174,22 @@ export const UNIVERSAL_CONFIG = {
|
|
|
174
174
|
max_files_changed_per_cycle: 10,
|
|
175
175
|
protected_paths: ['.github/**', 'docs/**', 'rigour.yml'],
|
|
176
176
|
},
|
|
177
|
+
context: {
|
|
178
|
+
enabled: true,
|
|
179
|
+
sensitivity: 0.8,
|
|
180
|
+
mining_depth: 100,
|
|
181
|
+
ignored_patterns: [],
|
|
182
|
+
},
|
|
183
|
+
environment: {
|
|
184
|
+
enabled: true,
|
|
185
|
+
enforce_contracts: true,
|
|
186
|
+
tools: {},
|
|
187
|
+
required_env: [],
|
|
188
|
+
},
|
|
177
189
|
},
|
|
178
190
|
output: {
|
|
179
191
|
report_path: 'rigour-report.json',
|
|
180
192
|
},
|
|
181
193
|
planned: [],
|
|
194
|
+
ignore: [],
|
|
182
195
|
};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -77,6 +77,38 @@ export declare const GatesSchema: z.ZodObject<{
|
|
|
77
77
|
max_files_changed_per_cycle?: number | undefined;
|
|
78
78
|
protected_paths?: string[] | undefined;
|
|
79
79
|
}>>>;
|
|
80
|
+
context: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
81
|
+
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
82
|
+
sensitivity: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
83
|
+
mining_depth: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
84
|
+
ignored_patterns: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
|
|
85
|
+
}, "strip", z.ZodTypeAny, {
|
|
86
|
+
enabled: boolean;
|
|
87
|
+
sensitivity: number;
|
|
88
|
+
mining_depth: number;
|
|
89
|
+
ignored_patterns: string[];
|
|
90
|
+
}, {
|
|
91
|
+
enabled?: boolean | undefined;
|
|
92
|
+
sensitivity?: number | undefined;
|
|
93
|
+
mining_depth?: number | undefined;
|
|
94
|
+
ignored_patterns?: string[] | undefined;
|
|
95
|
+
}>>>;
|
|
96
|
+
environment: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
97
|
+
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
98
|
+
enforce_contracts: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
99
|
+
tools: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>>;
|
|
100
|
+
required_env: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
|
|
101
|
+
}, "strip", z.ZodTypeAny, {
|
|
102
|
+
enabled: boolean;
|
|
103
|
+
enforce_contracts: boolean;
|
|
104
|
+
tools: Record<string, string>;
|
|
105
|
+
required_env: string[];
|
|
106
|
+
}, {
|
|
107
|
+
enabled?: boolean | undefined;
|
|
108
|
+
enforce_contracts?: boolean | undefined;
|
|
109
|
+
tools?: Record<string, string> | undefined;
|
|
110
|
+
required_env?: string[] | undefined;
|
|
111
|
+
}>>>;
|
|
80
112
|
}, "strip", z.ZodTypeAny, {
|
|
81
113
|
max_file_lines: number;
|
|
82
114
|
forbid_todos: boolean;
|
|
@@ -107,6 +139,18 @@ export declare const GatesSchema: z.ZodObject<{
|
|
|
107
139
|
max_files_changed_per_cycle: number;
|
|
108
140
|
protected_paths: string[];
|
|
109
141
|
};
|
|
142
|
+
context: {
|
|
143
|
+
enabled: boolean;
|
|
144
|
+
sensitivity: number;
|
|
145
|
+
mining_depth: number;
|
|
146
|
+
ignored_patterns: string[];
|
|
147
|
+
};
|
|
148
|
+
environment: {
|
|
149
|
+
enabled: boolean;
|
|
150
|
+
enforce_contracts: boolean;
|
|
151
|
+
tools: Record<string, string>;
|
|
152
|
+
required_env: string[];
|
|
153
|
+
};
|
|
110
154
|
}, {
|
|
111
155
|
max_file_lines?: number | undefined;
|
|
112
156
|
forbid_todos?: boolean | undefined;
|
|
@@ -137,6 +181,18 @@ export declare const GatesSchema: z.ZodObject<{
|
|
|
137
181
|
max_files_changed_per_cycle?: number | undefined;
|
|
138
182
|
protected_paths?: string[] | undefined;
|
|
139
183
|
} | undefined;
|
|
184
|
+
context?: {
|
|
185
|
+
enabled?: boolean | undefined;
|
|
186
|
+
sensitivity?: number | undefined;
|
|
187
|
+
mining_depth?: number | undefined;
|
|
188
|
+
ignored_patterns?: string[] | undefined;
|
|
189
|
+
} | undefined;
|
|
190
|
+
environment?: {
|
|
191
|
+
enabled?: boolean | undefined;
|
|
192
|
+
enforce_contracts?: boolean | undefined;
|
|
193
|
+
tools?: Record<string, string> | undefined;
|
|
194
|
+
required_env?: string[] | undefined;
|
|
195
|
+
} | undefined;
|
|
140
196
|
}>;
|
|
141
197
|
export declare const CommandsSchema: z.ZodObject<{
|
|
142
198
|
format: z.ZodOptional<z.ZodString>;
|
|
@@ -252,6 +308,38 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
252
308
|
max_files_changed_per_cycle?: number | undefined;
|
|
253
309
|
protected_paths?: string[] | undefined;
|
|
254
310
|
}>>>;
|
|
311
|
+
context: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
312
|
+
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
313
|
+
sensitivity: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
314
|
+
mining_depth: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
315
|
+
ignored_patterns: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
|
|
316
|
+
}, "strip", z.ZodTypeAny, {
|
|
317
|
+
enabled: boolean;
|
|
318
|
+
sensitivity: number;
|
|
319
|
+
mining_depth: number;
|
|
320
|
+
ignored_patterns: string[];
|
|
321
|
+
}, {
|
|
322
|
+
enabled?: boolean | undefined;
|
|
323
|
+
sensitivity?: number | undefined;
|
|
324
|
+
mining_depth?: number | undefined;
|
|
325
|
+
ignored_patterns?: string[] | undefined;
|
|
326
|
+
}>>>;
|
|
327
|
+
environment: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
328
|
+
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
329
|
+
enforce_contracts: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
330
|
+
tools: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>>;
|
|
331
|
+
required_env: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
|
|
332
|
+
}, "strip", z.ZodTypeAny, {
|
|
333
|
+
enabled: boolean;
|
|
334
|
+
enforce_contracts: boolean;
|
|
335
|
+
tools: Record<string, string>;
|
|
336
|
+
required_env: string[];
|
|
337
|
+
}, {
|
|
338
|
+
enabled?: boolean | undefined;
|
|
339
|
+
enforce_contracts?: boolean | undefined;
|
|
340
|
+
tools?: Record<string, string> | undefined;
|
|
341
|
+
required_env?: string[] | undefined;
|
|
342
|
+
}>>>;
|
|
255
343
|
}, "strip", z.ZodTypeAny, {
|
|
256
344
|
max_file_lines: number;
|
|
257
345
|
forbid_todos: boolean;
|
|
@@ -282,6 +370,18 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
282
370
|
max_files_changed_per_cycle: number;
|
|
283
371
|
protected_paths: string[];
|
|
284
372
|
};
|
|
373
|
+
context: {
|
|
374
|
+
enabled: boolean;
|
|
375
|
+
sensitivity: number;
|
|
376
|
+
mining_depth: number;
|
|
377
|
+
ignored_patterns: string[];
|
|
378
|
+
};
|
|
379
|
+
environment: {
|
|
380
|
+
enabled: boolean;
|
|
381
|
+
enforce_contracts: boolean;
|
|
382
|
+
tools: Record<string, string>;
|
|
383
|
+
required_env: string[];
|
|
384
|
+
};
|
|
285
385
|
}, {
|
|
286
386
|
max_file_lines?: number | undefined;
|
|
287
387
|
forbid_todos?: boolean | undefined;
|
|
@@ -312,6 +412,18 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
312
412
|
max_files_changed_per_cycle?: number | undefined;
|
|
313
413
|
protected_paths?: string[] | undefined;
|
|
314
414
|
} | undefined;
|
|
415
|
+
context?: {
|
|
416
|
+
enabled?: boolean | undefined;
|
|
417
|
+
sensitivity?: number | undefined;
|
|
418
|
+
mining_depth?: number | undefined;
|
|
419
|
+
ignored_patterns?: string[] | undefined;
|
|
420
|
+
} | undefined;
|
|
421
|
+
environment?: {
|
|
422
|
+
enabled?: boolean | undefined;
|
|
423
|
+
enforce_contracts?: boolean | undefined;
|
|
424
|
+
tools?: Record<string, string> | undefined;
|
|
425
|
+
required_env?: string[] | undefined;
|
|
426
|
+
} | undefined;
|
|
315
427
|
}>>>;
|
|
316
428
|
output: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
317
429
|
report_path: z.ZodDefault<z.ZodString>;
|
|
@@ -321,7 +433,9 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
321
433
|
report_path?: string | undefined;
|
|
322
434
|
}>>>;
|
|
323
435
|
planned: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
|
|
436
|
+
ignore: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
|
|
324
437
|
}, "strip", z.ZodTypeAny, {
|
|
438
|
+
ignore: string[];
|
|
325
439
|
version: number;
|
|
326
440
|
commands: {
|
|
327
441
|
format?: string | undefined;
|
|
@@ -359,6 +473,18 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
359
473
|
max_files_changed_per_cycle: number;
|
|
360
474
|
protected_paths: string[];
|
|
361
475
|
};
|
|
476
|
+
context: {
|
|
477
|
+
enabled: boolean;
|
|
478
|
+
sensitivity: number;
|
|
479
|
+
mining_depth: number;
|
|
480
|
+
ignored_patterns: string[];
|
|
481
|
+
};
|
|
482
|
+
environment: {
|
|
483
|
+
enabled: boolean;
|
|
484
|
+
enforce_contracts: boolean;
|
|
485
|
+
tools: Record<string, string>;
|
|
486
|
+
required_env: string[];
|
|
487
|
+
};
|
|
362
488
|
};
|
|
363
489
|
output: {
|
|
364
490
|
report_path: string;
|
|
@@ -367,6 +493,7 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
367
493
|
preset?: string | undefined;
|
|
368
494
|
paradigm?: string | undefined;
|
|
369
495
|
}, {
|
|
496
|
+
ignore?: string[] | undefined;
|
|
370
497
|
version?: number | undefined;
|
|
371
498
|
preset?: string | undefined;
|
|
372
499
|
paradigm?: string | undefined;
|
|
@@ -406,6 +533,18 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
406
533
|
max_files_changed_per_cycle?: number | undefined;
|
|
407
534
|
protected_paths?: string[] | undefined;
|
|
408
535
|
} | undefined;
|
|
536
|
+
context?: {
|
|
537
|
+
enabled?: boolean | undefined;
|
|
538
|
+
sensitivity?: number | undefined;
|
|
539
|
+
mining_depth?: number | undefined;
|
|
540
|
+
ignored_patterns?: string[] | undefined;
|
|
541
|
+
} | undefined;
|
|
542
|
+
environment?: {
|
|
543
|
+
enabled?: boolean | undefined;
|
|
544
|
+
enforce_contracts?: boolean | undefined;
|
|
545
|
+
tools?: Record<string, string> | undefined;
|
|
546
|
+
required_env?: string[] | undefined;
|
|
547
|
+
} | undefined;
|
|
409
548
|
} | undefined;
|
|
410
549
|
output?: {
|
|
411
550
|
report_path?: string | undefined;
|
|
@@ -415,6 +554,9 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
415
554
|
export type Gates = z.infer<typeof GatesSchema>;
|
|
416
555
|
export type Commands = z.infer<typeof CommandsSchema>;
|
|
417
556
|
export type Config = z.infer<typeof ConfigSchema>;
|
|
557
|
+
export type RawGates = z.input<typeof GatesSchema>;
|
|
558
|
+
export type RawCommands = z.input<typeof CommandsSchema>;
|
|
559
|
+
export type RawConfig = z.input<typeof ConfigSchema>;
|
|
418
560
|
export declare const StatusSchema: z.ZodEnum<["PASS", "FAIL", "SKIP", "ERROR"]>;
|
|
419
561
|
export type Status = z.infer<typeof StatusSchema>;
|
|
420
562
|
export declare const FailureSchema: z.ZodObject<{
|
|
@@ -470,6 +612,10 @@ export declare const ReportSchema: z.ZodObject<{
|
|
|
470
612
|
score?: number | undefined;
|
|
471
613
|
}>;
|
|
472
614
|
}, "strip", z.ZodTypeAny, {
|
|
615
|
+
stats: {
|
|
616
|
+
duration_ms: number;
|
|
617
|
+
score?: number | undefined;
|
|
618
|
+
};
|
|
473
619
|
status: "PASS" | "FAIL" | "SKIP" | "ERROR";
|
|
474
620
|
summary: Record<string, "PASS" | "FAIL" | "SKIP" | "ERROR">;
|
|
475
621
|
failures: {
|
|
@@ -479,11 +625,11 @@ export declare const ReportSchema: z.ZodObject<{
|
|
|
479
625
|
files?: string[] | undefined;
|
|
480
626
|
hint?: string | undefined;
|
|
481
627
|
}[];
|
|
628
|
+
}, {
|
|
482
629
|
stats: {
|
|
483
630
|
duration_ms: number;
|
|
484
631
|
score?: number | undefined;
|
|
485
632
|
};
|
|
486
|
-
}, {
|
|
487
633
|
status: "PASS" | "FAIL" | "SKIP" | "ERROR";
|
|
488
634
|
summary: Record<string, "PASS" | "FAIL" | "SKIP" | "ERROR">;
|
|
489
635
|
failures: {
|
|
@@ -493,9 +639,5 @@ export declare const ReportSchema: z.ZodObject<{
|
|
|
493
639
|
files?: string[] | undefined;
|
|
494
640
|
hint?: string | undefined;
|
|
495
641
|
}[];
|
|
496
|
-
stats: {
|
|
497
|
-
duration_ms: number;
|
|
498
|
-
score?: number | undefined;
|
|
499
|
-
};
|
|
500
642
|
}>;
|
|
501
643
|
export type Report = z.infer<typeof ReportSchema>;
|
package/dist/types/index.js
CHANGED
|
@@ -34,6 +34,18 @@ export const GatesSchema = z.object({
|
|
|
34
34
|
max_files_changed_per_cycle: z.number().optional().default(10),
|
|
35
35
|
protected_paths: z.array(z.string()).optional().default(['.github/**', 'docs/**', 'rigour.yml']),
|
|
36
36
|
}).optional().default({}),
|
|
37
|
+
context: z.object({
|
|
38
|
+
enabled: z.boolean().optional().default(true),
|
|
39
|
+
sensitivity: z.number().min(0).max(1).optional().default(0.8), // 0.8 correlation threshold
|
|
40
|
+
mining_depth: z.number().optional().default(100), // Number of files to sample
|
|
41
|
+
ignored_patterns: z.array(z.string()).optional().default([]),
|
|
42
|
+
}).optional().default({}),
|
|
43
|
+
environment: z.object({
|
|
44
|
+
enabled: z.boolean().optional().default(true),
|
|
45
|
+
enforce_contracts: z.boolean().optional().default(true), // Auto-discovery of versions from truth sources
|
|
46
|
+
tools: z.record(z.string()).optional().default({}), // Explicit overrides
|
|
47
|
+
required_env: z.array(z.string()).optional().default([]),
|
|
48
|
+
}).optional().default({}),
|
|
37
49
|
});
|
|
38
50
|
export const CommandsSchema = z.object({
|
|
39
51
|
format: z.string().optional(),
|
|
@@ -51,6 +63,7 @@ export const ConfigSchema = z.object({
|
|
|
51
63
|
report_path: z.string().default('rigour-report.json'),
|
|
52
64
|
}).optional().default({}),
|
|
53
65
|
planned: z.array(z.string()).optional().default([]),
|
|
66
|
+
ignore: z.array(z.string()).optional().default([]),
|
|
54
67
|
});
|
|
55
68
|
export const StatusSchema = z.enum(['PASS', 'FAIL', 'SKIP', 'ERROR']);
|
|
56
69
|
export const FailureSchema = z.object({
|
package/dist/utils/scanner.js
CHANGED
|
@@ -12,15 +12,20 @@ export class FileScanner {
|
|
|
12
12
|
'rigour-report.json'
|
|
13
13
|
];
|
|
14
14
|
static async findFiles(options) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const patterns = (options.patterns || this.DEFAULT_PATTERNS).map(p => p.replace(/\\/g, '/'));
|
|
16
|
+
const userIgnore = options.ignore || [];
|
|
17
|
+
const ignore = [...new Set([...this.DEFAULT_IGNORE, ...userIgnore])].map(p => p.replace(/\\/g, '/'));
|
|
18
|
+
const normalizedCwd = options.cwd.replace(/\\/g, '/');
|
|
19
|
+
return globby(patterns, {
|
|
20
|
+
cwd: normalizedCwd,
|
|
21
|
+
ignore: ignore,
|
|
18
22
|
});
|
|
19
23
|
}
|
|
20
24
|
static async readFiles(cwd, files) {
|
|
21
25
|
const contents = new Map();
|
|
22
26
|
for (const file of files) {
|
|
23
|
-
const
|
|
27
|
+
const normalizedFile = file.replace(/\//g, path.sep);
|
|
28
|
+
const filePath = path.isAbsolute(normalizedFile) ? normalizedFile : path.join(cwd, normalizedFile);
|
|
24
29
|
contents.set(file, await fs.readFile(filePath, 'utf-8'));
|
|
25
30
|
}
|
|
26
31
|
return contents;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { FileScanner } from './scanner.js';
|
|
3
|
+
import { globby } from 'globby';
|
|
4
|
+
vi.mock('globby', () => ({
|
|
5
|
+
globby: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
describe('FileScanner', () => {
|
|
8
|
+
it('should merge default ignores with user ignores', async () => {
|
|
9
|
+
const options = {
|
|
10
|
+
cwd: '/test',
|
|
11
|
+
ignore: ['custom-ignore']
|
|
12
|
+
};
|
|
13
|
+
await FileScanner.findFiles(options);
|
|
14
|
+
const call = vi.mocked(globby).mock.calls[0];
|
|
15
|
+
const ignore = call[1].ignore;
|
|
16
|
+
expect(ignore).toContain('**/node_modules/**');
|
|
17
|
+
expect(ignore).toContain('custom-ignore');
|
|
18
|
+
});
|
|
19
|
+
it('should normalize paths to forward slashes', async () => {
|
|
20
|
+
const options = {
|
|
21
|
+
cwd: 'C:\\test\\path',
|
|
22
|
+
patterns: ['**\\*.ts']
|
|
23
|
+
};
|
|
24
|
+
await FileScanner.findFiles(options);
|
|
25
|
+
const call = vi.mocked(globby).mock.calls[1];
|
|
26
|
+
expect(call[0][0]).toBe('**/*.ts');
|
|
27
|
+
expect(call[1]?.cwd).toBe('C:/test/path');
|
|
28
|
+
});
|
|
29
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"fs-extra": "^11.3.3",
|
|
19
19
|
"globby": "^14.1.0",
|
|
20
20
|
"micromatch": "^4.0.8",
|
|
21
|
+
"semver": "^7.7.3",
|
|
21
22
|
"typescript": "^5.9.3",
|
|
22
23
|
"web-tree-sitter": "^0.26.3",
|
|
23
24
|
"yaml": "^2.3.4",
|
|
@@ -26,7 +27,8 @@
|
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"@types/fs-extra": "^11.0.4",
|
|
28
29
|
"@types/micromatch": "^4.0.10",
|
|
29
|
-
"@types/node": "^25.0.3"
|
|
30
|
+
"@types/node": "^25.0.3",
|
|
31
|
+
"@types/semver": "^7.7.1"
|
|
30
32
|
},
|
|
31
33
|
"scripts": {
|
|
32
34
|
"build": "tsc",
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
},
|
|
38
|
+
},
|
|
39
|
+
output: { report_path: 'rigour-report.json' }
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const runner = new GateRunner(config as any);
|
|
43
|
+
const report = await runner.run(TEST_CWD);
|
|
44
|
+
|
|
45
|
+
const driftFailures = report.failures.filter(f => f.id === 'context-drift');
|
|
46
|
+
expect(driftFailures.length).toBeGreaterThan(0);
|
|
47
|
+
expect(driftFailures[0].details).toContain('GCP_PROJECT_ID_PRODUCTION');
|
|
48
|
+
expect(driftFailures[0].hint).toContain('GCP_PROJECT_ID');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should not flag valid environment variables', async () => {
|
|
52
|
+
await fs.writeFile(path.join(TEST_CWD, 'valid.js'), `
|
|
53
|
+
const id = process.env.GCP_PROJECT_ID;
|
|
54
|
+
`);
|
|
55
|
+
|
|
56
|
+
const config = {
|
|
57
|
+
version: 1,
|
|
58
|
+
commands: {},
|
|
59
|
+
gates: {
|
|
60
|
+
context: { enabled: true },
|
|
61
|
+
},
|
|
62
|
+
output: { report_path: 'rigour-report.json' }
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const runner = new GateRunner(config as any);
|
|
66
|
+
const report = await runner.run(TEST_CWD);
|
|
67
|
+
|
|
68
|
+
const driftFailures = report.failures.filter(f => f.id === 'context-drift');
|
|
69
|
+
// Filter out failures from other files if they still exist in TEST_CWD
|
|
70
|
+
const specificFailures = driftFailures.filter(f => f.files?.includes('valid.js'));
|
|
71
|
+
expect(specificFailures.length).toBe(0);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
});
|