@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,104 @@
|
|
|
1
|
+
import { FileScanner } from '../utils/scanner.js';
|
|
2
|
+
import { Config } from '../types/index.js';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
|
|
6
|
+
export interface ProjectAnchor {
|
|
7
|
+
id: string;
|
|
8
|
+
type: 'env' | 'naming' | 'import';
|
|
9
|
+
pattern: string;
|
|
10
|
+
confidence: number;
|
|
11
|
+
occurrences: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface GoldenRecord {
|
|
15
|
+
anchors: ProjectAnchor[];
|
|
16
|
+
metadata: {
|
|
17
|
+
scannedFiles: number;
|
|
18
|
+
detectedCasing: 'camelCase' | 'snake_case' | 'PascalCase' | 'unknown';
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ContextEngine {
|
|
23
|
+
constructor(private config: Config) { }
|
|
24
|
+
|
|
25
|
+
async discover(cwd: string): Promise<GoldenRecord> {
|
|
26
|
+
const anchors: ProjectAnchor[] = [];
|
|
27
|
+
const files = await FileScanner.findFiles({
|
|
28
|
+
cwd,
|
|
29
|
+
patterns: [
|
|
30
|
+
'**/*.{ts,js,py,yaml,yml,json}',
|
|
31
|
+
'.env*',
|
|
32
|
+
'**/.env*',
|
|
33
|
+
'**/package.json',
|
|
34
|
+
'**/Dockerfile',
|
|
35
|
+
'**/*.tf'
|
|
36
|
+
]
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const limit = this.config.gates.context?.mining_depth || 100;
|
|
40
|
+
const samples = files.slice(0, limit);
|
|
41
|
+
|
|
42
|
+
const envVars = new Map<string, number>();
|
|
43
|
+
let scannedFiles = 0;
|
|
44
|
+
|
|
45
|
+
for (const file of samples) {
|
|
46
|
+
try {
|
|
47
|
+
const content = await fs.readFile(path.join(cwd, file), 'utf-8');
|
|
48
|
+
scannedFiles++;
|
|
49
|
+
this.mineEnvVars(content, file, envVars);
|
|
50
|
+
} catch (e) { }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(`[ContextEngine] Discovered ${envVars.size} env anchors`);
|
|
54
|
+
|
|
55
|
+
// Convert envVars to anchors
|
|
56
|
+
for (const [name, count] of envVars.entries()) {
|
|
57
|
+
const confidence = count >= 2 ? 1 : 0.5;
|
|
58
|
+
anchors.push({
|
|
59
|
+
id: name,
|
|
60
|
+
type: 'env',
|
|
61
|
+
pattern: name,
|
|
62
|
+
occurrences: count,
|
|
63
|
+
confidence
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
anchors,
|
|
69
|
+
metadata: {
|
|
70
|
+
scannedFiles,
|
|
71
|
+
detectedCasing: 'unknown', // TODO: Implement casing discovery
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private mineEnvVars(content: string, file: string, registry: Map<string, number>) {
|
|
77
|
+
const isAnchorSource = file.includes('.env') || file.includes('yml') || file.includes('yaml');
|
|
78
|
+
|
|
79
|
+
if (isAnchorSource) {
|
|
80
|
+
const matches = content.matchAll(/^\s*([A-Z0-9_]+)\s*=/gm);
|
|
81
|
+
for (const match of matches) {
|
|
82
|
+
// Anchors from .env count for more initially
|
|
83
|
+
registry.set(match[1], (registry.get(match[1]) || 0) + 2);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Source code matches (process.env.VAR or process.env['VAR'])
|
|
88
|
+
const tsJsMatches = content.matchAll(/process\.env(?:\.([A-Z0-9_]+)|\[['"]([A-Z0-9_]+)['"]\])/g);
|
|
89
|
+
for (const match of tsJsMatches) {
|
|
90
|
+
const name = match[1] || match[2];
|
|
91
|
+
this.incrementRegistry(registry, name);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Python matches (os.environ.get('VAR') or os.environ['VAR'])
|
|
95
|
+
const pyMatches = content.matchAll(/os\.environ(?:\.get\(|\[)['"]([A-Z0-9_]+)['"]/g);
|
|
96
|
+
for (const match of pyMatches) {
|
|
97
|
+
this.incrementRegistry(registry, match[1]);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private incrementRegistry(registry: Map<string, number>, key: string) {
|
|
102
|
+
registry.set(key, (registry.get(key) || 0) + 1);
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/templates/index.ts
CHANGED
|
@@ -190,9 +190,22 @@ export const UNIVERSAL_CONFIG: Config = {
|
|
|
190
190
|
max_files_changed_per_cycle: 10,
|
|
191
191
|
protected_paths: ['.github/**', 'docs/**', 'rigour.yml'],
|
|
192
192
|
},
|
|
193
|
+
context: {
|
|
194
|
+
enabled: true,
|
|
195
|
+
sensitivity: 0.8,
|
|
196
|
+
mining_depth: 100,
|
|
197
|
+
ignored_patterns: [],
|
|
198
|
+
},
|
|
199
|
+
environment: {
|
|
200
|
+
enabled: true,
|
|
201
|
+
enforce_contracts: true,
|
|
202
|
+
tools: {},
|
|
203
|
+
required_env: [],
|
|
204
|
+
},
|
|
193
205
|
},
|
|
194
206
|
output: {
|
|
195
207
|
report_path: 'rigour-report.json',
|
|
196
208
|
},
|
|
197
209
|
planned: [],
|
|
210
|
+
ignore: [],
|
|
198
211
|
};
|
package/src/types/index.ts
CHANGED
|
@@ -35,6 +35,18 @@ export const GatesSchema = z.object({
|
|
|
35
35
|
max_files_changed_per_cycle: z.number().optional().default(10),
|
|
36
36
|
protected_paths: z.array(z.string()).optional().default(['.github/**', 'docs/**', 'rigour.yml']),
|
|
37
37
|
}).optional().default({}),
|
|
38
|
+
context: z.object({
|
|
39
|
+
enabled: z.boolean().optional().default(true),
|
|
40
|
+
sensitivity: z.number().min(0).max(1).optional().default(0.8), // 0.8 correlation threshold
|
|
41
|
+
mining_depth: z.number().optional().default(100), // Number of files to sample
|
|
42
|
+
ignored_patterns: z.array(z.string()).optional().default([]),
|
|
43
|
+
}).optional().default({}),
|
|
44
|
+
environment: z.object({
|
|
45
|
+
enabled: z.boolean().optional().default(true),
|
|
46
|
+
enforce_contracts: z.boolean().optional().default(true), // Auto-discovery of versions from truth sources
|
|
47
|
+
tools: z.record(z.string()).optional().default({}), // Explicit overrides
|
|
48
|
+
required_env: z.array(z.string()).optional().default([]),
|
|
49
|
+
}).optional().default({}),
|
|
38
50
|
});
|
|
39
51
|
|
|
40
52
|
export const CommandsSchema = z.object({
|
|
@@ -54,12 +66,17 @@ export const ConfigSchema = z.object({
|
|
|
54
66
|
report_path: z.string().default('rigour-report.json'),
|
|
55
67
|
}).optional().default({}),
|
|
56
68
|
planned: z.array(z.string()).optional().default([]),
|
|
69
|
+
ignore: z.array(z.string()).optional().default([]),
|
|
57
70
|
});
|
|
58
71
|
|
|
59
72
|
export type Gates = z.infer<typeof GatesSchema>;
|
|
60
73
|
export type Commands = z.infer<typeof CommandsSchema>;
|
|
61
74
|
export type Config = z.infer<typeof ConfigSchema>;
|
|
62
75
|
|
|
76
|
+
export type RawGates = z.input<typeof GatesSchema>;
|
|
77
|
+
export type RawCommands = z.input<typeof CommandsSchema>;
|
|
78
|
+
export type RawConfig = z.input<typeof ConfigSchema>;
|
|
79
|
+
|
|
63
80
|
export const StatusSchema = z.enum(['PASS', 'FAIL', 'SKIP', 'ERROR']);
|
|
64
81
|
export type Status = z.infer<typeof StatusSchema>;
|
|
65
82
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { FileScanner } from './scanner.js';
|
|
3
|
+
import { globby } from 'globby';
|
|
4
|
+
|
|
5
|
+
vi.mock('globby', () => ({
|
|
6
|
+
globby: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
describe('FileScanner', () => {
|
|
10
|
+
it('should merge default ignores with user ignores', async () => {
|
|
11
|
+
const options = {
|
|
12
|
+
cwd: '/test',
|
|
13
|
+
ignore: ['custom-ignore']
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
await FileScanner.findFiles(options);
|
|
17
|
+
|
|
18
|
+
const call = vi.mocked(globby).mock.calls[0];
|
|
19
|
+
const ignore = (call[1] as any).ignore;
|
|
20
|
+
|
|
21
|
+
expect(ignore).toContain('**/node_modules/**');
|
|
22
|
+
expect(ignore).toContain('custom-ignore');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should normalize paths to forward slashes', async () => {
|
|
26
|
+
const options = {
|
|
27
|
+
cwd: 'C:\\test\\path',
|
|
28
|
+
patterns: ['**\\*.ts']
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
await FileScanner.findFiles(options);
|
|
32
|
+
|
|
33
|
+
const call = vi.mocked(globby).mock.calls[1];
|
|
34
|
+
expect(call[0][0]).toBe('**/*.ts');
|
|
35
|
+
expect(call[1]?.cwd).toBe('C:/test/path');
|
|
36
|
+
});
|
|
37
|
+
});
|
package/src/utils/scanner.ts
CHANGED
|
@@ -20,16 +20,22 @@ export class FileScanner {
|
|
|
20
20
|
];
|
|
21
21
|
|
|
22
22
|
static async findFiles(options: ScannerOptions): Promise<string[]> {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const patterns = (options.patterns || this.DEFAULT_PATTERNS).map(p => p.replace(/\\/g, '/'));
|
|
24
|
+
const userIgnore = options.ignore || [];
|
|
25
|
+
const ignore = [...new Set([...this.DEFAULT_IGNORE, ...userIgnore])].map(p => p.replace(/\\/g, '/'));
|
|
26
|
+
const normalizedCwd = options.cwd.replace(/\\/g, '/');
|
|
27
|
+
|
|
28
|
+
return globby(patterns, {
|
|
29
|
+
cwd: normalizedCwd,
|
|
30
|
+
ignore: ignore,
|
|
26
31
|
});
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
static async readFiles(cwd: string, files: string[]): Promise<Map<string, string>> {
|
|
30
35
|
const contents = new Map<string, string>();
|
|
31
36
|
for (const file of files) {
|
|
32
|
-
const
|
|
37
|
+
const normalizedFile = file.replace(/\//g, path.sep);
|
|
38
|
+
const filePath = path.isAbsolute(normalizedFile) ? normalizedFile : path.join(cwd, normalizedFile);
|
|
33
39
|
contents.set(file, await fs.readFile(filePath, 'utf-8'));
|
|
34
40
|
}
|
|
35
41
|
return contents;
|