@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.
Files changed (54) hide show
  1. package/dist/context.test.d.ts +1 -0
  2. package/dist/context.test.js +61 -0
  3. package/dist/discovery.js +12 -7
  4. package/dist/discovery.test.d.ts +1 -0
  5. package/dist/discovery.test.js +57 -0
  6. package/dist/environment.test.d.ts +1 -0
  7. package/dist/environment.test.js +97 -0
  8. package/dist/gates/ast-handlers/python.js +15 -1
  9. package/dist/gates/ast.js +6 -3
  10. package/dist/gates/base.d.ts +5 -1
  11. package/dist/gates/base.js +2 -2
  12. package/dist/gates/content.js +5 -1
  13. package/dist/gates/context.d.ts +8 -0
  14. package/dist/gates/context.js +43 -0
  15. package/dist/gates/coverage.js +6 -1
  16. package/dist/gates/dependency.js +40 -1
  17. package/dist/gates/environment.d.ts +8 -0
  18. package/dist/gates/environment.js +73 -0
  19. package/dist/gates/file.js +5 -1
  20. package/dist/gates/runner.d.ts +1 -1
  21. package/dist/gates/runner.js +19 -2
  22. package/dist/gates/safety.js +9 -3
  23. package/dist/safety.test.d.ts +1 -0
  24. package/dist/safety.test.js +42 -0
  25. package/dist/services/context-engine.d.ts +22 -0
  26. package/dist/services/context-engine.js +78 -0
  27. package/dist/templates/index.js +13 -0
  28. package/dist/types/index.d.ts +147 -5
  29. package/dist/types/index.js +13 -0
  30. package/dist/utils/scanner.js +9 -4
  31. package/dist/utils/scanner.test.d.ts +1 -0
  32. package/dist/utils/scanner.test.js +29 -0
  33. package/package.json +4 -2
  34. package/src/context.test.ts +73 -0
  35. package/src/discovery.test.ts +61 -0
  36. package/src/discovery.ts +11 -6
  37. package/src/environment.test.ts +115 -0
  38. package/src/gates/ast-handlers/python.ts +14 -1
  39. package/src/gates/ast.ts +7 -3
  40. package/src/gates/base.ts +6 -2
  41. package/src/gates/content.ts +5 -1
  42. package/src/gates/context.ts +55 -0
  43. package/src/gates/coverage.ts +5 -1
  44. package/src/gates/dependency.ts +65 -1
  45. package/src/gates/environment.ts +94 -0
  46. package/src/gates/file.ts +5 -1
  47. package/src/gates/runner.ts +23 -2
  48. package/src/gates/safety.ts +11 -4
  49. package/src/safety.test.ts +53 -0
  50. package/src/services/context-engine.ts +104 -0
  51. package/src/templates/index.ts +13 -0
  52. package/src/types/index.ts +17 -0
  53. package/src/utils/scanner.test.ts +37 -0
  54. 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
+ }
@@ -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
  };
@@ -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
+ });
@@ -20,16 +20,22 @@ export class FileScanner {
20
20
  ];
21
21
 
22
22
  static async findFiles(options: ScannerOptions): Promise<string[]> {
23
- return globby(options.patterns || this.DEFAULT_PATTERNS, {
24
- cwd: options.cwd,
25
- ignore: options.ignore || this.DEFAULT_IGNORE,
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 filePath = path.join(cwd, file);
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;