@rigour-labs/core 1.6.0 → 2.0.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 (40) hide show
  1. package/dist/discovery.js +12 -19
  2. package/dist/gates/ast-handlers/base.d.ts +12 -0
  3. package/dist/gates/ast-handlers/base.js +6 -0
  4. package/dist/gates/ast-handlers/python.d.ts +6 -0
  5. package/dist/gates/ast-handlers/python.js +64 -0
  6. package/dist/gates/ast-handlers/typescript.d.ts +9 -0
  7. package/dist/gates/ast-handlers/typescript.js +110 -0
  8. package/dist/gates/ast-handlers/universal.d.ts +8 -0
  9. package/dist/gates/ast-handlers/universal.js +156 -0
  10. package/dist/gates/ast.d.ts +1 -3
  11. package/dist/gates/ast.js +30 -109
  12. package/dist/gates/base.js +1 -5
  13. package/dist/gates/content.js +5 -9
  14. package/dist/gates/coverage.d.ts +8 -0
  15. package/dist/gates/coverage.js +62 -0
  16. package/dist/gates/dependency.js +7 -14
  17. package/dist/gates/file.js +5 -9
  18. package/dist/gates/runner.js +22 -22
  19. package/dist/gates/safety.js +4 -8
  20. package/dist/gates/structure.js +6 -13
  21. package/dist/index.js +8 -26
  22. package/dist/services/fix-packet-service.js +3 -7
  23. package/dist/services/state-service.js +9 -16
  24. package/dist/smoke.test.js +6 -8
  25. package/dist/templates/index.js +3 -6
  26. package/dist/types/fix-packet.js +22 -25
  27. package/dist/types/index.d.ts +5 -0
  28. package/dist/types/index.js +54 -56
  29. package/dist/utils/logger.js +8 -15
  30. package/dist/utils/scanner.js +7 -14
  31. package/package.json +3 -1
  32. package/src/gates/ast-handlers/base.ts +13 -0
  33. package/src/gates/ast-handlers/python.ts +71 -0
  34. package/src/gates/ast-handlers/python_parser.py +60 -0
  35. package/src/gates/ast-handlers/typescript.ts +125 -0
  36. package/src/gates/ast-handlers/universal.ts +184 -0
  37. package/src/gates/ast.ts +27 -127
  38. package/src/gates/coverage.ts +70 -0
  39. package/src/gates/runner.ts +4 -0
  40. package/src/types/index.ts +1 -0
@@ -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,50 @@
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}'], {
23
+
24
+ // Find all supported files
25
+ const files = await globby(['**/*.{ts,js,tsx,jsx,py,go,rs,cs,java,rb,c,cpp,php,swift,kt}'], {
17
26
  cwd: context.cwd,
18
- ignore: ['node_modules/**', 'dist/**', 'build/**', '**/*.test.*', '**/*.spec.*'],
27
+ ignore: ['node_modules/**', 'dist/**', 'build/**', '**/*.test.*', '**/*.spec.*', '**/__pycache__/**'],
19
28
  });
20
29
 
21
30
  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
+ const handler = this.handlers.find(h => h.supports(file));
32
+ if (!handler) continue;
31
33
 
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
-
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);
86
-
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;
121
-
122
- const isToMatch = micromatch.isMatch(resolved, rule.to);
123
-
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
- }
34
+ const fullPath = path.join(context.cwd, file);
35
+ try {
36
+ const content = await fs.readFile(fullPath, 'utf-8');
37
+ const gateFailures = await handler.run({
38
+ cwd: context.cwd,
39
+ file: file,
40
+ content
41
+ });
42
+ failures.push(...gateFailures);
43
+ } catch (error: any) {
44
+ // Individual file read failures shouldn't crash the whole run
133
45
  }
134
46
  }
135
- }
136
47
 
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';
48
+ return failures;
149
49
  }
150
50
  }
@@ -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
+ }
@@ -6,6 +6,7 @@ import { StructureGate } from './structure.js';
6
6
  import { ASTGate } from './ast.js';
7
7
  import { SafetyGate } from './safety.js';
8
8
  import { DependencyGate } from './dependency.js';
9
+ import { CoverageGate } from './coverage.js';
9
10
  import { execa } from 'execa';
10
11
  import { Logger } from '../utils/logger.js';
11
12
 
@@ -32,6 +33,7 @@ export class GateRunner {
32
33
  this.gates.push(new ASTGate(this.config.gates));
33
34
  this.gates.push(new DependencyGate(this.config));
34
35
  this.gates.push(new SafetyGate(this.config.gates));
36
+ this.gates.push(new CoverageGate(this.config.gates));
35
37
  }
36
38
 
37
39
  /**
@@ -94,6 +96,7 @@ export class GateRunner {
94
96
  }
95
97
 
96
98
  const status: Status = failures.length > 0 ? 'FAIL' : 'PASS';
99
+ const score = Math.max(0, 100 - (failures.length * 5)); // Basic SME scoring logic
97
100
 
98
101
  return {
99
102
  status,
@@ -101,6 +104,7 @@ export class GateRunner {
101
104
  failures,
102
105
  stats: {
103
106
  duration_ms: Date.now() - start,
107
+ score,
104
108
  },
105
109
  };
106
110
  }
@@ -78,6 +78,7 @@ export const ReportSchema = z.object({
78
78
  failures: z.array(FailureSchema),
79
79
  stats: z.object({
80
80
  duration_ms: z.number(),
81
+ score: z.number().optional(),
81
82
  }),
82
83
  });
83
84
  export type Report = z.infer<typeof ReportSchema>;