@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.
- package/dist/discovery.js +12 -19
- package/dist/gates/ast-handlers/base.d.ts +12 -0
- package/dist/gates/ast-handlers/base.js +6 -0
- package/dist/gates/ast-handlers/python.d.ts +6 -0
- package/dist/gates/ast-handlers/python.js +64 -0
- package/dist/gates/ast-handlers/typescript.d.ts +9 -0
- package/dist/gates/ast-handlers/typescript.js +110 -0
- package/dist/gates/ast-handlers/universal.d.ts +8 -0
- package/dist/gates/ast-handlers/universal.js +156 -0
- package/dist/gates/ast.d.ts +1 -3
- package/dist/gates/ast.js +30 -109
- package/dist/gates/base.js +1 -5
- package/dist/gates/content.js +5 -9
- package/dist/gates/coverage.d.ts +8 -0
- package/dist/gates/coverage.js +62 -0
- package/dist/gates/dependency.js +7 -14
- package/dist/gates/file.js +5 -9
- package/dist/gates/runner.js +22 -22
- package/dist/gates/safety.js +4 -8
- package/dist/gates/structure.js +6 -13
- package/dist/index.js +8 -26
- package/dist/services/fix-packet-service.js +3 -7
- package/dist/services/state-service.js +9 -16
- package/dist/smoke.test.js +6 -8
- package/dist/templates/index.js +3 -6
- package/dist/types/fix-packet.js +22 -25
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +54 -56
- package/dist/utils/logger.js +8 -15
- package/dist/utils/scanner.js +7 -14
- package/package.json +3 -1
- package/src/gates/ast-handlers/base.ts +13 -0
- package/src/gates/ast-handlers/python.ts +71 -0
- package/src/gates/ast-handlers/python_parser.py +60 -0
- package/src/gates/ast-handlers/typescript.ts +125 -0
- package/src/gates/ast-handlers/universal.ts +184 -0
- package/src/gates/ast.ts +27 -127
- package/src/gates/coverage.ts +70 -0
- package/src/gates/runner.ts +4 -0
- 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
|
|
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
|
-
|
|
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
|
|
23
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/gates/runner.ts
CHANGED
|
@@ -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
|
}
|