@rigour-labs/core 1.7.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.
Files changed (62) 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 -19
  4. package/dist/environment.test.d.ts +1 -0
  5. package/dist/environment.test.js +97 -0
  6. package/dist/gates/ast-handlers/base.d.ts +12 -0
  7. package/dist/gates/ast-handlers/base.js +6 -0
  8. package/dist/gates/ast-handlers/python.d.ts +6 -0
  9. package/dist/gates/ast-handlers/python.js +64 -0
  10. package/dist/gates/ast-handlers/typescript.d.ts +9 -0
  11. package/dist/gates/ast-handlers/typescript.js +110 -0
  12. package/dist/gates/ast-handlers/universal.d.ts +8 -0
  13. package/dist/gates/ast-handlers/universal.js +156 -0
  14. package/dist/gates/ast.d.ts +1 -3
  15. package/dist/gates/ast.js +34 -110
  16. package/dist/gates/base.d.ts +4 -0
  17. package/dist/gates/base.js +1 -5
  18. package/dist/gates/content.js +9 -9
  19. package/dist/gates/context.d.ts +8 -0
  20. package/dist/gates/context.js +43 -0
  21. package/dist/gates/coverage.d.ts +8 -0
  22. package/dist/gates/coverage.js +62 -0
  23. package/dist/gates/dependency.js +7 -14
  24. package/dist/gates/environment.d.ts +8 -0
  25. package/dist/gates/environment.js +73 -0
  26. package/dist/gates/file.js +9 -9
  27. package/dist/gates/runner.d.ts +1 -1
  28. package/dist/gates/runner.js +41 -24
  29. package/dist/gates/safety.js +4 -8
  30. package/dist/gates/structure.js +6 -13
  31. package/dist/index.js +8 -26
  32. package/dist/services/context-engine.d.ts +22 -0
  33. package/dist/services/context-engine.js +78 -0
  34. package/dist/services/fix-packet-service.js +3 -7
  35. package/dist/services/state-service.js +9 -16
  36. package/dist/smoke.test.js +6 -8
  37. package/dist/templates/index.js +16 -6
  38. package/dist/types/fix-packet.js +22 -25
  39. package/dist/types/index.d.ts +151 -4
  40. package/dist/types/index.js +67 -56
  41. package/dist/utils/logger.js +8 -15
  42. package/dist/utils/scanner.js +13 -16
  43. package/package.json +6 -2
  44. package/src/context.test.ts +73 -0
  45. package/src/environment.test.ts +115 -0
  46. package/src/gates/ast-handlers/base.ts +13 -0
  47. package/src/gates/ast-handlers/python.ts +71 -0
  48. package/src/gates/ast-handlers/python_parser.py +60 -0
  49. package/src/gates/ast-handlers/typescript.ts +125 -0
  50. package/src/gates/ast-handlers/universal.ts +184 -0
  51. package/src/gates/ast.ts +32 -128
  52. package/src/gates/base.ts +4 -0
  53. package/src/gates/content.ts +5 -1
  54. package/src/gates/context.ts +55 -0
  55. package/src/gates/coverage.ts +70 -0
  56. package/src/gates/environment.ts +94 -0
  57. package/src/gates/file.ts +5 -1
  58. package/src/gates/runner.ts +27 -2
  59. package/src/services/context-engine.ts +104 -0
  60. package/src/templates/index.ts +13 -0
  61. package/src/types/index.ts +18 -0
  62. package/src/utils/scanner.ts +9 -4
@@ -0,0 +1,125 @@
1
+ import ts from 'typescript';
2
+ import { ASTHandler, ASTHandlerContext } from './base.js';
3
+ import { Failure } from '../../types/index.js';
4
+ import micromatch from 'micromatch';
5
+ import path from 'path';
6
+
7
+ export class TypeScriptHandler extends ASTHandler {
8
+ supports(file: string): boolean {
9
+ return /\.(ts|js|tsx|jsx)$/.test(file);
10
+ }
11
+
12
+ async run(context: ASTHandlerContext): Promise<Failure[]> {
13
+ const failures: Failure[] = [];
14
+ const sourceFile = ts.createSourceFile(context.file, context.content, ts.ScriptTarget.Latest, true);
15
+ this.analyzeSourceFile(sourceFile, context.file, failures);
16
+ return failures;
17
+ }
18
+
19
+ private analyzeSourceFile(sourceFile: ts.SourceFile, relativePath: string, failures: Failure[]) {
20
+ const astConfig = this.config.ast || {};
21
+ const maxComplexity = astConfig.complexity || 10;
22
+ const maxMethods = astConfig.max_methods || 10;
23
+ const maxParams = astConfig.max_params || 5;
24
+
25
+ const visit = (node: ts.Node) => {
26
+ if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isArrowFunction(node)) {
27
+ const name = this.getNodeName(node);
28
+
29
+ if (node.parameters.length > maxParams) {
30
+ failures.push({
31
+ id: 'AST_MAX_PARAMS',
32
+ title: `Function '${name}' has ${node.parameters.length} parameters (max: ${maxParams})`,
33
+ details: `High parameter count detected in ${relativePath}`,
34
+ files: [relativePath],
35
+ hint: `Reduce number of parameters or use an options object.`
36
+ });
37
+ }
38
+
39
+ let complexity = 1;
40
+ const countComplexity = (n: ts.Node) => {
41
+ if (ts.isIfStatement(n) || ts.isCaseClause(n) || ts.isDefaultClause(n) ||
42
+ ts.isForStatement(n) || ts.isForInStatement(n) || ts.isForOfStatement(n) ||
43
+ ts.isWhileStatement(n) || ts.isDoStatement(n) || ts.isConditionalExpression(n)) {
44
+ complexity++;
45
+ }
46
+ if (ts.isBinaryExpression(n)) {
47
+ if (n.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
48
+ n.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
49
+ complexity++;
50
+ }
51
+ }
52
+ ts.forEachChild(n, countComplexity);
53
+ };
54
+ ts.forEachChild(node, countComplexity);
55
+
56
+ if (complexity > maxComplexity) {
57
+ failures.push({
58
+ id: 'AST_COMPLEXITY',
59
+ title: `Function '${name}' has cyclomatic complexity of ${complexity} (max: ${maxComplexity})`,
60
+ details: `High complexity detected in ${relativePath}`,
61
+ files: [relativePath],
62
+ hint: `Refactor '${name}' into smaller, more focused functions.`
63
+ });
64
+ }
65
+ }
66
+
67
+ if (ts.isClassDeclaration(node)) {
68
+ const name = node.name?.text || 'Anonymous Class';
69
+ const methods = node.members.filter(ts.isMethodDeclaration);
70
+
71
+ if (methods.length > maxMethods) {
72
+ failures.push({
73
+ id: 'AST_MAX_METHODS',
74
+ title: `Class '${name}' has ${methods.length} methods (max: ${maxMethods})`,
75
+ details: `God Object pattern detected in ${relativePath}`,
76
+ files: [relativePath],
77
+ hint: `Class '${name}' is becoming too large. Split it into smaller services.`
78
+ });
79
+ }
80
+ }
81
+
82
+ if (ts.isImportDeclaration(node)) {
83
+ const importPath = (node.moduleSpecifier as ts.StringLiteral).text;
84
+ this.checkBoundary(importPath, relativePath, failures);
85
+ }
86
+
87
+ ts.forEachChild(node, visit);
88
+ };
89
+
90
+ ts.forEachChild(sourceFile, visit);
91
+ }
92
+
93
+ private checkBoundary(importPath: string, relativePath: string, failures: Failure[]) {
94
+ const boundaries = (this.config as any).architecture?.boundaries || [];
95
+ for (const rule of boundaries) {
96
+ if (micromatch.isMatch(relativePath, rule.from)) {
97
+ const resolved = importPath.startsWith('.')
98
+ ? path.join(path.dirname(relativePath), importPath)
99
+ : importPath;
100
+
101
+ if (rule.mode === 'deny' && micromatch.isMatch(resolved, rule.to)) {
102
+ failures.push({
103
+ id: 'ARCH_BOUNDARY',
104
+ title: `Architectural Violation`,
105
+ details: `'${relativePath}' is forbidden from importing '${importPath}' (denied by boundary rule).`,
106
+ files: [relativePath],
107
+ hint: `Remove this import to maintain architectural layering.`
108
+ });
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ private getNodeName(node: ts.Node): string {
115
+ if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
116
+ return node.name?.getText() || 'anonymous';
117
+ }
118
+ if (ts.isArrowFunction(node)) {
119
+ const parent = node.parent;
120
+ if (ts.isVariableDeclaration(parent)) return parent.name.getText();
121
+ return 'anonymous arrow';
122
+ }
123
+ return 'unknown';
124
+ }
125
+ }
@@ -0,0 +1,184 @@
1
+ import * as _Parser from 'web-tree-sitter';
2
+ const Parser = (_Parser as any).default || _Parser;
3
+ import { ASTHandler, ASTHandlerContext } from './base.js';
4
+ import { Failure } from '../../types/index.js';
5
+ import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+
10
+ interface LanguageConfig {
11
+ grammarPath: string;
12
+ extensions: string[];
13
+ queries: {
14
+ methods: string;
15
+ parameters: string;
16
+ complexity: string; // Cyclomatic
17
+ nesting: string; // For Cognitive Complexity
18
+ securitySinks?: string;
19
+ resourceLeaks?: string;
20
+ nPlusOne?: string;
21
+ ecosystemBlunders?: string;
22
+ }
23
+ }
24
+
25
+ export class UniversalASTHandler extends ASTHandler {
26
+ private parser?: any;
27
+ private languages: Record<string, LanguageConfig> = {
28
+ '.go': {
29
+ grammarPath: '../../vendor/grammars/tree-sitter-go.wasm',
30
+ extensions: ['.go'],
31
+ queries: {
32
+ complexity: '(if_statement) (for_statement) (select_statement) (case_clause)',
33
+ nesting: '(if_statement (block . (if_statement))) (for_statement (block . (for_statement)))',
34
+ parameters: '(parameter_list (parameter_declaration) @param)',
35
+ methods: '(method_declaration) @method (function_declaration) @method',
36
+ securitySinks: '(call_expression function: (selector_expression field: (field_identifier) @id (#match? @id "^(Command|exec|System)$")))',
37
+ ecosystemBlunders: '(if_statement !condition: (binary_expression left: (identifier) @err (#eq? @err "err")))' // Missing err check
38
+ }
39
+ },
40
+ '.py': {
41
+ grammarPath: '../../vendor/grammars/tree-sitter-python.wasm',
42
+ extensions: ['.py'],
43
+ queries: {
44
+ complexity: '(if_statement) (for_statement) (while_statement) (with_statement)',
45
+ nesting: '(if_statement (block (if_statement)))',
46
+ parameters: '(parameters (identifier) @param)',
47
+ methods: '(function_definition) @method',
48
+ securitySinks: '(call_expression function: (identifier) @func (#match? @func "^(eval|exec|os\\.system)$"))',
49
+ ecosystemBlunders: '(parameters (default_parameter value: (list) @mutable))' // Mutable default
50
+ }
51
+ },
52
+ '.java': {
53
+ grammarPath: '../../vendor/grammars/tree-sitter-java.wasm',
54
+ extensions: ['.java'],
55
+ queries: {
56
+ complexity: '(if_statement) (for_statement) (while_statement) (switch_label)',
57
+ nesting: '(if_statement (block (if_statement))) (for_statement (block (for_statement)))',
58
+ parameters: '(formal_parameters (formal_parameter) @param)',
59
+ methods: '(method_declaration) @method',
60
+ securitySinks: '(method_declaration (modifiers (native))) @native (method_invocation name: (identifier) @name (#match? @name "^(exec|System\\.load)$"))',
61
+ ecosystemBlunders: '(catch_clause body: (block . ))' // Empty catch
62
+ }
63
+ },
64
+ '.rs': {
65
+ grammarPath: '../../vendor/grammars/tree-sitter-rust.wasm',
66
+ extensions: ['.rs'],
67
+ queries: {
68
+ complexity: '(if_expression) (for_expression) (while_expression) (loop_expression) (match_arm)',
69
+ nesting: '(if_expression (block (if_expression))) (for_expression (block (for_expression)))',
70
+ parameters: '(parameters (parameter) @param)',
71
+ methods: '(impl_item (function_item)) @method (function_item) @method',
72
+ securitySinks: '(unsafe_block) @unsafe',
73
+ ecosystemBlunders: '(call_expression function: (field_expression field: (field_identifier) @id (#eq? @id "unwrap")))' // .unwrap()
74
+ }
75
+ },
76
+ '.cs': {
77
+ grammarPath: '../../vendor/grammars/tree-sitter-c_sharp.wasm',
78
+ extensions: ['.cs'],
79
+ queries: {
80
+ complexity: '(if_statement) (for_statement) (foreach_statement) (while_statement) (switch_section)',
81
+ nesting: '(if_statement (block (if_statement))) (for_statement (block (for_statement)))',
82
+ parameters: '(parameter_list (parameter) @param)',
83
+ methods: '(method_declaration) @method',
84
+ securitySinks: '(attribute name: (identifier) @attr (#eq? @attr "DllImport")) @violation'
85
+ }
86
+ },
87
+ '.cpp': {
88
+ grammarPath: '../../vendor/grammars/tree-sitter-cpp.wasm',
89
+ extensions: ['.cpp', '.cc', '.cxx', '.h', '.hpp'],
90
+ queries: {
91
+ complexity: '(if_statement) (for_statement) (while_statement) (case_statement)',
92
+ nesting: '(if_statement (compound_statement (if_statement)))',
93
+ parameters: '(parameter_list (parameter_declaration) @param)',
94
+ methods: '(function_definition) @method',
95
+ securitySinks: '(call_expression function: (identifier) @name (#match? @name "^(malloc|free|system|popen)$"))'
96
+ }
97
+ }
98
+ };
99
+
100
+ supports(file: string): boolean {
101
+ const ext = path.extname(file).toLowerCase();
102
+ return ext in this.languages;
103
+ }
104
+
105
+ async run(context: ASTHandlerContext): Promise<Failure[]> {
106
+ const failures: Failure[] = [];
107
+ const ext = path.extname(context.file).toLowerCase();
108
+ const config = this.languages[ext];
109
+ if (!config) return [];
110
+
111
+ if (!this.parser) {
112
+ await (Parser as any).init();
113
+ this.parser = new (Parser as any)();
114
+ }
115
+
116
+ try {
117
+ const Lang = await Parser.Language.load(path.resolve(__dirname, config.grammarPath));
118
+ this.parser.setLanguage(Lang);
119
+ const tree = this.parser.parse(context.content);
120
+ const astConfig = this.config.ast || {};
121
+
122
+ // 1. Structural Methods Audit
123
+ const methodQuery = (Lang as any).query(config.queries.methods);
124
+ const methodMatches = methodQuery.matches(tree.rootNode);
125
+
126
+ for (const match of methodMatches) {
127
+ for (const capture of match.captures) {
128
+ const node = capture.node;
129
+ const name = node.childForFieldName('name')?.text || 'anonymous';
130
+
131
+ // SME: Cognitive Complexity (Nesting depth + Cyclomatic)
132
+ const nesting = (Lang as any).query(config.queries.nesting).captures(node).length;
133
+ const cyclomatic = (Lang as any).query(config.queries.complexity).captures(node).length + 1;
134
+ const cognitive = cyclomatic + (nesting * 2);
135
+
136
+ if (cognitive > (astConfig.complexity || 10)) {
137
+ failures.push({
138
+ id: 'SME_COGNITIVE_LOAD',
139
+ title: `Method '${name}' has high cognitive load (${cognitive})`,
140
+ details: `Deeply nested or complex logic detected in ${context.file}.`,
141
+ files: [context.file],
142
+ hint: `Flatten logical branches and extract nested loops.`
143
+ });
144
+ }
145
+ }
146
+ }
147
+
148
+ // 2. Security Sinks
149
+ if (config.queries.securitySinks) {
150
+ const securityQuery = (Lang as any).query(config.queries.securitySinks);
151
+ const sinks = securityQuery.captures(tree.rootNode);
152
+ for (const capture of sinks) {
153
+ failures.push({
154
+ id: 'SME_SECURITY_SINK',
155
+ title: `Unsafe function call detected: ${capture.node.text}`,
156
+ details: `Potentially dangerous execution in ${context.file}.`,
157
+ files: [context.file],
158
+ hint: `Avoid using shell execution or eval. Use safe alternatives.`
159
+ });
160
+ }
161
+ }
162
+
163
+ // 3. Ecosystem Blunders
164
+ if (config.queries.ecosystemBlunders) {
165
+ const blunderQuery = (Lang as any).query(config.queries.ecosystemBlunders);
166
+ const blunders = blunderQuery.captures(tree.rootNode);
167
+ for (const capture of blunders) {
168
+ failures.push({
169
+ id: 'SME_BEST_PRACTICE',
170
+ title: `Ecosystem anti-pattern detected`,
171
+ details: `Violation of ${ext} best practices in ${context.file}.`,
172
+ files: [context.file],
173
+ hint: `Review language-specific best practices (e.g., error handling or mutable defaults).`
174
+ });
175
+ }
176
+ }
177
+
178
+ } catch (e) {
179
+ // Parser skip
180
+ }
181
+
182
+ return failures;
183
+ }
184
+ }
package/src/gates/ast.ts CHANGED
@@ -1,150 +1,54 @@
1
- import ts from 'typescript';
2
1
  import fs from 'fs-extra';
3
2
  import path from 'path';
4
3
  import { globby } from 'globby';
5
4
  import { Gate, GateContext } from './base.js';
6
5
  import { Failure, Gates } from '../types/index.js';
7
- import micromatch from 'micromatch';
6
+ import { ASTHandler } from './ast-handlers/base.js';
7
+ import { TypeScriptHandler } from './ast-handlers/typescript.js';
8
+ import { PythonHandler } from './ast-handlers/python.js';
9
+ import { UniversalASTHandler } from './ast-handlers/universal.js';
8
10
 
9
11
  export class ASTGate extends Gate {
12
+ private handlers: ASTHandler[] = [];
13
+
10
14
  constructor(private config: Gates) {
11
15
  super('ast-analysis', 'AST Structural Analysis');
16
+ this.handlers.push(new TypeScriptHandler(config));
17
+ this.handlers.push(new PythonHandler(config));
18
+ this.handlers.push(new UniversalASTHandler(config));
12
19
  }
13
20
 
14
21
  async run(context: GateContext): Promise<Failure[]> {
15
22
  const failures: Failure[] = [];
16
- const files = await globby(['**/*.{ts,js,tsx,jsx}'], {
17
- cwd: context.cwd,
18
- ignore: ['node_modules/**', 'dist/**', 'build/**', '**/*.test.*', '**/*.spec.*'],
19
- });
20
-
21
- for (const file of files) {
22
- const fullPath = path.join(context.cwd, file);
23
- const content = await fs.readFile(fullPath, 'utf-8');
24
- const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
25
-
26
- this.analyzeSourceFile(sourceFile, file, failures);
27
- }
28
-
29
- return failures;
30
- }
31
-
32
- private analyzeSourceFile(sourceFile: ts.SourceFile, relativePath: string, failures: Failure[]) {
33
- const astConfig = this.config.ast || {};
34
- const maxComplexity = astConfig.complexity || 10;
35
- const maxMethods = astConfig.max_methods || 10;
36
- const maxParams = astConfig.max_params || 5;
37
-
38
- const visit = (node: ts.Node) => {
39
- // 1. Complexity & Params for functions
40
- if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isArrowFunction(node)) {
41
- const name = this.getNodeName(node);
42
-
43
- // Parameter count
44
- if (node.parameters.length > maxParams) {
45
- failures.push(this.createFailure(
46
- `Function '${name}' has ${node.parameters.length} parameters (max: ${maxParams})`,
47
- [relativePath],
48
- `Reduce number of parameters or use an options object.`
49
- ));
50
- // Update: Failures in Runner will be mapped to FixPacket
51
- (failures[failures.length - 1] as any).metrics = { count: node.parameters.length, max: maxParams };
52
- }
53
-
54
- // Cyclomatic Complexity (Simplified: nodes that cause branching)
55
- let complexity = 1;
56
- const countComplexity = (n: ts.Node) => {
57
- if (ts.isIfStatement(n) || ts.isCaseClause(n) || ts.isDefaultClause(n) ||
58
- ts.isForStatement(n) || ts.isForInStatement(n) || ts.isForOfStatement(n) ||
59
- ts.isWhileStatement(n) || ts.isDoStatement(n) || ts.isConditionalExpression(n)) {
60
- complexity++;
61
- }
62
- if (ts.isBinaryExpression(n)) {
63
- if (n.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
64
- n.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
65
- complexity++;
66
- }
67
- }
68
- ts.forEachChild(n, countComplexity);
69
- };
70
- ts.forEachChild(node, countComplexity);
71
-
72
- if (complexity > maxComplexity) {
73
- failures.push(this.createFailure(
74
- `Function '${name}' has cyclomatic complexity of ${complexity} (max: ${maxComplexity})`,
75
- [relativePath],
76
- `Refactor '${name}' into smaller, more focused functions.`
77
- ));
78
- (failures[failures.length - 1] as any).metrics = { complexity, max: maxComplexity };
79
- }
80
- }
81
23
 
82
- // 2. Class metrics
83
- if (ts.isClassDeclaration(node)) {
84
- const name = node.name?.text || 'Anonymous Class';
85
- const methods = node.members.filter(ts.isMethodDeclaration);
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, '/');
86
27
 
87
- if (methods.length > maxMethods) {
88
- failures.push(this.createFailure(
89
- `Class '${name}' has ${methods.length} methods (max: ${maxMethods})`,
90
- [relativePath],
91
- `Class '${name}' is becoming a 'God Object'. Split it into smaller services.`
92
- ));
93
- (failures[failures.length - 1] as any).metrics = { methodCount: methods.length, max: maxMethods };
94
- }
95
- }
96
-
97
- // 3. Import check for Layer Boundaries
98
- if (ts.isImportDeclaration(node)) {
99
- const importPath = (node.moduleSpecifier as ts.StringLiteral).text;
100
- this.checkBoundary(importPath, relativePath, failures);
101
- }
102
-
103
- ts.forEachChild(node, visit);
104
- };
105
-
106
- ts.forEachChild(sourceFile, visit);
107
- }
108
-
109
- private checkBoundary(importPath: string, relativePath: string, failures: Failure[]) {
110
- const boundaries = (this.config as any).architecture?.boundaries || [];
111
- if (boundaries.length === 0) return;
112
-
113
- for (const rule of boundaries) {
114
- const isFromMatch = micromatch.isMatch(relativePath, rule.from);
115
- if (isFromMatch) {
116
- // Approximate resolution (simplified for now)
117
- // Real implementation would need to handle alias and absolute path resolution
118
- const resolved = importPath.startsWith('.')
119
- ? path.join(path.dirname(relativePath), importPath)
120
- : importPath;
28
+ // Find all supported files
29
+ const files = await globby(patterns, {
30
+ cwd: normalizedCwd,
31
+ ignore: ignore,
32
+ });
121
33
 
122
- const isToMatch = micromatch.isMatch(resolved, rule.to);
34
+ for (const file of files) {
35
+ const handler = this.handlers.find(h => h.supports(file));
36
+ if (!handler) continue;
123
37
 
124
- if (rule.mode === 'deny' && isToMatch) {
125
- failures.push(this.createFailure(
126
- `Architectural Violation: '${relativePath}' is forbidden from importing '${importPath}' (denied by boundary rule).`,
127
- [relativePath],
128
- `Remove this import to maintain architectural layering.`
129
- ));
130
- } else if (rule.mode === 'allow' && !isToMatch && importPath.startsWith('.')) {
131
- // Complexity: Allow rules are trickier to implement strictly without full resolution
132
- }
38
+ const fullPath = path.join(context.cwd, file);
39
+ try {
40
+ const content = await fs.readFile(fullPath, 'utf-8');
41
+ const gateFailures = await handler.run({
42
+ cwd: context.cwd,
43
+ file: file,
44
+ content
45
+ });
46
+ failures.push(...gateFailures);
47
+ } catch (error: any) {
48
+ // Individual file read failures shouldn't crash the whole run
133
49
  }
134
50
  }
135
- }
136
51
 
137
- private getNodeName(node: ts.Node): string {
138
- if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
139
- return node.name?.getText() || 'anonymous';
140
- }
141
- if (ts.isArrowFunction(node)) {
142
- const parent = node.parent;
143
- if (ts.isVariableDeclaration(parent)) {
144
- return parent.name.getText();
145
- }
146
- return 'anonymous arrow';
147
- }
148
- return 'unknown';
52
+ return failures;
149
53
  }
150
54
  }
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
+ }
@@ -0,0 +1,70 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { Gate, GateContext } from './base.js';
4
+ import { Failure, Gates } from '../types/index.js';
5
+ import { globby } from 'globby';
6
+
7
+ export class CoverageGate extends Gate {
8
+ constructor(private config: Gates) {
9
+ super('coverage-guard', 'Dynamic Coverage Guard');
10
+ }
11
+
12
+ async run(context: GateContext): Promise<Failure[]> {
13
+ const failures: Failure[] = [];
14
+
15
+ // 1. Locate coverage report (lcov.info is standard)
16
+ const reports = await globby(['**/lcov.info', '**/coverage-final.json'], {
17
+ cwd: context.cwd,
18
+ ignore: ['node_modules/**']
19
+ });
20
+
21
+ if (reports.length === 0) {
22
+ // If no reports found, and coverage is required, we could flag it.
23
+ // But for now, we'll just skip silently if not configured.
24
+ return [];
25
+ }
26
+
27
+ // 2. Parse coverage (Simplified LCOV parser for demonstration)
28
+ const coverageData = await this.parseLcov(path.join(context.cwd, reports[0]));
29
+
30
+ // 3. Quality Handshake: SME SME LOGIC
31
+ // We look for files that have high complexity but low coverage.
32
+ // In a real implementation, we would share data between ASTGate and CoverageGate.
33
+ // For this demo, we'll implement a standalone check.
34
+
35
+ for (const [file, stats] of Object.entries(coverageData)) {
36
+ const coverage = (stats.hit / stats.found) * 100;
37
+ const threshold = stats.isComplex ? 80 : 50; // SME logic: Complex files need higher coverage
38
+
39
+ if (coverage < threshold) {
40
+ failures.push({
41
+ id: 'DYNAMIC_COVERAGE_LOW',
42
+ title: `Low coverage for high-risk file: ${file}`,
43
+ details: `Current coverage: ${coverage.toFixed(2)}%. Required: ${threshold}% due to structural risk.`,
44
+ files: [file],
45
+ hint: `Add dynamic tests to cover complex logical branches in this file.`
46
+ });
47
+ }
48
+ }
49
+
50
+ return failures;
51
+ }
52
+
53
+ private async parseLcov(reportPath: string): Promise<Record<string, { found: number, hit: number, isComplex: boolean }>> {
54
+ const content = await fs.readFile(reportPath, 'utf-8');
55
+ const results: Record<string, { found: number, hit: number, isComplex: boolean }> = {};
56
+ let currentFile = '';
57
+
58
+ for (const line of content.split('\n')) {
59
+ if (line.startsWith('SF:')) {
60
+ currentFile = line.substring(3);
61
+ results[currentFile] = { found: 0, hit: 0, isComplex: false };
62
+ } else if (line.startsWith('LF:')) {
63
+ results[currentFile].found = parseInt(line.substring(3));
64
+ } else if (line.startsWith('LH:')) {
65
+ results[currentFile].hit = parseInt(line.substring(3));
66
+ }
67
+ }
68
+ return results;
69
+ }
70
+ }