@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.
- package/dist/context.test.d.ts +1 -0
- package/dist/context.test.js +61 -0
- package/dist/discovery.js +12 -19
- package/dist/environment.test.d.ts +1 -0
- package/dist/environment.test.js +97 -0
- 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 +34 -110
- package/dist/gates/base.d.ts +4 -0
- package/dist/gates/base.js +1 -5
- package/dist/gates/content.js +9 -9
- package/dist/gates/context.d.ts +8 -0
- package/dist/gates/context.js +43 -0
- 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/environment.d.ts +8 -0
- package/dist/gates/environment.js +73 -0
- package/dist/gates/file.js +9 -9
- package/dist/gates/runner.d.ts +1 -1
- package/dist/gates/runner.js +41 -24
- package/dist/gates/safety.js +4 -8
- package/dist/gates/structure.js +6 -13
- package/dist/index.js +8 -26
- package/dist/services/context-engine.d.ts +22 -0
- package/dist/services/context-engine.js +78 -0
- 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 +16 -6
- package/dist/types/fix-packet.js +22 -25
- package/dist/types/index.d.ts +151 -4
- package/dist/types/index.js +67 -56
- package/dist/utils/logger.js +8 -15
- package/dist/utils/scanner.js +13 -16
- package/package.json +6 -2
- package/src/context.test.ts +73 -0
- package/src/environment.test.ts +115 -0
- 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 +32 -128
- package/src/gates/base.ts +4 -0
- package/src/gates/content.ts +5 -1
- package/src/gates/context.ts +55 -0
- package/src/gates/coverage.ts +70 -0
- package/src/gates/environment.ts +94 -0
- package/src/gates/file.ts +5 -1
- package/src/gates/runner.ts +27 -2
- package/src/services/context-engine.ts +104 -0
- package/src/templates/index.ts +13 -0
- package/src/types/index.ts +18 -0
- package/src/utils/scanner.ts +9 -4
package/dist/gates/ast.js
CHANGED
|
@@ -1,124 +1,48 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const path_1 = __importDefault(require("path"));
|
|
10
|
-
const globby_1 = require("globby");
|
|
11
|
-
const base_js_1 = require("./base.js");
|
|
12
|
-
const micromatch_1 = __importDefault(require("micromatch"));
|
|
13
|
-
class ASTGate extends base_js_1.Gate {
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { globby } from 'globby';
|
|
4
|
+
import { Gate } from './base.js';
|
|
5
|
+
import { TypeScriptHandler } from './ast-handlers/typescript.js';
|
|
6
|
+
import { PythonHandler } from './ast-handlers/python.js';
|
|
7
|
+
import { UniversalASTHandler } from './ast-handlers/universal.js';
|
|
8
|
+
export class ASTGate extends Gate {
|
|
14
9
|
config;
|
|
10
|
+
handlers = [];
|
|
15
11
|
constructor(config) {
|
|
16
12
|
super('ast-analysis', 'AST Structural Analysis');
|
|
17
13
|
this.config = config;
|
|
14
|
+
this.handlers.push(new TypeScriptHandler(config));
|
|
15
|
+
this.handlers.push(new PythonHandler(config));
|
|
16
|
+
this.handlers.push(new UniversalASTHandler(config));
|
|
18
17
|
}
|
|
19
18
|
async run(context) {
|
|
20
19
|
const failures = [];
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const patterns = (context.patterns || ['**/*.{ts,js,tsx,jsx,py,go,rs,cs,java,rb,c,cpp,php,swift,kt}']).map(p => p.replace(/\\/g, '/'));
|
|
21
|
+
const ignore = (context.ignore || ['node_modules/**', 'dist/**', 'build/**', '**/*.test.*', '**/*.spec.*', '**/__pycache__/**']).map(p => p.replace(/\\/g, '/'));
|
|
22
|
+
const normalizedCwd = context.cwd.replace(/\\/g, '/');
|
|
23
|
+
// Find all supported files
|
|
24
|
+
const files = await globby(patterns, {
|
|
25
|
+
cwd: normalizedCwd,
|
|
26
|
+
ignore: ignore,
|
|
24
27
|
});
|
|
25
28
|
for (const file of files) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const visit = (node) => {
|
|
39
|
-
// 1. Complexity & Params for functions
|
|
40
|
-
if (typescript_1.default.isFunctionDeclaration(node) || typescript_1.default.isMethodDeclaration(node) || typescript_1.default.isArrowFunction(node)) {
|
|
41
|
-
const name = this.getNodeName(node);
|
|
42
|
-
// Parameter count
|
|
43
|
-
if (node.parameters.length > maxParams) {
|
|
44
|
-
failures.push(this.createFailure(`Function '${name}' has ${node.parameters.length} parameters (max: ${maxParams})`, [relativePath], `Reduce number of parameters or use an options object.`));
|
|
45
|
-
// Update: Failures in Runner will be mapped to FixPacket
|
|
46
|
-
failures[failures.length - 1].metrics = { count: node.parameters.length, max: maxParams };
|
|
47
|
-
}
|
|
48
|
-
// Cyclomatic Complexity (Simplified: nodes that cause branching)
|
|
49
|
-
let complexity = 1;
|
|
50
|
-
const countComplexity = (n) => {
|
|
51
|
-
if (typescript_1.default.isIfStatement(n) || typescript_1.default.isCaseClause(n) || typescript_1.default.isDefaultClause(n) ||
|
|
52
|
-
typescript_1.default.isForStatement(n) || typescript_1.default.isForInStatement(n) || typescript_1.default.isForOfStatement(n) ||
|
|
53
|
-
typescript_1.default.isWhileStatement(n) || typescript_1.default.isDoStatement(n) || typescript_1.default.isConditionalExpression(n)) {
|
|
54
|
-
complexity++;
|
|
55
|
-
}
|
|
56
|
-
if (typescript_1.default.isBinaryExpression(n)) {
|
|
57
|
-
if (n.operatorToken.kind === typescript_1.default.SyntaxKind.AmpersandAmpersandToken ||
|
|
58
|
-
n.operatorToken.kind === typescript_1.default.SyntaxKind.BarBarToken) {
|
|
59
|
-
complexity++;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
typescript_1.default.forEachChild(n, countComplexity);
|
|
63
|
-
};
|
|
64
|
-
typescript_1.default.forEachChild(node, countComplexity);
|
|
65
|
-
if (complexity > maxComplexity) {
|
|
66
|
-
failures.push(this.createFailure(`Function '${name}' has cyclomatic complexity of ${complexity} (max: ${maxComplexity})`, [relativePath], `Refactor '${name}' into smaller, more focused functions.`));
|
|
67
|
-
failures[failures.length - 1].metrics = { complexity, max: maxComplexity };
|
|
68
|
-
}
|
|
29
|
+
const handler = this.handlers.find(h => h.supports(file));
|
|
30
|
+
if (!handler)
|
|
31
|
+
continue;
|
|
32
|
+
const fullPath = path.join(context.cwd, file);
|
|
33
|
+
try {
|
|
34
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
35
|
+
const gateFailures = await handler.run({
|
|
36
|
+
cwd: context.cwd,
|
|
37
|
+
file: file,
|
|
38
|
+
content
|
|
39
|
+
});
|
|
40
|
+
failures.push(...gateFailures);
|
|
69
41
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const name = node.name?.text || 'Anonymous Class';
|
|
73
|
-
const methods = node.members.filter(typescript_1.default.isMethodDeclaration);
|
|
74
|
-
if (methods.length > maxMethods) {
|
|
75
|
-
failures.push(this.createFailure(`Class '${name}' has ${methods.length} methods (max: ${maxMethods})`, [relativePath], `Class '${name}' is becoming a 'God Object'. Split it into smaller services.`));
|
|
76
|
-
failures[failures.length - 1].metrics = { methodCount: methods.length, max: maxMethods };
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
// 3. Import check for Layer Boundaries
|
|
80
|
-
if (typescript_1.default.isImportDeclaration(node)) {
|
|
81
|
-
const importPath = node.moduleSpecifier.text;
|
|
82
|
-
this.checkBoundary(importPath, relativePath, failures);
|
|
83
|
-
}
|
|
84
|
-
typescript_1.default.forEachChild(node, visit);
|
|
85
|
-
};
|
|
86
|
-
typescript_1.default.forEachChild(sourceFile, visit);
|
|
87
|
-
}
|
|
88
|
-
checkBoundary(importPath, relativePath, failures) {
|
|
89
|
-
const boundaries = this.config.architecture?.boundaries || [];
|
|
90
|
-
if (boundaries.length === 0)
|
|
91
|
-
return;
|
|
92
|
-
for (const rule of boundaries) {
|
|
93
|
-
const isFromMatch = micromatch_1.default.isMatch(relativePath, rule.from);
|
|
94
|
-
if (isFromMatch) {
|
|
95
|
-
// Approximate resolution (simplified for now)
|
|
96
|
-
// Real implementation would need to handle alias and absolute path resolution
|
|
97
|
-
const resolved = importPath.startsWith('.')
|
|
98
|
-
? path_1.default.join(path_1.default.dirname(relativePath), importPath)
|
|
99
|
-
: importPath;
|
|
100
|
-
const isToMatch = micromatch_1.default.isMatch(resolved, rule.to);
|
|
101
|
-
if (rule.mode === 'deny' && isToMatch) {
|
|
102
|
-
failures.push(this.createFailure(`Architectural Violation: '${relativePath}' is forbidden from importing '${importPath}' (denied by boundary rule).`, [relativePath], `Remove this import to maintain architectural layering.`));
|
|
103
|
-
}
|
|
104
|
-
else if (rule.mode === 'allow' && !isToMatch && importPath.startsWith('.')) {
|
|
105
|
-
// Complexity: Allow rules are trickier to implement strictly without full resolution
|
|
106
|
-
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
// Individual file read failures shouldn't crash the whole run
|
|
107
44
|
}
|
|
108
45
|
}
|
|
109
|
-
|
|
110
|
-
getNodeName(node) {
|
|
111
|
-
if (typescript_1.default.isFunctionDeclaration(node) || typescript_1.default.isMethodDeclaration(node)) {
|
|
112
|
-
return node.name?.getText() || 'anonymous';
|
|
113
|
-
}
|
|
114
|
-
if (typescript_1.default.isArrowFunction(node)) {
|
|
115
|
-
const parent = node.parent;
|
|
116
|
-
if (typescript_1.default.isVariableDeclaration(parent)) {
|
|
117
|
-
return parent.name.getText();
|
|
118
|
-
}
|
|
119
|
-
return 'anonymous arrow';
|
|
120
|
-
}
|
|
121
|
-
return 'unknown';
|
|
46
|
+
return failures;
|
|
122
47
|
}
|
|
123
48
|
}
|
|
124
|
-
exports.ASTGate = ASTGate;
|
package/dist/gates/base.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { GoldenRecord } from '../services/context-engine.js';
|
|
1
2
|
import { Failure } from '../types/index.js';
|
|
2
3
|
export interface GateContext {
|
|
3
4
|
cwd: string;
|
|
5
|
+
record?: GoldenRecord;
|
|
6
|
+
ignore?: string[];
|
|
7
|
+
patterns?: string[];
|
|
4
8
|
}
|
|
5
9
|
export declare abstract class Gate {
|
|
6
10
|
readonly id: string;
|
package/dist/gates/base.js
CHANGED
package/dist/gates/content.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const base_js_1 = require("./base.js");
|
|
5
|
-
const scanner_js_1 = require("../utils/scanner.js");
|
|
6
|
-
class ContentGate extends base_js_1.Gate {
|
|
1
|
+
import { Gate } from './base.js';
|
|
2
|
+
import { FileScanner } from '../utils/scanner.js';
|
|
3
|
+
export class ContentGate extends Gate {
|
|
7
4
|
config;
|
|
8
5
|
constructor(config) {
|
|
9
6
|
super('content-check', 'Forbidden Content');
|
|
@@ -17,8 +14,12 @@ class ContentGate extends base_js_1.Gate {
|
|
|
17
14
|
patterns.push(/FIXME/i);
|
|
18
15
|
if (patterns.length === 0)
|
|
19
16
|
return [];
|
|
20
|
-
const files = await
|
|
21
|
-
|
|
17
|
+
const files = await FileScanner.findFiles({
|
|
18
|
+
cwd: context.cwd,
|
|
19
|
+
ignore: context.ignore,
|
|
20
|
+
patterns: context.patterns
|
|
21
|
+
});
|
|
22
|
+
const contents = await FileScanner.readFiles(context.cwd, files);
|
|
22
23
|
const violations = [];
|
|
23
24
|
for (const [file, content] of contents) {
|
|
24
25
|
for (const pattern of patterns) {
|
|
@@ -36,4 +37,3 @@ class ContentGate extends base_js_1.Gate {
|
|
|
36
37
|
return [];
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
|
-
exports.ContentGate = ContentGate;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Gate, GateContext } from './base.js';
|
|
2
|
+
import { Failure, Gates } from '../types/index.js';
|
|
3
|
+
export declare class ContextGate extends Gate {
|
|
4
|
+
private config;
|
|
5
|
+
constructor(config: Gates);
|
|
6
|
+
run(context: GateContext): Promise<Failure[]>;
|
|
7
|
+
private checkEnvDrift;
|
|
8
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Gate } from './base.js';
|
|
2
|
+
import { FileScanner } from '../utils/scanner.js';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
export class ContextGate extends Gate {
|
|
6
|
+
config;
|
|
7
|
+
constructor(config) {
|
|
8
|
+
super('context-drift', 'Context Awareness & Drift Detection');
|
|
9
|
+
this.config = config;
|
|
10
|
+
}
|
|
11
|
+
async run(context) {
|
|
12
|
+
const failures = [];
|
|
13
|
+
const record = context.record;
|
|
14
|
+
if (!record || !this.config.context?.enabled)
|
|
15
|
+
return [];
|
|
16
|
+
const files = await FileScanner.findFiles({ cwd: context.cwd });
|
|
17
|
+
const envAnchors = record.anchors.filter(a => a.type === 'env' && a.confidence >= 1);
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
try {
|
|
20
|
+
const content = await fs.readFile(path.join(context.cwd, file), 'utf-8');
|
|
21
|
+
// 1. Detect Redundant Suffixes (The Golden Example)
|
|
22
|
+
this.checkEnvDrift(content, file, envAnchors, failures);
|
|
23
|
+
}
|
|
24
|
+
catch (e) { }
|
|
25
|
+
}
|
|
26
|
+
return failures;
|
|
27
|
+
}
|
|
28
|
+
checkEnvDrift(content, file, anchors, failures) {
|
|
29
|
+
// Find all environment variable accesses in the content
|
|
30
|
+
const matches = content.matchAll(/process\.env(?:\.([A-Z0-9_]+)|\[['"]([A-Z0-9_]+)['"]\])/g);
|
|
31
|
+
for (const match of matches) {
|
|
32
|
+
const accessedVar = match[1] || match[2];
|
|
33
|
+
for (const anchor of anchors) {
|
|
34
|
+
// If the accessed variable contains the anchor but is not equal to it,
|
|
35
|
+
// it's a potential "invented" redundancy (e.g. CORE_URL vs CORE_URL_PROD)
|
|
36
|
+
if (accessedVar !== anchor.id && accessedVar.includes(anchor.id)) {
|
|
37
|
+
const deviation = accessedVar.replace(anchor.id, '').replace(/^_|_$/, '');
|
|
38
|
+
failures.push(this.createFailure(`Context Drift: Redundant variation '${accessedVar}' detected in ${file}.`, [file], `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.`));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Gate, GateContext } from './base.js';
|
|
2
|
+
import { Failure, Gates } from '../types/index.js';
|
|
3
|
+
export declare class CoverageGate extends Gate {
|
|
4
|
+
private config;
|
|
5
|
+
constructor(config: Gates);
|
|
6
|
+
run(context: GateContext): Promise<Failure[]>;
|
|
7
|
+
private parseLcov;
|
|
8
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Gate } from './base.js';
|
|
4
|
+
import { globby } from 'globby';
|
|
5
|
+
export class CoverageGate extends Gate {
|
|
6
|
+
config;
|
|
7
|
+
constructor(config) {
|
|
8
|
+
super('coverage-guard', 'Dynamic Coverage Guard');
|
|
9
|
+
this.config = config;
|
|
10
|
+
}
|
|
11
|
+
async run(context) {
|
|
12
|
+
const failures = [];
|
|
13
|
+
// 1. Locate coverage report (lcov.info is standard)
|
|
14
|
+
const reports = await globby(['**/lcov.info', '**/coverage-final.json'], {
|
|
15
|
+
cwd: context.cwd,
|
|
16
|
+
ignore: ['node_modules/**']
|
|
17
|
+
});
|
|
18
|
+
if (reports.length === 0) {
|
|
19
|
+
// If no reports found, and coverage is required, we could flag it.
|
|
20
|
+
// But for now, we'll just skip silently if not configured.
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
// 2. Parse coverage (Simplified LCOV parser for demonstration)
|
|
24
|
+
const coverageData = await this.parseLcov(path.join(context.cwd, reports[0]));
|
|
25
|
+
// 3. Quality Handshake: SME SME LOGIC
|
|
26
|
+
// We look for files that have high complexity but low coverage.
|
|
27
|
+
// In a real implementation, we would share data between ASTGate and CoverageGate.
|
|
28
|
+
// For this demo, we'll implement a standalone check.
|
|
29
|
+
for (const [file, stats] of Object.entries(coverageData)) {
|
|
30
|
+
const coverage = (stats.hit / stats.found) * 100;
|
|
31
|
+
const threshold = stats.isComplex ? 80 : 50; // SME logic: Complex files need higher coverage
|
|
32
|
+
if (coverage < threshold) {
|
|
33
|
+
failures.push({
|
|
34
|
+
id: 'DYNAMIC_COVERAGE_LOW',
|
|
35
|
+
title: `Low coverage for high-risk file: ${file}`,
|
|
36
|
+
details: `Current coverage: ${coverage.toFixed(2)}%. Required: ${threshold}% due to structural risk.`,
|
|
37
|
+
files: [file],
|
|
38
|
+
hint: `Add dynamic tests to cover complex logical branches in this file.`
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return failures;
|
|
43
|
+
}
|
|
44
|
+
async parseLcov(reportPath) {
|
|
45
|
+
const content = await fs.readFile(reportPath, 'utf-8');
|
|
46
|
+
const results = {};
|
|
47
|
+
let currentFile = '';
|
|
48
|
+
for (const line of content.split('\n')) {
|
|
49
|
+
if (line.startsWith('SF:')) {
|
|
50
|
+
currentFile = line.substring(3);
|
|
51
|
+
results[currentFile] = { found: 0, hit: 0, isComplex: false };
|
|
52
|
+
}
|
|
53
|
+
else if (line.startsWith('LF:')) {
|
|
54
|
+
results[currentFile].found = parseInt(line.substring(3));
|
|
55
|
+
}
|
|
56
|
+
else if (line.startsWith('LH:')) {
|
|
57
|
+
results[currentFile].hit = parseInt(line.substring(3));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return results;
|
|
61
|
+
}
|
|
62
|
+
}
|
package/dist/gates/dependency.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.DependencyGate = void 0;
|
|
7
|
-
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
-
const path_1 = __importDefault(require("path"));
|
|
9
|
-
const base_js_1 = require("./base.js");
|
|
10
|
-
class DependencyGate extends base_js_1.Gate {
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Gate } from './base.js';
|
|
4
|
+
export class DependencyGate extends Gate {
|
|
11
5
|
config;
|
|
12
6
|
constructor(config) {
|
|
13
7
|
super('dependency-guardian', 'Dependency Guardian');
|
|
@@ -20,10 +14,10 @@ class DependencyGate extends base_js_1.Gate {
|
|
|
20
14
|
return [];
|
|
21
15
|
const { cwd } = context;
|
|
22
16
|
// 1. Scan Node.js (package.json)
|
|
23
|
-
const pkgPath =
|
|
24
|
-
if (await
|
|
17
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
18
|
+
if (await fs.pathExists(pkgPath)) {
|
|
25
19
|
try {
|
|
26
|
-
const pkg = await
|
|
20
|
+
const pkg = await fs.readJson(pkgPath);
|
|
27
21
|
const allDeps = {
|
|
28
22
|
...(pkg.dependencies || {}),
|
|
29
23
|
...(pkg.devDependencies || {}),
|
|
@@ -40,4 +34,3 @@ class DependencyGate extends base_js_1.Gate {
|
|
|
40
34
|
return failures;
|
|
41
35
|
}
|
|
42
36
|
}
|
|
43
|
-
exports.DependencyGate = DependencyGate;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Gate, GateContext } from './base.js';
|
|
2
|
+
import { Failure, Gates } from '../types/index.js';
|
|
3
|
+
export declare class EnvironmentGate extends Gate {
|
|
4
|
+
private config;
|
|
5
|
+
constructor(config: Gates);
|
|
6
|
+
run(context: GateContext): Promise<Failure[]>;
|
|
7
|
+
private discoverContracts;
|
|
8
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Gate } from './base.js';
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
import semver from 'semver';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
export class EnvironmentGate extends Gate {
|
|
7
|
+
config;
|
|
8
|
+
constructor(config) {
|
|
9
|
+
super('environment-alignment', 'Environment & Tooling Alignment');
|
|
10
|
+
this.config = config;
|
|
11
|
+
}
|
|
12
|
+
async run(context) {
|
|
13
|
+
const failures = [];
|
|
14
|
+
const envConfig = this.config.environment;
|
|
15
|
+
if (!envConfig || !envConfig.enabled)
|
|
16
|
+
return [];
|
|
17
|
+
const contracts = envConfig.enforce_contracts ? await this.discoverContracts(context.cwd) : {};
|
|
18
|
+
const toolsToCheck = { ...contracts, ...(envConfig.tools || {}) };
|
|
19
|
+
// 1. Verify Tool Versions
|
|
20
|
+
for (const [tool, range] of Object.entries(toolsToCheck)) {
|
|
21
|
+
// Ensure range is a string
|
|
22
|
+
const semverRange = String(range);
|
|
23
|
+
try {
|
|
24
|
+
const { stdout } = await execa(tool, ['--version'], { shell: true });
|
|
25
|
+
const versionMatch = stdout.match(/(\d+\.\d+\.\d+)/);
|
|
26
|
+
if (versionMatch) {
|
|
27
|
+
const version = versionMatch[1];
|
|
28
|
+
if (!semver.satisfies(version, semverRange)) {
|
|
29
|
+
failures.push(this.createFailure(`Environment Alignment: Tool '${tool}' version mismatch.`, [], `Project requires '${tool} ${semverRange}' (discovered from contract), but found version '${version}'. Please align your local environment to prevent drift.`));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
failures.push(this.createFailure(`Environment Alignment: Could not determine version for '${tool}'.`, [], `Ensure '${tool} --version' returns a standard SemVer string.`));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
failures.push(this.createFailure(`Environment Alignment: Required tool '${tool}' is missing.`, [], `Install '${tool}' and ensure it is in your $PATH.`));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// 2. Verify Required Env Vars
|
|
41
|
+
const requiredEnv = envConfig.required_env || [];
|
|
42
|
+
for (const envVar of requiredEnv) {
|
|
43
|
+
if (!process.env[envVar]) {
|
|
44
|
+
failures.push(this.createFailure(`Environment Alignment: Missing required environment variable '${envVar}'.`, [], `Ensure '${envVar}' is defined in your environment or .env file.`));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return failures;
|
|
48
|
+
}
|
|
49
|
+
async discoverContracts(cwd) {
|
|
50
|
+
const contracts = {};
|
|
51
|
+
// 1. Scan pyproject.toml (for ruff, mypy)
|
|
52
|
+
const pyprojectPath = path.join(cwd, 'pyproject.toml');
|
|
53
|
+
if (await fs.pathExists(pyprojectPath)) {
|
|
54
|
+
const content = await fs.readFile(pyprojectPath, 'utf-8');
|
|
55
|
+
// SME Logic: Look for ruff and mypy version constraints
|
|
56
|
+
// Handle both ruff = "^0.14.0" and ruff = { version = "^0.14.0" }
|
|
57
|
+
const ruffMatch = content.match(/ruff\s*=\s*(?:['"]([^'"]+)['"]|\{\s*version\s*=\s*['"]([^'"]+)['"]\s*\})/);
|
|
58
|
+
if (ruffMatch)
|
|
59
|
+
contracts['ruff'] = ruffMatch[1] || ruffMatch[2];
|
|
60
|
+
const mypyMatch = content.match(/mypy\s*=\s*(?:['"]([^'"]+)['"]|\{\s*version\s*=\s*['"]([^'"]+)['"]\s*\})/);
|
|
61
|
+
if (mypyMatch)
|
|
62
|
+
contracts['mypy'] = mypyMatch[1] || mypyMatch[2];
|
|
63
|
+
}
|
|
64
|
+
// 2. Scan package.json (for node/npm/pnpm)
|
|
65
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
66
|
+
if (await fs.pathExists(pkgPath)) {
|
|
67
|
+
const pkg = await fs.readJson(pkgPath);
|
|
68
|
+
if (pkg.engines?.node)
|
|
69
|
+
contracts['node'] = pkg.engines.node;
|
|
70
|
+
}
|
|
71
|
+
return contracts;
|
|
72
|
+
}
|
|
73
|
+
}
|
package/dist/gates/file.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const base_js_1 = require("./base.js");
|
|
5
|
-
const scanner_js_1 = require("../utils/scanner.js");
|
|
6
|
-
class FileGate extends base_js_1.Gate {
|
|
1
|
+
import { Gate } from './base.js';
|
|
2
|
+
import { FileScanner } from '../utils/scanner.js';
|
|
3
|
+
export class FileGate extends Gate {
|
|
7
4
|
config;
|
|
8
5
|
constructor(config) {
|
|
9
6
|
super('file-size', 'File Size Limit');
|
|
10
7
|
this.config = config;
|
|
11
8
|
}
|
|
12
9
|
async run(context) {
|
|
13
|
-
const files = await
|
|
14
|
-
|
|
10
|
+
const files = await FileScanner.findFiles({
|
|
11
|
+
cwd: context.cwd,
|
|
12
|
+
ignore: context.ignore,
|
|
13
|
+
patterns: context.patterns
|
|
14
|
+
});
|
|
15
|
+
const contents = await FileScanner.readFiles(context.cwd, files);
|
|
15
16
|
const violations = [];
|
|
16
17
|
for (const [file, content] of contents) {
|
|
17
18
|
const lines = content.split('\n').length;
|
|
@@ -27,4 +28,3 @@ class FileGate extends base_js_1.Gate {
|
|
|
27
28
|
return [];
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
|
-
exports.FileGate = FileGate;
|
package/dist/gates/runner.d.ts
CHANGED
package/dist/gates/runner.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { FileGate } from './file.js';
|
|
2
|
+
import { ContentGate } from './content.js';
|
|
3
|
+
import { StructureGate } from './structure.js';
|
|
4
|
+
import { ASTGate } from './ast.js';
|
|
5
|
+
import { SafetyGate } from './safety.js';
|
|
6
|
+
import { DependencyGate } from './dependency.js';
|
|
7
|
+
import { CoverageGate } from './coverage.js';
|
|
8
|
+
import { ContextGate } from './context.js';
|
|
9
|
+
import { ContextEngine } from '../services/context-engine.js';
|
|
10
|
+
import { EnvironmentGate } from './environment.js'; // [NEW]
|
|
11
|
+
import { execa } from 'execa';
|
|
12
|
+
import { Logger } from '../utils/logger.js';
|
|
13
|
+
export class GateRunner {
|
|
13
14
|
config;
|
|
14
15
|
gates = [];
|
|
15
16
|
constructor(config) {
|
|
@@ -18,18 +19,26 @@ class GateRunner {
|
|
|
18
19
|
}
|
|
19
20
|
initializeGates() {
|
|
20
21
|
if (this.config.gates.max_file_lines) {
|
|
21
|
-
this.gates.push(new
|
|
22
|
+
this.gates.push(new FileGate({ maxLines: this.config.gates.max_file_lines }));
|
|
22
23
|
}
|
|
23
|
-
this.gates.push(new
|
|
24
|
+
this.gates.push(new ContentGate({
|
|
24
25
|
forbidTodos: !!this.config.gates.forbid_todos,
|
|
25
26
|
forbidFixme: !!this.config.gates.forbid_fixme,
|
|
26
27
|
}));
|
|
27
28
|
if (this.config.gates.required_files) {
|
|
28
|
-
this.gates.push(new
|
|
29
|
+
this.gates.push(new StructureGate({ requiredFiles: this.config.gates.required_files }));
|
|
30
|
+
}
|
|
31
|
+
this.gates.push(new ASTGate(this.config.gates));
|
|
32
|
+
this.gates.push(new DependencyGate(this.config));
|
|
33
|
+
this.gates.push(new SafetyGate(this.config.gates));
|
|
34
|
+
this.gates.push(new CoverageGate(this.config.gates));
|
|
35
|
+
if (this.config.gates.context?.enabled) {
|
|
36
|
+
this.gates.push(new ContextGate(this.config.gates));
|
|
37
|
+
}
|
|
38
|
+
// Environment Alignment Gate (Should be prioritized)
|
|
39
|
+
if (this.config.gates.environment?.enabled) {
|
|
40
|
+
this.gates.unshift(new EnvironmentGate(this.config.gates));
|
|
29
41
|
}
|
|
30
|
-
this.gates.push(new ast_js_1.ASTGate(this.config.gates));
|
|
31
|
-
this.gates.push(new dependency_js_1.DependencyGate(this.config));
|
|
32
|
-
this.gates.push(new safety_js_1.SafetyGate(this.config.gates));
|
|
33
42
|
}
|
|
34
43
|
/**
|
|
35
44
|
* Allows adding custom gates dynamically (SOLID - Open/Closed Principle)
|
|
@@ -37,14 +46,21 @@ class GateRunner {
|
|
|
37
46
|
addGate(gate) {
|
|
38
47
|
this.gates.push(gate);
|
|
39
48
|
}
|
|
40
|
-
async run(cwd) {
|
|
49
|
+
async run(cwd, patterns) {
|
|
41
50
|
const start = Date.now();
|
|
42
51
|
const failures = [];
|
|
43
52
|
const summary = {};
|
|
53
|
+
const ignore = this.config.ignore;
|
|
54
|
+
// 0. Run Context Discovery
|
|
55
|
+
let record;
|
|
56
|
+
if (this.config.gates.context?.enabled) {
|
|
57
|
+
const engine = new ContextEngine(this.config);
|
|
58
|
+
record = await engine.discover(cwd);
|
|
59
|
+
}
|
|
44
60
|
// 1. Run internal gates
|
|
45
61
|
for (const gate of this.gates) {
|
|
46
62
|
try {
|
|
47
|
-
const gateFailures = await gate.run({ cwd });
|
|
63
|
+
const gateFailures = await gate.run({ cwd, record, ignore, patterns });
|
|
48
64
|
if (gateFailures.length > 0) {
|
|
49
65
|
failures.push(...gateFailures);
|
|
50
66
|
summary[gate.id] = 'FAIL';
|
|
@@ -54,7 +70,7 @@ class GateRunner {
|
|
|
54
70
|
}
|
|
55
71
|
}
|
|
56
72
|
catch (error) {
|
|
57
|
-
|
|
73
|
+
Logger.error(`Gate ${gate.id} failed with error: ${error.message}`);
|
|
58
74
|
summary[gate.id] = 'ERROR';
|
|
59
75
|
failures.push({
|
|
60
76
|
id: gate.id,
|
|
@@ -73,8 +89,8 @@ class GateRunner {
|
|
|
73
89
|
continue;
|
|
74
90
|
}
|
|
75
91
|
try {
|
|
76
|
-
|
|
77
|
-
await
|
|
92
|
+
Logger.info(`Running command gate: ${key} (${cmd})`);
|
|
93
|
+
await execa(cmd, { shell: true, cwd });
|
|
78
94
|
summary[key] = 'PASS';
|
|
79
95
|
}
|
|
80
96
|
catch (error) {
|
|
@@ -89,14 +105,15 @@ class GateRunner {
|
|
|
89
105
|
}
|
|
90
106
|
}
|
|
91
107
|
const status = failures.length > 0 ? 'FAIL' : 'PASS';
|
|
108
|
+
const score = Math.max(0, 100 - (failures.length * 5)); // Basic SME scoring logic
|
|
92
109
|
return {
|
|
93
110
|
status,
|
|
94
111
|
summary,
|
|
95
112
|
failures,
|
|
96
113
|
stats: {
|
|
97
114
|
duration_ms: Date.now() - start,
|
|
115
|
+
score,
|
|
98
116
|
},
|
|
99
117
|
};
|
|
100
118
|
}
|
|
101
119
|
}
|
|
102
|
-
exports.GateRunner = GateRunner;
|