@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.
Files changed (49) hide show
  1. package/dist/gates/ast-handlers/typescript.js +39 -12
  2. package/dist/gates/ast-handlers/universal.js +9 -3
  3. package/dist/gates/ast.js +15 -1
  4. package/dist/gates/ast.test.d.ts +1 -0
  5. package/dist/gates/ast.test.js +112 -0
  6. package/dist/gates/content.d.ts +5 -0
  7. package/dist/gates/content.js +66 -7
  8. package/dist/gates/content.test.d.ts +1 -0
  9. package/dist/gates/content.test.js +73 -0
  10. package/dist/gates/context-window-artifacts.d.ts +1 -0
  11. package/dist/gates/context-window-artifacts.js +10 -3
  12. package/dist/gates/context.d.ts +1 -0
  13. package/dist/gates/context.js +29 -8
  14. package/dist/gates/deep-analysis.js +2 -2
  15. package/dist/gates/deprecated-apis.d.ts +1 -0
  16. package/dist/gates/deprecated-apis.js +15 -2
  17. package/dist/gates/hallucinated-imports.d.ts +14 -0
  18. package/dist/gates/hallucinated-imports.js +267 -60
  19. package/dist/gates/hallucinated-imports.test.js +164 -1
  20. package/dist/gates/inconsistent-error-handling.d.ts +1 -0
  21. package/dist/gates/inconsistent-error-handling.js +12 -1
  22. package/dist/gates/phantom-apis.d.ts +2 -0
  23. package/dist/gates/phantom-apis.js +28 -3
  24. package/dist/gates/phantom-apis.test.js +14 -0
  25. package/dist/gates/promise-safety.d.ts +2 -0
  26. package/dist/gates/promise-safety.js +31 -9
  27. package/dist/gates/runner.js +8 -2
  28. package/dist/gates/runner.test.d.ts +1 -0
  29. package/dist/gates/runner.test.js +65 -0
  30. package/dist/gates/security-patterns.d.ts +1 -0
  31. package/dist/gates/security-patterns.js +22 -6
  32. package/dist/gates/security-patterns.test.js +18 -0
  33. package/dist/hooks/templates.d.ts +1 -1
  34. package/dist/hooks/templates.js +12 -12
  35. package/dist/inference/executable.d.ts +6 -0
  36. package/dist/inference/executable.js +29 -0
  37. package/dist/inference/executable.test.d.ts +1 -0
  38. package/dist/inference/executable.test.js +41 -0
  39. package/dist/inference/model-manager.d.ts +3 -1
  40. package/dist/inference/model-manager.js +76 -8
  41. package/dist/inference/model-manager.test.d.ts +1 -0
  42. package/dist/inference/model-manager.test.js +24 -0
  43. package/dist/inference/sidecar-provider.d.ts +1 -0
  44. package/dist/inference/sidecar-provider.js +124 -31
  45. package/dist/services/context-engine.js +1 -1
  46. package/dist/templates/universal-config.js +3 -3
  47. package/dist/types/index.js +3 -3
  48. package/dist/utils/scanner.js +6 -0
  49. 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 = 50;
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 ignore = (context.ignore || ['**/node_modules/**', '**/dist/**', '**/build/**', '**/*.test.*', '**/*.spec.*', '**/__pycache__/**']).map(p => p.replace(/\\/g, '/'));
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
+ });
@@ -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
  }
@@ -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 patterns = [];
11
+ const markers = [];
11
12
  if (this.config.forbidTodos)
12
- patterns.push(/TODO/i);
13
+ markers.push('TODO');
13
14
  if (this.config.forbidFixme)
14
- patterns.push(/FIXME/i);
15
- if (patterns.length === 0)
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
- for (const pattern of patterns) {
28
- if (pattern.test(line)) {
29
- failures.push(this.createFailure(`Forbidden placeholder '${pattern.source}' found`, [file], 'Remove forbidden comments. address the root cause or create a tracked issue.', undefined, index + 1, index + 1, 'info'));
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 ?? 100,
30
- degradation_threshold: config.degradation_threshold ?? 0.4,
31
- signals_required: config.signals_required ?? 2,
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);
@@ -41,4 +41,5 @@ export declare class ContextGate extends Gate {
41
41
  */
42
42
  private detectCasing;
43
43
  private addPattern;
44
+ private stripComments;
44
45
  }
@@ -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({ cwd: context.cwd });
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(content, file, envAnchors, failures);
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(content, file, namingPatterns);
40
- this.collectImportPatterns(content, file, importPatterns);
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
- absoluteCount.set(file, (absoluteCount.get(file) || 0) + 1);
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 🔒 100% local analysis. Your code never leaves this machine.\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 is sent to cloud.\n`);
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;