@rigour-labs/core 4.0.4 → 4.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/gates/ast-handlers/typescript.js +39 -12
- package/dist/gates/ast-handlers/universal.js +9 -3
- package/dist/gates/ast.js +15 -1
- package/dist/gates/ast.test.d.ts +1 -0
- package/dist/gates/ast.test.js +112 -0
- package/dist/gates/content.d.ts +5 -0
- package/dist/gates/content.js +66 -7
- package/dist/gates/content.test.d.ts +1 -0
- package/dist/gates/content.test.js +73 -0
- package/dist/gates/context-window-artifacts.d.ts +1 -0
- package/dist/gates/context-window-artifacts.js +10 -3
- package/dist/gates/context.d.ts +1 -0
- package/dist/gates/context.js +29 -8
- package/dist/gates/deep-analysis.js +2 -2
- package/dist/gates/deprecated-apis.d.ts +1 -0
- package/dist/gates/deprecated-apis.js +15 -2
- package/dist/gates/hallucinated-imports.d.ts +14 -0
- package/dist/gates/hallucinated-imports.js +267 -60
- package/dist/gates/hallucinated-imports.test.js +164 -1
- package/dist/gates/inconsistent-error-handling.d.ts +1 -0
- package/dist/gates/inconsistent-error-handling.js +12 -1
- package/dist/gates/phantom-apis.d.ts +2 -0
- package/dist/gates/phantom-apis.js +28 -3
- package/dist/gates/phantom-apis.test.js +14 -0
- package/dist/gates/promise-safety.d.ts +2 -0
- package/dist/gates/promise-safety.js +31 -9
- package/dist/gates/runner.js +8 -2
- package/dist/gates/runner.test.d.ts +1 -0
- package/dist/gates/runner.test.js +65 -0
- package/dist/gates/security-patterns.d.ts +1 -0
- package/dist/gates/security-patterns.js +22 -6
- package/dist/gates/security-patterns.test.js +18 -0
- package/dist/hooks/templates.d.ts +1 -1
- package/dist/hooks/templates.js +12 -12
- package/dist/inference/executable.d.ts +6 -0
- package/dist/inference/executable.js +29 -0
- package/dist/inference/executable.test.d.ts +1 -0
- package/dist/inference/executable.test.js +41 -0
- package/dist/inference/model-manager.d.ts +3 -1
- package/dist/inference/model-manager.js +76 -8
- package/dist/inference/model-manager.test.d.ts +1 -0
- package/dist/inference/model-manager.test.js +24 -0
- package/dist/inference/sidecar-provider.d.ts +1 -0
- package/dist/inference/sidecar-provider.js +124 -31
- package/dist/services/context-engine.js +1 -1
- package/dist/templates/universal-config.js +3 -3
- package/dist/types/index.js +3 -3
- package/dist/utils/scanner.js +6 -0
- package/package.json +7 -2
|
@@ -20,7 +20,7 @@ export class TypeScriptHandler extends ASTHandler {
|
|
|
20
20
|
const maxMethods = astConfig.max_methods || 10;
|
|
21
21
|
const maxParams = astConfig.max_params || 5;
|
|
22
22
|
// Limit failures per file to avoid output bloat on large files
|
|
23
|
-
const MAX_FAILURES_PER_FILE =
|
|
23
|
+
const MAX_FAILURES_PER_FILE = 5;
|
|
24
24
|
const fileFailureCount = {};
|
|
25
25
|
const addFailure = (failure) => {
|
|
26
26
|
const ruleId = failure.id;
|
|
@@ -36,7 +36,9 @@ export class TypeScriptHandler extends ASTHandler {
|
|
|
36
36
|
title: `More than ${MAX_FAILURES_PER_FILE} ${ruleId} violations in ${relativePath}`,
|
|
37
37
|
details: `Truncated output: showing first ${MAX_FAILURES_PER_FILE} violations. Consider fixing the root cause.`,
|
|
38
38
|
files: [relativePath],
|
|
39
|
-
hint: `This file has many violations. Fix them systematically or exclude the file if it's legacy code
|
|
39
|
+
hint: `This file has many violations. Fix them systematically or exclude the file if it's legacy code.`,
|
|
40
|
+
severity: 'medium',
|
|
41
|
+
provenance: 'traditional',
|
|
40
42
|
});
|
|
41
43
|
}
|
|
42
44
|
return false;
|
|
@@ -61,7 +63,9 @@ export class TypeScriptHandler extends ASTHandler {
|
|
|
61
63
|
details: `Use 'const' or 'let' instead of 'var' in ${relativePath}:${line}`,
|
|
62
64
|
files: [relativePath],
|
|
63
65
|
line,
|
|
64
|
-
hint: `Replace 'var' with 'const' (preferred) or 'let' for modern JavaScript
|
|
66
|
+
hint: `Replace 'var' with 'const' (preferred) or 'let' for modern JavaScript.`,
|
|
67
|
+
severity: 'medium',
|
|
68
|
+
provenance: 'traditional',
|
|
65
69
|
});
|
|
66
70
|
}
|
|
67
71
|
}
|
|
@@ -75,7 +79,9 @@ export class TypeScriptHandler extends ASTHandler {
|
|
|
75
79
|
details: `Use ES6 'import' instead of 'require()' in ${relativePath}:${line}`,
|
|
76
80
|
files: [relativePath],
|
|
77
81
|
line,
|
|
78
|
-
hint: `Replace require('module') with import module from 'module'
|
|
82
|
+
hint: `Replace require('module') with import module from 'module'.`,
|
|
83
|
+
severity: 'medium',
|
|
84
|
+
provenance: 'traditional',
|
|
79
85
|
});
|
|
80
86
|
}
|
|
81
87
|
}
|
|
@@ -91,7 +97,9 @@ export class TypeScriptHandler extends ASTHandler {
|
|
|
91
97
|
details: `Use rest parameters (...args) instead of 'arguments' in ${relativePath}:${line}`,
|
|
92
98
|
files: [relativePath],
|
|
93
99
|
line,
|
|
94
|
-
hint: `Replace 'arguments' with rest parameters: function(...args) { }
|
|
100
|
+
hint: `Replace 'arguments' with rest parameters: function(...args) { }`,
|
|
101
|
+
severity: 'medium',
|
|
102
|
+
provenance: 'traditional',
|
|
95
103
|
});
|
|
96
104
|
}
|
|
97
105
|
}
|
|
@@ -105,7 +113,9 @@ export class TypeScriptHandler extends ASTHandler {
|
|
|
105
113
|
details: `Prototype pollution vulnerability in ${relativePath}:${line}`,
|
|
106
114
|
files: [relativePath],
|
|
107
115
|
line,
|
|
108
|
-
hint: `Use Object.getPrototypeOf() or Object.setPrototypeOf() instead of __proto__
|
|
116
|
+
hint: `Use Object.getPrototypeOf() or Object.setPrototypeOf() instead of __proto__.`,
|
|
117
|
+
severity: 'critical',
|
|
118
|
+
provenance: 'security',
|
|
109
119
|
});
|
|
110
120
|
}
|
|
111
121
|
// Check for bracket notation __proto__ access: obj["__proto__"]
|
|
@@ -119,7 +129,9 @@ export class TypeScriptHandler extends ASTHandler {
|
|
|
119
129
|
details: `Potential prototype pollution via bracket notation in ${relativePath}:${line}`,
|
|
120
130
|
files: [relativePath],
|
|
121
131
|
line,
|
|
122
|
-
hint: `Block access to '${accessKey}' property when handling user input. Use allowlist for object keys
|
|
132
|
+
hint: `Block access to '${accessKey}' property when handling user input. Use allowlist for object keys.`,
|
|
133
|
+
severity: 'critical',
|
|
134
|
+
provenance: 'security',
|
|
123
135
|
});
|
|
124
136
|
}
|
|
125
137
|
}
|
|
@@ -139,7 +151,9 @@ export class TypeScriptHandler extends ASTHandler {
|
|
|
139
151
|
details: `Object.assign({}, ...) can propagate prototype pollution in ${relativePath}:${line}`,
|
|
140
152
|
files: [relativePath],
|
|
141
153
|
line,
|
|
142
|
-
hint: `Validate and sanitize source objects before merging. Block __proto__ and constructor keys
|
|
154
|
+
hint: `Validate and sanitize source objects before merging. Block __proto__ and constructor keys.`,
|
|
155
|
+
severity: 'high',
|
|
156
|
+
provenance: 'security',
|
|
143
157
|
});
|
|
144
158
|
}
|
|
145
159
|
}
|
|
@@ -154,11 +168,18 @@ export class TypeScriptHandler extends ASTHandler {
|
|
|
154
168
|
title: `Function '${name}' has ${node.parameters.length} parameters (max: ${maxParams})`,
|
|
155
169
|
details: `High parameter count detected in ${relativePath}`,
|
|
156
170
|
files: [relativePath],
|
|
157
|
-
hint: `Reduce number of parameters or use an options object
|
|
171
|
+
hint: `Reduce number of parameters or use an options object.`,
|
|
172
|
+
severity: 'medium',
|
|
173
|
+
provenance: 'traditional',
|
|
158
174
|
});
|
|
159
175
|
}
|
|
160
176
|
let complexity = 1;
|
|
161
177
|
const countComplexity = (n) => {
|
|
178
|
+
// Nested functions have their own complexity budget.
|
|
179
|
+
// Do not attribute their branches to the parent function.
|
|
180
|
+
if (n !== node && ts.isFunctionLike(n)) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
162
183
|
if (ts.isIfStatement(n) || ts.isCaseClause(n) || ts.isDefaultClause(n) ||
|
|
163
184
|
ts.isForStatement(n) || ts.isForInStatement(n) || ts.isForOfStatement(n) ||
|
|
164
185
|
ts.isWhileStatement(n) || ts.isDoStatement(n) || ts.isConditionalExpression(n)) {
|
|
@@ -179,7 +200,9 @@ export class TypeScriptHandler extends ASTHandler {
|
|
|
179
200
|
title: `Function '${name}' has cyclomatic complexity of ${complexity} (max: ${maxComplexity})`,
|
|
180
201
|
details: `High complexity detected in ${relativePath}`,
|
|
181
202
|
files: [relativePath],
|
|
182
|
-
hint: `Refactor '${name}' into smaller, more focused functions
|
|
203
|
+
hint: `Refactor '${name}' into smaller, more focused functions.`,
|
|
204
|
+
severity: 'medium',
|
|
205
|
+
provenance: 'traditional',
|
|
183
206
|
});
|
|
184
207
|
}
|
|
185
208
|
}
|
|
@@ -192,7 +215,9 @@ export class TypeScriptHandler extends ASTHandler {
|
|
|
192
215
|
title: `Class '${name}' has ${methods.length} methods (max: ${maxMethods})`,
|
|
193
216
|
details: `God Object pattern detected in ${relativePath}`,
|
|
194
217
|
files: [relativePath],
|
|
195
|
-
hint: `Class '${name}' is becoming too large. Split it into smaller services
|
|
218
|
+
hint: `Class '${name}' is becoming too large. Split it into smaller services.`,
|
|
219
|
+
severity: 'medium',
|
|
220
|
+
provenance: 'traditional',
|
|
196
221
|
});
|
|
197
222
|
}
|
|
198
223
|
}
|
|
@@ -217,7 +242,9 @@ export class TypeScriptHandler extends ASTHandler {
|
|
|
217
242
|
title: `Architectural Violation`,
|
|
218
243
|
details: `'${relativePath}' is forbidden from importing '${importPath}' (denied by boundary rule).`,
|
|
219
244
|
files: [relativePath],
|
|
220
|
-
hint: `Remove this import to maintain architectural layering
|
|
245
|
+
hint: `Remove this import to maintain architectural layering.`,
|
|
246
|
+
severity: 'high',
|
|
247
|
+
provenance: 'traditional',
|
|
221
248
|
});
|
|
222
249
|
}
|
|
223
250
|
}
|
|
@@ -114,7 +114,9 @@ export class UniversalASTHandler extends ASTHandler {
|
|
|
114
114
|
title: `Method '${name}' has high cognitive load (${cognitive})`,
|
|
115
115
|
details: `Deeply nested or complex logic detected in ${context.file}.`,
|
|
116
116
|
files: [context.file],
|
|
117
|
-
hint: `Flatten logical branches and extract nested loops
|
|
117
|
+
hint: `Flatten logical branches and extract nested loops.`,
|
|
118
|
+
severity: 'medium',
|
|
119
|
+
provenance: 'traditional',
|
|
118
120
|
});
|
|
119
121
|
}
|
|
120
122
|
}
|
|
@@ -129,7 +131,9 @@ export class UniversalASTHandler extends ASTHandler {
|
|
|
129
131
|
title: `Unsafe function call detected: ${capture.node.text}`,
|
|
130
132
|
details: `Potentially dangerous execution in ${context.file}.`,
|
|
131
133
|
files: [context.file],
|
|
132
|
-
hint: `Avoid using shell execution or eval. Use safe alternatives
|
|
134
|
+
hint: `Avoid using shell execution or eval. Use safe alternatives.`,
|
|
135
|
+
severity: 'high',
|
|
136
|
+
provenance: 'security',
|
|
133
137
|
});
|
|
134
138
|
}
|
|
135
139
|
}
|
|
@@ -143,7 +147,9 @@ export class UniversalASTHandler extends ASTHandler {
|
|
|
143
147
|
title: `Ecosystem anti-pattern detected`,
|
|
144
148
|
details: `Violation of ${ext} best practices in ${context.file}.`,
|
|
145
149
|
files: [context.file],
|
|
146
|
-
hint: `Review language-specific best practices (e.g., error handling or mutable defaults)
|
|
150
|
+
hint: `Review language-specific best practices (e.g., error handling or mutable defaults).`,
|
|
151
|
+
severity: 'medium',
|
|
152
|
+
provenance: 'traditional',
|
|
147
153
|
});
|
|
148
154
|
}
|
|
149
155
|
}
|
package/dist/gates/ast.js
CHANGED
|
@@ -18,7 +18,21 @@ export class ASTGate extends Gate {
|
|
|
18
18
|
async run(context) {
|
|
19
19
|
const failures = [];
|
|
20
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
|
|
21
|
+
const defaultIgnore = [
|
|
22
|
+
'**/node_modules/**',
|
|
23
|
+
'**/dist/**',
|
|
24
|
+
'**/build/**',
|
|
25
|
+
'**/studio-dist/**',
|
|
26
|
+
'**/.next/**',
|
|
27
|
+
'**/coverage/**',
|
|
28
|
+
'**/out/**',
|
|
29
|
+
'**/target/**',
|
|
30
|
+
'**/examples/**',
|
|
31
|
+
'**/*.test.*',
|
|
32
|
+
'**/*.spec.*',
|
|
33
|
+
'**/__pycache__/**',
|
|
34
|
+
];
|
|
35
|
+
const ignore = [...new Set([...(context.ignore || []), ...defaultIgnore])].map(p => p.replace(/\\/g, '/'));
|
|
22
36
|
const normalizedCwd = context.cwd.replace(/\\/g, '/');
|
|
23
37
|
// Find all supported files
|
|
24
38
|
const files = await globby(patterns, {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { ASTGate } from './ast.js';
|
|
6
|
+
describe('ASTGate ignore behavior', () => {
|
|
7
|
+
let testDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ast-gate-test-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
it('keeps default ignores when context.ignore is an empty array', async () => {
|
|
15
|
+
const gate = new ASTGate({
|
|
16
|
+
ast: { max_params: 1 },
|
|
17
|
+
});
|
|
18
|
+
fs.mkdirSync(path.join(testDir, 'node_modules', 'example'), { recursive: true });
|
|
19
|
+
fs.writeFileSync(path.join(testDir, 'node_modules', 'example', 'bad.js'), 'function fromDeps(a, b, c) { return a + b + c; }', 'utf-8');
|
|
20
|
+
fs.mkdirSync(path.join(testDir, 'src'), { recursive: true });
|
|
21
|
+
fs.writeFileSync(path.join(testDir, 'src', 'ok.js'), 'function ok(a) { return a; }', 'utf-8');
|
|
22
|
+
const failures = await gate.run({
|
|
23
|
+
cwd: testDir,
|
|
24
|
+
ignore: [],
|
|
25
|
+
});
|
|
26
|
+
expect(failures).toHaveLength(0);
|
|
27
|
+
});
|
|
28
|
+
it('merges user ignore patterns with default ignores', async () => {
|
|
29
|
+
const gate = new ASTGate({
|
|
30
|
+
ast: { max_params: 1 },
|
|
31
|
+
});
|
|
32
|
+
fs.mkdirSync(path.join(testDir, 'generated'), { recursive: true });
|
|
33
|
+
fs.writeFileSync(path.join(testDir, 'generated', 'bad.js'), 'function generated(a, b, c) { return a + b + c; }', 'utf-8');
|
|
34
|
+
fs.mkdirSync(path.join(testDir, 'src'), { recursive: true });
|
|
35
|
+
fs.writeFileSync(path.join(testDir, 'src', 'ok.js'), 'function ok(a) { return a; }', 'utf-8');
|
|
36
|
+
const failures = await gate.run({
|
|
37
|
+
cwd: testDir,
|
|
38
|
+
ignore: ['generated/**'],
|
|
39
|
+
});
|
|
40
|
+
expect(failures).toHaveLength(0);
|
|
41
|
+
});
|
|
42
|
+
it('ignores generated studio-dist assets by default', async () => {
|
|
43
|
+
const gate = new ASTGate({
|
|
44
|
+
ast: { max_params: 1 },
|
|
45
|
+
});
|
|
46
|
+
fs.mkdirSync(path.join(testDir, 'packages', 'rigour-cli', 'studio-dist', 'assets'), { recursive: true });
|
|
47
|
+
fs.writeFileSync(path.join(testDir, 'packages', 'rigour-cli', 'studio-dist', 'assets', 'index.js'), 'function fromBundle(a, b, c) { return a + b + c; }', 'utf-8');
|
|
48
|
+
const failures = await gate.run({
|
|
49
|
+
cwd: testDir,
|
|
50
|
+
ignore: [],
|
|
51
|
+
});
|
|
52
|
+
expect(failures).toHaveLength(0);
|
|
53
|
+
});
|
|
54
|
+
it('ignores examples directory by default', async () => {
|
|
55
|
+
const gate = new ASTGate({
|
|
56
|
+
ast: { max_params: 1 },
|
|
57
|
+
});
|
|
58
|
+
fs.mkdirSync(path.join(testDir, 'examples', 'demo', 'src'), { recursive: true });
|
|
59
|
+
fs.writeFileSync(path.join(testDir, 'examples', 'demo', 'src', 'bad.js'), 'function noisy(a, b, c) { return a + b + c; }', 'utf-8');
|
|
60
|
+
const failures = await gate.run({
|
|
61
|
+
cwd: testDir,
|
|
62
|
+
ignore: [],
|
|
63
|
+
});
|
|
64
|
+
expect(failures).toHaveLength(0);
|
|
65
|
+
});
|
|
66
|
+
it('sets severity and provenance on AST violations', async () => {
|
|
67
|
+
const gate = new ASTGate({
|
|
68
|
+
ast: { max_params: 1 },
|
|
69
|
+
});
|
|
70
|
+
fs.mkdirSync(path.join(testDir, 'src'), { recursive: true });
|
|
71
|
+
fs.writeFileSync(path.join(testDir, 'src', 'bad.js'), 'function tooMany(a, b, c) { return a + b + c; }', 'utf-8');
|
|
72
|
+
const failures = await gate.run({ cwd: testDir, ignore: [] });
|
|
73
|
+
const target = failures.find((failure) => failure.id === 'AST_MAX_PARAMS');
|
|
74
|
+
expect(target).toBeDefined();
|
|
75
|
+
expect(target?.severity).toBe('medium');
|
|
76
|
+
expect(target?.provenance).toBe('traditional');
|
|
77
|
+
});
|
|
78
|
+
it('marks prototype pollution findings as security-critical', async () => {
|
|
79
|
+
const gate = new ASTGate({
|
|
80
|
+
ast: { max_params: 10 },
|
|
81
|
+
});
|
|
82
|
+
fs.mkdirSync(path.join(testDir, 'src'), { recursive: true });
|
|
83
|
+
fs.writeFileSync(path.join(testDir, 'src', 'pollute.js'), 'const target = {}; target.__proto__ = {};', 'utf-8');
|
|
84
|
+
const failures = await gate.run({ cwd: testDir, ignore: [] });
|
|
85
|
+
const target = failures.find((failure) => failure.id === 'SECURITY_PROTOTYPE_POLLUTION');
|
|
86
|
+
expect(target).toBeDefined();
|
|
87
|
+
expect(target?.severity).toBe('critical');
|
|
88
|
+
expect(target?.provenance).toBe('security');
|
|
89
|
+
});
|
|
90
|
+
it('does not attribute nested function complexity to parent function', async () => {
|
|
91
|
+
const gate = new ASTGate({
|
|
92
|
+
ast: { complexity: 3, max_params: 10 },
|
|
93
|
+
});
|
|
94
|
+
fs.mkdirSync(path.join(testDir, 'src'), { recursive: true });
|
|
95
|
+
fs.writeFileSync(path.join(testDir, 'src', 'nested.ts'), `
|
|
96
|
+
function outer(flag: boolean) {
|
|
97
|
+
function inner(x: number) {
|
|
98
|
+
if (x > 0) { return 1; }
|
|
99
|
+
if (x < 0) { return -1; }
|
|
100
|
+
if (x === 0) { return 0; }
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
return flag ? inner(1) : inner(-1);
|
|
104
|
+
}
|
|
105
|
+
`, 'utf-8');
|
|
106
|
+
const failures = await gate.run({ cwd: testDir, ignore: [] });
|
|
107
|
+
const complexityTitles = failures
|
|
108
|
+
.filter((failure) => failure.id === 'AST_COMPLEXITY')
|
|
109
|
+
.map((failure) => failure.title);
|
|
110
|
+
expect(complexityTitles.some((title) => title.includes("'outer'"))).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|
package/dist/gates/content.d.ts
CHANGED
|
@@ -8,4 +8,9 @@ export declare class ContentGate extends Gate {
|
|
|
8
8
|
private config;
|
|
9
9
|
constructor(config: ContentGateConfig);
|
|
10
10
|
run(context: GateContext): Promise<Failure[]>;
|
|
11
|
+
private shouldScanFile;
|
|
12
|
+
private shouldSkipFile;
|
|
13
|
+
private extractCommentText;
|
|
14
|
+
private normalizeCommentText;
|
|
15
|
+
private hasForbiddenMarker;
|
|
11
16
|
}
|
package/dist/gates/content.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Gate } from './base.js';
|
|
2
2
|
import { FileScanner } from '../utils/scanner.js';
|
|
3
|
+
import path from 'path';
|
|
3
4
|
export class ContentGate extends Gate {
|
|
4
5
|
config;
|
|
5
6
|
constructor(config) {
|
|
@@ -7,12 +8,12 @@ export class ContentGate extends Gate {
|
|
|
7
8
|
this.config = config;
|
|
8
9
|
}
|
|
9
10
|
async run(context) {
|
|
10
|
-
const
|
|
11
|
+
const markers = [];
|
|
11
12
|
if (this.config.forbidTodos)
|
|
12
|
-
|
|
13
|
+
markers.push('TODO');
|
|
13
14
|
if (this.config.forbidFixme)
|
|
14
|
-
|
|
15
|
-
if (
|
|
15
|
+
markers.push('FIXME');
|
|
16
|
+
if (markers.length === 0)
|
|
16
17
|
return [];
|
|
17
18
|
const files = await FileScanner.findFiles({
|
|
18
19
|
cwd: context.cwd,
|
|
@@ -22,15 +23,73 @@ export class ContentGate extends Gate {
|
|
|
22
23
|
const contents = await FileScanner.readFiles(context.cwd, files);
|
|
23
24
|
const failures = [];
|
|
24
25
|
for (const [file, content] of contents) {
|
|
26
|
+
if (!this.shouldScanFile(file))
|
|
27
|
+
continue;
|
|
28
|
+
if (this.shouldSkipFile(file))
|
|
29
|
+
continue;
|
|
25
30
|
const lines = content.split('\n');
|
|
26
31
|
lines.forEach((line, index) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
const commentText = this.extractCommentText(line);
|
|
33
|
+
if (!commentText)
|
|
34
|
+
return;
|
|
35
|
+
const normalizedComment = this.normalizeCommentText(commentText);
|
|
36
|
+
for (const marker of markers) {
|
|
37
|
+
if (this.hasForbiddenMarker(normalizedComment, marker)) {
|
|
38
|
+
failures.push(this.createFailure(`Forbidden placeholder '${marker}' found`, [file], 'Remove forbidden comments. address the root cause or create a tracked issue.', undefined, index + 1, index + 1, 'info'));
|
|
30
39
|
}
|
|
31
40
|
}
|
|
32
41
|
});
|
|
33
42
|
}
|
|
34
43
|
return failures;
|
|
35
44
|
}
|
|
45
|
+
shouldScanFile(file) {
|
|
46
|
+
const ext = path.extname(file).toLowerCase();
|
|
47
|
+
return new Set([
|
|
48
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
49
|
+
'.py', '.go', '.rb', '.java', '.kt', '.cs',
|
|
50
|
+
'.rs', '.php', '.swift', '.scala', '.sh', '.bash',
|
|
51
|
+
'.yml', '.yaml', '.json'
|
|
52
|
+
]).has(ext);
|
|
53
|
+
}
|
|
54
|
+
shouldSkipFile(file) {
|
|
55
|
+
const normalized = file.replace(/\\/g, '/');
|
|
56
|
+
return (normalized.includes('/examples/') ||
|
|
57
|
+
normalized.includes('/__tests__/') ||
|
|
58
|
+
/\.test\.[^.]+$/i.test(normalized) ||
|
|
59
|
+
/\.spec\.[^.]+$/i.test(normalized));
|
|
60
|
+
}
|
|
61
|
+
extractCommentText(line) {
|
|
62
|
+
const trimmed = line.trim();
|
|
63
|
+
if (!trimmed)
|
|
64
|
+
return null;
|
|
65
|
+
// Whole-line comments across common languages.
|
|
66
|
+
if (trimmed.startsWith('//') ||
|
|
67
|
+
trimmed.startsWith('#') ||
|
|
68
|
+
trimmed.startsWith('/*') ||
|
|
69
|
+
trimmed.startsWith('*') ||
|
|
70
|
+
trimmed.startsWith('<!--')) {
|
|
71
|
+
return trimmed;
|
|
72
|
+
}
|
|
73
|
+
// Inline comments (JS/TS/Go style)
|
|
74
|
+
const slashIdx = line.indexOf('//');
|
|
75
|
+
if (slashIdx >= 0)
|
|
76
|
+
return line.slice(slashIdx);
|
|
77
|
+
// Inline Python/Ruby shell-style comments
|
|
78
|
+
const hashIdx = line.indexOf('#');
|
|
79
|
+
if (hashIdx >= 0)
|
|
80
|
+
return line.slice(hashIdx);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
normalizeCommentText(commentText) {
|
|
84
|
+
return commentText
|
|
85
|
+
.trim()
|
|
86
|
+
.replace(/^(?:\/\/+|#+|\/\*+|\*+|<!--+)\s*/, '')
|
|
87
|
+
.trim();
|
|
88
|
+
}
|
|
89
|
+
hasForbiddenMarker(commentText, marker) {
|
|
90
|
+
// Treat placeholder markers only when used as actionable prefixes,
|
|
91
|
+
// e.g. "TODO: ...", not as explanatory references like "count TODO markers".
|
|
92
|
+
const placeholderPrefix = new RegExp(`^${marker}\\b(?:\\s*[:\\-]|\\s|$)`, 'i');
|
|
93
|
+
return placeholderPrefix.test(commentText);
|
|
94
|
+
}
|
|
36
95
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import { ContentGate } from './content.js';
|
|
6
|
+
describe('ContentGate', () => {
|
|
7
|
+
let testDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'content-gate-test-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
it('flags TODO and FIXME in comments', async () => {
|
|
15
|
+
fs.writeFileSync(path.join(testDir, 'app.ts'), `
|
|
16
|
+
// TODO: remove temporary fallback
|
|
17
|
+
const value = 1;
|
|
18
|
+
const x = value; // FIXME clean this before release
|
|
19
|
+
`);
|
|
20
|
+
const gate = new ContentGate({ forbidTodos: true, forbidFixme: true });
|
|
21
|
+
const failures = await gate.run({ cwd: testDir });
|
|
22
|
+
expect(failures).toHaveLength(2);
|
|
23
|
+
});
|
|
24
|
+
it('does not flag TODO or FIXME in non-comment string literals', async () => {
|
|
25
|
+
fs.writeFileSync(path.join(testDir, 'app.ts'), `
|
|
26
|
+
const message = "TODO this is user-facing text";
|
|
27
|
+
const note = "FIXME should not trigger in strings";
|
|
28
|
+
`);
|
|
29
|
+
const gate = new ContentGate({ forbidTodos: true, forbidFixme: true });
|
|
30
|
+
const failures = await gate.run({ cwd: testDir });
|
|
31
|
+
expect(failures).toHaveLength(0);
|
|
32
|
+
});
|
|
33
|
+
it('does not scan markdown files', async () => {
|
|
34
|
+
fs.writeFileSync(path.join(testDir, 'README.md'), `
|
|
35
|
+
# TODO
|
|
36
|
+
This is a product roadmap item.
|
|
37
|
+
`);
|
|
38
|
+
const gate = new ContentGate({ forbidTodos: true, forbidFixme: true });
|
|
39
|
+
const failures = await gate.run({ cwd: testDir });
|
|
40
|
+
expect(failures).toHaveLength(0);
|
|
41
|
+
});
|
|
42
|
+
it('respects config toggles', async () => {
|
|
43
|
+
fs.writeFileSync(path.join(testDir, 'app.ts'), `
|
|
44
|
+
// TODO: one
|
|
45
|
+
// FIXME: two
|
|
46
|
+
`);
|
|
47
|
+
const todoOnly = new ContentGate({ forbidTodos: true, forbidFixme: false });
|
|
48
|
+
const todoFailures = await todoOnly.run({ cwd: testDir });
|
|
49
|
+
expect(todoFailures).toHaveLength(1);
|
|
50
|
+
expect(todoFailures[0].details).toContain("TODO");
|
|
51
|
+
const fixmeOnly = new ContentGate({ forbidTodos: false, forbidFixme: true });
|
|
52
|
+
const fixmeFailures = await fixmeOnly.run({ cwd: testDir });
|
|
53
|
+
expect(fixmeFailures).toHaveLength(1);
|
|
54
|
+
expect(fixmeFailures[0].details).toContain("FIXME");
|
|
55
|
+
});
|
|
56
|
+
it('does not flag explanatory mentions of TODO/FIXME in comments', async () => {
|
|
57
|
+
fs.writeFileSync(path.join(testDir, 'notes.ts'), `
|
|
58
|
+
// counts TODO and FIXME markers from parsed files
|
|
59
|
+
const metrics = true;
|
|
60
|
+
`);
|
|
61
|
+
const gate = new ContentGate({ forbidTodos: true, forbidFixme: true });
|
|
62
|
+
const failures = await gate.run({ cwd: testDir });
|
|
63
|
+
expect(failures).toHaveLength(0);
|
|
64
|
+
});
|
|
65
|
+
it('skips test/spec files', async () => {
|
|
66
|
+
fs.writeFileSync(path.join(testDir, 'sample.test.ts'), `
|
|
67
|
+
// TODO: this should not be checked by content gate
|
|
68
|
+
`);
|
|
69
|
+
const gate = new ContentGate({ forbidTodos: true, forbidFixme: true });
|
|
70
|
+
const failures = await gate.run({ cwd: testDir });
|
|
71
|
+
expect(failures).toHaveLength(0);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -28,6 +28,7 @@ export declare class ContextWindowArtifactsGate extends Gate {
|
|
|
28
28
|
constructor(config?: ContextWindowArtifactsConfig);
|
|
29
29
|
protected get provenance(): Provenance;
|
|
30
30
|
run(context: GateContext): Promise<Failure[]>;
|
|
31
|
+
private shouldSkipFile;
|
|
31
32
|
private analyzeFile;
|
|
32
33
|
private measureHalf;
|
|
33
34
|
private measureFunctionLengths;
|
|
@@ -26,9 +26,9 @@ export class ContextWindowArtifactsGate extends Gate {
|
|
|
26
26
|
super('context-window-artifacts', 'Context Window Artifact Detection');
|
|
27
27
|
this.config = {
|
|
28
28
|
enabled: config.enabled ?? true,
|
|
29
|
-
min_file_lines: config.min_file_lines ??
|
|
30
|
-
degradation_threshold: config.degradation_threshold ?? 0.
|
|
31
|
-
signals_required: config.signals_required ??
|
|
29
|
+
min_file_lines: config.min_file_lines ?? 180,
|
|
30
|
+
degradation_threshold: config.degradation_threshold ?? 0.55,
|
|
31
|
+
signals_required: config.signals_required ?? 4,
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
get provenance() { return 'ai-drift'; }
|
|
@@ -43,6 +43,8 @@ export class ContextWindowArtifactsGate extends Gate {
|
|
|
43
43
|
});
|
|
44
44
|
Logger.info(`Context Window Artifacts: Scanning ${files.length} files`);
|
|
45
45
|
for (const file of files) {
|
|
46
|
+
if (this.shouldSkipFile(file))
|
|
47
|
+
continue;
|
|
46
48
|
try {
|
|
47
49
|
const content = await fs.readFile(path.join(context.cwd, file), 'utf-8');
|
|
48
50
|
const lines = content.split('\n');
|
|
@@ -60,6 +62,11 @@ export class ContextWindowArtifactsGate extends Gate {
|
|
|
60
62
|
}
|
|
61
63
|
return failures;
|
|
62
64
|
}
|
|
65
|
+
shouldSkipFile(file) {
|
|
66
|
+
const normalized = file.replace(/\\/g, '/');
|
|
67
|
+
return (normalized.includes('/examples/') ||
|
|
68
|
+
normalized.includes('/src/gates/'));
|
|
69
|
+
}
|
|
63
70
|
analyzeFile(content, file) {
|
|
64
71
|
const lines = content.split('\n');
|
|
65
72
|
const midpoint = Math.floor(lines.length / 2);
|
package/dist/gates/context.d.ts
CHANGED
package/dist/gates/context.js
CHANGED
|
@@ -24,7 +24,13 @@ export class ContextGate extends Gate {
|
|
|
24
24
|
const record = context.record;
|
|
25
25
|
if (!record || !this.extendedConfig.enabled)
|
|
26
26
|
return [];
|
|
27
|
-
const files = await FileScanner.findFiles({
|
|
27
|
+
const files = await FileScanner.findFiles({
|
|
28
|
+
cwd: context.cwd,
|
|
29
|
+
patterns: ['**/*.{ts,js,tsx,jsx,py,go,java,cs,rb,kt,swift,rs,php}'],
|
|
30
|
+
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
|
|
31
|
+
'**/studio-dist/**', '**/.next/**', '**/coverage/**', '**/out/**',
|
|
32
|
+
'**/*.test.*', '**/*.spec.*', '**/examples/**', '**/docs/**'],
|
|
33
|
+
});
|
|
28
34
|
const envAnchors = record.anchors.filter(a => a.type === 'env' && a.confidence >= 1);
|
|
29
35
|
// Collect all patterns across files for cross-file analysis
|
|
30
36
|
const namingPatterns = new Map();
|
|
@@ -32,12 +38,13 @@ export class ContextGate extends Gate {
|
|
|
32
38
|
for (const file of files) {
|
|
33
39
|
try {
|
|
34
40
|
const content = await fs.readFile(path.join(context.cwd, file), 'utf-8');
|
|
41
|
+
const codeContent = this.stripComments(content);
|
|
35
42
|
// 1. Original: Detect Redundant Suffixes (The Golden Example)
|
|
36
|
-
this.checkEnvDrift(
|
|
43
|
+
this.checkEnvDrift(codeContent, file, envAnchors, failures);
|
|
37
44
|
// 2. NEW: Cross-file pattern collection
|
|
38
45
|
if (this.extendedConfig.cross_file_patterns) {
|
|
39
|
-
this.collectNamingPatterns(
|
|
40
|
-
this.collectImportPatterns(
|
|
46
|
+
this.collectNamingPatterns(codeContent, file, namingPatterns);
|
|
47
|
+
this.collectImportPatterns(codeContent, file, importPatterns);
|
|
41
48
|
}
|
|
42
49
|
}
|
|
43
50
|
catch (e) { }
|
|
@@ -158,11 +165,13 @@ export class ContextGate extends Gate {
|
|
|
158
165
|
if (imp.startsWith('.') || imp.startsWith('..')) {
|
|
159
166
|
relativeCount.set(file, (relativeCount.get(file) || 0) + 1);
|
|
160
167
|
}
|
|
161
|
-
else if (!imp.startsWith('@') && !imp.includes('/')) {
|
|
162
|
-
// Skip external packages
|
|
163
|
-
}
|
|
164
168
|
else {
|
|
165
|
-
|
|
169
|
+
// Count only local alias styles as "absolute local imports".
|
|
170
|
+
// Scoped packages like @rigour-labs/core should be treated as external.
|
|
171
|
+
const isLocalAlias = imp.startsWith('@/') || imp.startsWith('~/') || imp.startsWith('src/');
|
|
172
|
+
if (isLocalAlias) {
|
|
173
|
+
absoluteCount.set(file, (absoluteCount.get(file) || 0) + 1);
|
|
174
|
+
}
|
|
166
175
|
}
|
|
167
176
|
}
|
|
168
177
|
}
|
|
@@ -203,4 +212,16 @@ export class ContextGate extends Gate {
|
|
|
203
212
|
}
|
|
204
213
|
patterns.get(type).push(entry);
|
|
205
214
|
}
|
|
215
|
+
stripComments(content) {
|
|
216
|
+
// Remove block comments first, then strip line comments.
|
|
217
|
+
const noBlockComments = content.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
218
|
+
const lines = noBlockComments.split('\n').map((line) => {
|
|
219
|
+
const trimmed = line.trim();
|
|
220
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('#')) {
|
|
221
|
+
return '';
|
|
222
|
+
}
|
|
223
|
+
return line.replace(/\/\/.*$/, '');
|
|
224
|
+
});
|
|
225
|
+
return lines.join('\n');
|
|
226
|
+
}
|
|
206
227
|
}
|
|
@@ -40,10 +40,10 @@ export class DeepAnalysisGate extends Gate {
|
|
|
40
40
|
]);
|
|
41
41
|
const isLocal = !this.config.options.apiKey || this.config.options.provider === 'local';
|
|
42
42
|
if (isLocal) {
|
|
43
|
-
onProgress?.('\n 🔒
|
|
43
|
+
onProgress?.('\n 🔒 Local sidecar/model execution. Code remains on this machine.\n');
|
|
44
44
|
}
|
|
45
45
|
else {
|
|
46
|
-
onProgress?.(`\n ☁️ Using ${this.config.options.provider} API. Code
|
|
46
|
+
onProgress?.(`\n ☁️ Using ${this.config.options.provider} API. Code context may be sent to the provider.\n`);
|
|
47
47
|
}
|
|
48
48
|
// Step 1: AST extracts facts
|
|
49
49
|
onProgress?.(' Extracting code facts...');
|
|
@@ -46,6 +46,7 @@ export declare class DeprecatedApisGate extends Gate {
|
|
|
46
46
|
constructor(config?: DeprecatedApisConfig);
|
|
47
47
|
protected get provenance(): Provenance;
|
|
48
48
|
run(context: GateContext): Promise<Failure[]>;
|
|
49
|
+
private shouldSkipFile;
|
|
49
50
|
private checkNodeDeprecated;
|
|
50
51
|
private checkWebDeprecated;
|
|
51
52
|
private checkPythonDeprecated;
|