@rigour-labs/core 2.0.0 → 2.1.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.
@@ -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>;
@@ -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({
@@ -12,15 +12,19 @@ export class FileScanner {
12
12
  'rigour-report.json'
13
13
  ];
14
14
  static async findFiles(options) {
15
- return globby(options.patterns || this.DEFAULT_PATTERNS, {
16
- cwd: options.cwd,
17
- ignore: options.ignore || this.DEFAULT_IGNORE,
15
+ const patterns = (options.patterns || this.DEFAULT_PATTERNS).map(p => p.replace(/\\/g, '/'));
16
+ const ignore = (options.ignore || this.DEFAULT_IGNORE).map(p => p.replace(/\\/g, '/'));
17
+ const normalizedCwd = options.cwd.replace(/\\/g, '/');
18
+ return globby(patterns, {
19
+ cwd: normalizedCwd,
20
+ ignore: ignore,
18
21
  });
19
22
  }
20
23
  static async readFiles(cwd, files) {
21
24
  const contents = new Map();
22
25
  for (const file of files) {
23
- const filePath = path.join(cwd, file);
26
+ const normalizedFile = file.replace(/\//g, path.sep);
27
+ const filePath = path.isAbsolute(normalizedFile) ? normalizedFile : path.join(cwd, normalizedFile);
24
28
  contents.set(file, await fs.readFile(filePath, 'utf-8'));
25
29
  }
26
30
  return contents;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "2.0.0",
3
+ "version": "2.1.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,115 @@
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
+ });
package/src/gates/ast.ts CHANGED
@@ -21,10 +21,14 @@ export class ASTGate extends Gate {
21
21
  async run(context: GateContext): Promise<Failure[]> {
22
22
  const failures: Failure[] = [];
23
23
 
24
+ const patterns = (context.patterns || ['**/*.{ts,js,tsx,jsx,py,go,rs,cs,java,rb,c,cpp,php,swift,kt}']).map(p => p.replace(/\\/g, '/'));
25
+ const ignore = (context.ignore || ['node_modules/**', 'dist/**', 'build/**', '**/*.test.*', '**/*.spec.*', '**/__pycache__/**']).map(p => p.replace(/\\/g, '/'));
26
+ const normalizedCwd = context.cwd.replace(/\\/g, '/');
27
+
24
28
  // Find all supported files
25
- const files = await globby(['**/*.{ts,js,tsx,jsx,py,go,rs,cs,java,rb,c,cpp,php,swift,kt}'], {
26
- cwd: context.cwd,
27
- ignore: ['node_modules/**', 'dist/**', 'build/**', '**/*.test.*', '**/*.spec.*', '**/__pycache__/**'],
29
+ const files = await globby(patterns, {
30
+ cwd: normalizedCwd,
31
+ ignore: ignore,
28
32
  });
29
33
 
30
34
  for (const file of files) {
package/src/gates/base.ts CHANGED
@@ -1,7 +1,11 @@
1
+ import { GoldenRecord } from '../services/context-engine.js';
1
2
  import { Failure } from '../types/index.js';
2
3
 
3
4
  export interface GateContext {
4
5
  cwd: string;
6
+ record?: GoldenRecord;
7
+ ignore?: string[];
8
+ patterns?: string[];
5
9
  }
6
10
 
7
11
  export abstract class Gate {
@@ -19,7 +19,11 @@ export class ContentGate extends Gate {
19
19
 
20
20
  if (patterns.length === 0) return [];
21
21
 
22
- const files = await FileScanner.findFiles({ cwd: context.cwd });
22
+ const files = await FileScanner.findFiles({
23
+ cwd: context.cwd,
24
+ ignore: context.ignore,
25
+ patterns: context.patterns
26
+ });
23
27
  const contents = await FileScanner.readFiles(context.cwd, files);
24
28
 
25
29
  const violations: string[] = [];
@@ -0,0 +1,55 @@
1
+ import { Gate, GateContext } from './base.js';
2
+ import { Failure, Gates } from '../types/index.js';
3
+ import { FileScanner } from '../utils/scanner.js';
4
+ import fs from 'fs-extra';
5
+ import path from 'path';
6
+
7
+ export class ContextGate extends Gate {
8
+ constructor(private config: Gates) {
9
+ super('context-drift', 'Context Awareness & Drift Detection');
10
+ }
11
+
12
+ async run(context: GateContext): Promise<Failure[]> {
13
+ const failures: Failure[] = [];
14
+ const record = context.record;
15
+ if (!record || !this.config.context?.enabled) return [];
16
+
17
+ const files = await FileScanner.findFiles({ cwd: context.cwd });
18
+ const envAnchors = record.anchors.filter(a => a.type === 'env' && a.confidence >= 1);
19
+
20
+ for (const file of files) {
21
+ try {
22
+ const content = await fs.readFile(path.join(context.cwd, file), 'utf-8');
23
+
24
+ // 1. Detect Redundant Suffixes (The Golden Example)
25
+ this.checkEnvDrift(content, file, envAnchors, failures);
26
+
27
+ } catch (e) { }
28
+ }
29
+
30
+ return failures;
31
+ }
32
+
33
+ private checkEnvDrift(content: string, file: string, anchors: any[], failures: Failure[]) {
34
+ // Find all environment variable accesses in the content
35
+ const matches = content.matchAll(/process\.env(?:\.([A-Z0-9_]+)|\[['"]([A-Z0-9_]+)['"]\])/g);
36
+
37
+ for (const match of matches) {
38
+ const accessedVar = match[1] || match[2];
39
+
40
+ for (const anchor of anchors) {
41
+ // If the accessed variable contains the anchor but is not equal to it,
42
+ // it's a potential "invented" redundancy (e.g. CORE_URL vs CORE_URL_PROD)
43
+ if (accessedVar !== anchor.id && accessedVar.includes(anchor.id)) {
44
+ const deviation = accessedVar.replace(anchor.id, '').replace(/^_|_$/, '');
45
+
46
+ failures.push(this.createFailure(
47
+ `Context Drift: Redundant variation '${accessedVar}' detected in ${file}.`,
48
+ [file],
49
+ `The project already uses '${anchor.id}' as a standard anchor. Avoid inventing variations like '${deviation}'. Reuse the existing anchor or align with established project patterns.`
50
+ ));
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }