@rigour-labs/core 6.2.3 → 6.2.4
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/hallucinated-imports/index.js +13 -9
- package/dist/gates/hallucinated-imports.test.js +10 -0
- package/dist/gates/js-style-context.d.ts +3 -0
- package/dist/gates/js-style-context.js +65 -0
- package/dist/gates/js-style-context.test.d.ts +1 -0
- package/dist/gates/js-style-context.test.js +35 -0
- package/dist/gates/logic-drift-git-base.d.ts +7 -0
- package/dist/gates/logic-drift-git-base.js +41 -0
- package/dist/gates/logic-drift.d.ts +3 -3
- package/dist/gates/logic-drift.js +33 -7
- package/dist/gates/logic-drift.test.d.ts +1 -0
- package/dist/gates/logic-drift.test.js +45 -0
- package/dist/gates/security-command-execution.d.ts +5 -0
- package/dist/gates/security-command-execution.js +123 -0
- package/dist/gates/security-patterns-data.js +1 -17
- package/dist/gates/security-patterns.d.ts +0 -5
- package/dist/gates/security-patterns.js +11 -19
- package/dist/gates/security-patterns.test.js +37 -2
- package/dist/gates/style-drift.d.ts +4 -4
- package/dist/gates/style-drift.js +12 -16
- package/dist/gates/style-drift.test.d.ts +1 -0
- package/dist/gates/style-drift.test.js +26 -0
- package/package.json +6 -6
|
@@ -48,21 +48,25 @@ export class HallucinatedImportsGate extends Gate {
|
|
|
48
48
|
const hallucinated = [];
|
|
49
49
|
const defaultPatterns = ['**/*.{ts,js,tsx,jsx,py,go,rb,cs,rs,java,kt}'];
|
|
50
50
|
const scanPatterns = context.patterns || defaultPatterns;
|
|
51
|
+
const ignore = [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
|
|
52
|
+
'**/examples/**', '**/studio-dist/**', '**/.next/**', '**/coverage/**',
|
|
53
|
+
'**/*.test.*', '**/*.spec.*', '**/__tests__/**',
|
|
54
|
+
'**/.venv/**', '**/venv/**', '**/vendor/**', '**/bin/Debug/**', '**/bin/Release/**', '**/obj/**',
|
|
55
|
+
'**/target/debug/**', '**/target/release/**', '**/out/**', '**/.gradle/**', '**/gradle/**'];
|
|
51
56
|
const files = await FileScanner.findFiles({
|
|
52
57
|
cwd: context.cwd,
|
|
53
58
|
patterns: scanPatterns,
|
|
54
|
-
ignore
|
|
55
|
-
'**/examples/**',
|
|
56
|
-
'**/studio-dist/**', '**/.next/**', '**/coverage/**',
|
|
57
|
-
'**/*.test.*', '**/*.spec.*', '**/__tests__/**',
|
|
58
|
-
'**/.venv/**', '**/venv/**', '**/vendor/**', '**/bin/Debug/**', '**/bin/Release/**', '**/obj/**',
|
|
59
|
-
'**/target/debug/**', '**/target/release/**',
|
|
60
|
-
'**/out/**', '**/.gradle/**', '**/gradle/**'],
|
|
59
|
+
ignore,
|
|
61
60
|
});
|
|
62
61
|
const analyzableFiles = files.filter(file => !this.shouldSkipFile(file));
|
|
63
62
|
Logger.info(`Hallucinated Imports: Scanning ${analyzableFiles.length} files`);
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
// A scoped review scans changed files, but relative imports may point
|
|
64
|
+
// to unchanged files. Resolve against the project, not the edit set.
|
|
65
|
+
const resolutionFiles = context.patterns
|
|
66
|
+
? await FileScanner.findFiles({ cwd: context.cwd, patterns: defaultPatterns, ignore })
|
|
67
|
+
: files;
|
|
68
|
+
const projectFiles = new Set(resolutionFiles.map(f => f.replace(/\\/g, '/')));
|
|
69
|
+
const allProjectFiles = projectFiles;
|
|
66
70
|
const packageJson = await loadPackageJson(context.cwd);
|
|
67
71
|
const rootDeps = new Set([
|
|
68
72
|
...Object.keys(packageJson?.dependencies || {}),
|
|
@@ -264,6 +264,16 @@ describe('HallucinatedImportsGate — JS/TS Node builtins', () => {
|
|
|
264
264
|
mockReaddirSync.mockReturnValue([]);
|
|
265
265
|
gate = new HallucinatedImportsGate({ enabled: true });
|
|
266
266
|
});
|
|
267
|
+
it('resolves changed-file imports against unchanged project files', async () => {
|
|
268
|
+
FileScanner.findFiles
|
|
269
|
+
.mockResolvedValueOnce(['src/feature.ts'])
|
|
270
|
+
.mockResolvedValueOnce(['src/feature.ts', 'src/types.ts']);
|
|
271
|
+
mockReadFile.mockResolvedValue("import type { Value } from './types.js';\n");
|
|
272
|
+
mockPathExists.mockResolvedValue(false);
|
|
273
|
+
const failures = await gate.run({ ...context, patterns: ['src/feature.ts'] });
|
|
274
|
+
expect(failures).toEqual([]);
|
|
275
|
+
expect(FileScanner.findFiles).toHaveBeenCalledTimes(2);
|
|
276
|
+
});
|
|
267
277
|
it('should NOT flag Node.js built-in modules', async () => {
|
|
268
278
|
const jsContent = `
|
|
269
279
|
import fs from 'fs';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { classifyCasing } from './language-adapters/types.js';
|
|
3
|
+
const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
|
|
4
|
+
function isNextRoute(file) {
|
|
5
|
+
return /(?:^|\/)app(?:\/[^/]+)*\/route\.[cm]?[jt]sx?$/.test(file.replace(/\\/g, '/'));
|
|
6
|
+
}
|
|
7
|
+
function exported(node) {
|
|
8
|
+
return ts.canHaveModifiers(node)
|
|
9
|
+
&& Boolean(ts.getModifiers(node)?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword));
|
|
10
|
+
}
|
|
11
|
+
function containsJsx(node) {
|
|
12
|
+
let found = false;
|
|
13
|
+
const visit = (child) => {
|
|
14
|
+
if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child) || ts.isJsxFragment(child)) {
|
|
15
|
+
found = true;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (!found)
|
|
19
|
+
ts.forEachChild(child, visit);
|
|
20
|
+
};
|
|
21
|
+
ts.forEachChild(node, visit);
|
|
22
|
+
return found;
|
|
23
|
+
}
|
|
24
|
+
function isComponent(name, node, file) {
|
|
25
|
+
return /\.[jt]sx$/.test(file) && /^[A-Z]/.test(name) && containsJsx(node);
|
|
26
|
+
}
|
|
27
|
+
function isModuleConstant(declaration) {
|
|
28
|
+
const list = declaration.parent;
|
|
29
|
+
return ts.isVariableDeclarationList(list)
|
|
30
|
+
&& (list.flags & ts.NodeFlags.Const) !== 0
|
|
31
|
+
&& ts.isVariableStatement(list.parent)
|
|
32
|
+
&& ts.isSourceFile(list.parent.parent);
|
|
33
|
+
}
|
|
34
|
+
function isFunctionValue(initializer) {
|
|
35
|
+
return Boolean(initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)));
|
|
36
|
+
}
|
|
37
|
+
/** Extract comparable JS/TS names, excluding framework-defined identifiers. */
|
|
38
|
+
export function extractComparableJsNames(content, file) {
|
|
39
|
+
const kind = /\.[jt]sx$/.test(file) ? ts.ScriptKind.TSX : /\.[cm]?ts$/.test(file) ? ts.ScriptKind.TS : ts.ScriptKind.JS;
|
|
40
|
+
const source = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true, kind);
|
|
41
|
+
const patterns = [];
|
|
42
|
+
const route = isNextRoute(file);
|
|
43
|
+
const visit = (node) => {
|
|
44
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
45
|
+
const name = node.name.text;
|
|
46
|
+
if (!(route && exported(node) && HTTP_METHODS.has(name)) && !isComponent(name, node, file)) {
|
|
47
|
+
patterns.push({ name, kind: 'function', convention: classifyCasing(name) });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
51
|
+
const name = node.name.text;
|
|
52
|
+
const fn = isFunctionValue(node.initializer);
|
|
53
|
+
const statement = node.parent.parent;
|
|
54
|
+
const routeExport = route && HTTP_METHODS.has(name) && ts.isVariableStatement(statement) && exported(statement);
|
|
55
|
+
const component = fn && isComponent(name, node.initializer, file);
|
|
56
|
+
const conventionalConstant = !fn && isModuleConstant(node) && classifyCasing(name) === 'SCREAMING_SNAKE';
|
|
57
|
+
if (!routeExport && !component && !conventionalConstant) {
|
|
58
|
+
patterns.push({ name, kind: fn ? 'function' : 'variable', convention: classifyCasing(name) });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
ts.forEachChild(node, visit);
|
|
62
|
+
};
|
|
63
|
+
visit(source);
|
|
64
|
+
return patterns;
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { extractComparableJsNames } from './js-style-context.js';
|
|
3
|
+
describe('context-aware JS naming', () => {
|
|
4
|
+
it('excludes exported Next route methods only inside app route files', () => {
|
|
5
|
+
const code = `
|
|
6
|
+
export async function GET() { return Response.json({}); }
|
|
7
|
+
export const POST = async () => Response.json({});
|
|
8
|
+
function GET_HELPER() { return 1; }
|
|
9
|
+
`;
|
|
10
|
+
expect(extractComparableJsNames(code, 'src/app/api/items/route.ts').map(item => item.name))
|
|
11
|
+
.toEqual(['GET_HELPER']);
|
|
12
|
+
expect(extractComparableJsNames(code, 'src/lib/route.ts').map(item => item.name))
|
|
13
|
+
.toEqual(['GET', 'POST', 'GET_HELPER']);
|
|
14
|
+
});
|
|
15
|
+
it('excludes JSX-returning components but keeps ordinary PascalCase functions', () => {
|
|
16
|
+
const code = `
|
|
17
|
+
export function StepReview() { return <section />; }
|
|
18
|
+
const WizardField = () => <input />;
|
|
19
|
+
function ParseValue() { return 1; }
|
|
20
|
+
`;
|
|
21
|
+
expect(extractComparableJsNames(code, 'src/components/WizardField.tsx').map(item => item.name))
|
|
22
|
+
.toEqual(['ParseValue']);
|
|
23
|
+
});
|
|
24
|
+
it('excludes module constants but keeps unconventional local variables', () => {
|
|
25
|
+
const code = `
|
|
26
|
+
const EMAIL_TEMPLATE = 'hello';
|
|
27
|
+
function sendMessage() {
|
|
28
|
+
const LOCAL_VALUE = 1;
|
|
29
|
+
return LOCAL_VALUE;
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
32
|
+
expect(extractComparableJsNames(code, 'src/lib/template.ts').map(item => item.name))
|
|
33
|
+
.toEqual(['sendMessage', 'LOCAL_VALUE']);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface GitLogicBase {
|
|
2
|
+
changedFiles: Set<string>;
|
|
3
|
+
readAtBase: (file: string) => string | null;
|
|
4
|
+
}
|
|
5
|
+
export declare function isGitWorktree(cwd: string): boolean;
|
|
6
|
+
/** Use a fixed commit for the entire scan; never move the baseline on read. */
|
|
7
|
+
export declare function resolveGitLogicBase(cwd: string): GitLogicBase | null;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
export function isGitWorktree(cwd) {
|
|
3
|
+
return git(cwd, ['rev-parse', '--is-inside-work-tree'])?.trim() === 'true';
|
|
4
|
+
}
|
|
5
|
+
function git(cwd, args) {
|
|
6
|
+
try {
|
|
7
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 16 * 1024 * 1024 });
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Use a fixed commit for the entire scan; never move the baseline on read. */
|
|
14
|
+
export function resolveGitLogicBase(cwd) {
|
|
15
|
+
// Git reports the path relative to the worktree root without relying on
|
|
16
|
+
// platform-specific path casing or Windows short-name expansion.
|
|
17
|
+
const prefix = git(cwd, ['rev-parse', '--show-prefix']);
|
|
18
|
+
if (prefix === null || prefix.trim())
|
|
19
|
+
return null;
|
|
20
|
+
const currentBranch = git(cwd, ['symbolic-ref', '--quiet', '--short', 'HEAD'])?.trim();
|
|
21
|
+
const candidates = [
|
|
22
|
+
process.env.GITHUB_BASE_REF ? `refs/remotes/origin/${process.env.GITHUB_BASE_REF}` : '',
|
|
23
|
+
'refs/remotes/origin/main', 'refs/heads/main',
|
|
24
|
+
'refs/remotes/origin/master', 'refs/heads/master',
|
|
25
|
+
].filter(Boolean);
|
|
26
|
+
const mainRef = candidates.find(ref => git(cwd, ['rev-parse', '--verify', '--quiet', ref])?.trim());
|
|
27
|
+
if (!mainRef)
|
|
28
|
+
return null;
|
|
29
|
+
const base = currentBranch === 'main' || currentBranch === 'master'
|
|
30
|
+
? git(cwd, ['rev-parse', 'HEAD'])?.trim()
|
|
31
|
+
: git(cwd, ['merge-base', 'HEAD', mainRef])?.trim();
|
|
32
|
+
if (!base)
|
|
33
|
+
return null;
|
|
34
|
+
const diff = git(cwd, ['diff', '--name-only', '-z', '--diff-filter=ACMR', base, '--']);
|
|
35
|
+
if (diff === null)
|
|
36
|
+
return null;
|
|
37
|
+
return {
|
|
38
|
+
changedFiles: new Set(diff.split('\0').filter(Boolean)),
|
|
39
|
+
readAtBase: file => git(cwd, ['show', `${base}:${file}`]),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
* - Tests might still pass (if they don't cover edge cases)
|
|
13
13
|
* - The change looks intentional ("AI refactored the function")
|
|
14
14
|
*
|
|
15
|
-
* Strategy:
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Strategy: Compare changed functions against the Git main merge base.
|
|
16
|
+
* Repeated scans of the same change must report the same review signals.
|
|
17
|
+
* Non-Git projects retain the legacy local baseline behavior.
|
|
18
18
|
*/
|
|
19
19
|
import { Gate, GateContext } from './base.js';
|
|
20
20
|
import { Failure, Provenance } from '../types/index.js';
|
|
@@ -12,15 +12,16 @@
|
|
|
12
12
|
* - Tests might still pass (if they don't cover edge cases)
|
|
13
13
|
* - The change looks intentional ("AI refactored the function")
|
|
14
14
|
*
|
|
15
|
-
* Strategy:
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Strategy: Compare changed functions against the Git main merge base.
|
|
16
|
+
* Repeated scans of the same change must report the same review signals.
|
|
17
|
+
* Non-Git projects retain the legacy local baseline behavior.
|
|
18
18
|
*/
|
|
19
19
|
import { Gate } from './base.js';
|
|
20
20
|
import { FileScanner } from '../utils/scanner.js';
|
|
21
21
|
import { Logger } from '../utils/logger.js';
|
|
22
22
|
import { languageAdapters } from './language-adapters/index.js';
|
|
23
23
|
import { extractCallSequence, isDangerousMutation } from './logic-drift-extractors.js';
|
|
24
|
+
import { isGitWorktree, resolveGitLogicBase } from './logic-drift-git-base.js';
|
|
24
25
|
import fs from 'fs-extra';
|
|
25
26
|
import path from 'path';
|
|
26
27
|
import crypto from 'crypto';
|
|
@@ -50,16 +51,35 @@ export class LogicDriftGate extends Gate {
|
|
|
50
51
|
});
|
|
51
52
|
if (files.length === 0)
|
|
52
53
|
return [];
|
|
54
|
+
// In Git worktrees, compare only files changed from an immutable base.
|
|
55
|
+
const gitBase = resolveGitLogicBase(context.cwd);
|
|
56
|
+
if (!gitBase && isGitWorktree(context.cwd)) {
|
|
57
|
+
Logger.info('Logic Drift: no main reference available; comparison unavailable');
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
const scanFiles = gitBase ? files.filter(file => gitBase.changedFiles.has(file)) : files;
|
|
61
|
+
if (gitBase && scanFiles.length === 0)
|
|
62
|
+
return [];
|
|
53
63
|
// Extract current function baselines
|
|
54
64
|
const currentFunctions = [];
|
|
55
|
-
const contents = await FileScanner.readFiles(context.cwd,
|
|
65
|
+
const contents = await FileScanner.readFiles(context.cwd, scanFiles, context.fileCache);
|
|
56
66
|
for (const [file, content] of contents) {
|
|
57
67
|
const extracted = this.extractFunctionBaselines(content, file);
|
|
58
68
|
currentFunctions.push(...extracted);
|
|
59
69
|
}
|
|
60
|
-
//
|
|
70
|
+
// Git is authoritative when present; old local snapshots are fallback
|
|
71
|
+
// only for projects without an available Git main reference.
|
|
61
72
|
let previousBaseline = null;
|
|
62
|
-
if (
|
|
73
|
+
if (gitBase) {
|
|
74
|
+
const functions = [];
|
|
75
|
+
for (const file of scanFiles) {
|
|
76
|
+
const content = gitBase.readAtBase(file);
|
|
77
|
+
if (content !== null)
|
|
78
|
+
functions.push(...this.extractFunctionBaselines(content, file));
|
|
79
|
+
}
|
|
80
|
+
previousBaseline = { functions, createdAt: '', lastUpdated: '', scanCount: 0 };
|
|
81
|
+
}
|
|
82
|
+
else if (await fs.pathExists(baselinePath)) {
|
|
63
83
|
try {
|
|
64
84
|
const raw = await fs.readJson(baselinePath);
|
|
65
85
|
// Validate baseline has required structure
|
|
@@ -131,7 +151,8 @@ export class LogicDriftGate extends Gate {
|
|
|
131
151
|
lastUpdated: new Date().toISOString(),
|
|
132
152
|
scanCount: previousBaseline.scanCount + 1,
|
|
133
153
|
};
|
|
134
|
-
|
|
154
|
+
if (!gitBase)
|
|
155
|
+
await fs.writeJson(baselinePath, updatedBaseline, { spaces: 2 });
|
|
135
156
|
if (failures.length > 0) {
|
|
136
157
|
Logger.info(`Logic Drift: Found ${failures.length} logic mutations`);
|
|
137
158
|
}
|
|
@@ -146,7 +167,12 @@ export class LogicDriftGate extends Gate {
|
|
|
146
167
|
}
|
|
147
168
|
const skipKeywords = new Set(['if', 'for', 'while', 'switch', 'catch', 'constructor', 'else', 'elif', 'elsif', 'rescue']);
|
|
148
169
|
const functions = adapter.extractFunctions(content, file);
|
|
170
|
+
const seen = new Set();
|
|
149
171
|
for (const fn of functions) {
|
|
172
|
+
const identity = `${fn.name}:${fn.startLine}`;
|
|
173
|
+
if (seen.has(identity))
|
|
174
|
+
continue;
|
|
175
|
+
seen.add(identity);
|
|
150
176
|
// Skip control flow keywords
|
|
151
177
|
if (skipKeywords.has(fn.name))
|
|
152
178
|
continue;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { LogicDriftGate } from './logic-drift.js';
|
|
7
|
+
describe('LogicDriftGate Git baseline', () => {
|
|
8
|
+
let cwd;
|
|
9
|
+
const git = (...args) => execFileSync('git', args, { cwd, stdio: 'pipe' });
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'logic-drift-'));
|
|
12
|
+
git('init', '-q', '-b', 'main');
|
|
13
|
+
git('config', 'user.name', 'Rigour Test');
|
|
14
|
+
git('config', 'user.email', 'test@example.invalid');
|
|
15
|
+
fs.mkdirSync(path.join(cwd, 'src'));
|
|
16
|
+
fs.writeFileSync(path.join(cwd, 'src/rules.ts'), 'export function eligible(score: number) { return score >= 10; }\n');
|
|
17
|
+
git('add', '.');
|
|
18
|
+
git('commit', '-qm', 'base');
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => { fs.rmSync(cwd, { recursive: true, force: true }); });
|
|
21
|
+
it('reports a branch mutation consistently and clears it after merge', async () => {
|
|
22
|
+
const gate = new LogicDriftGate();
|
|
23
|
+
expect(await gate.run({ cwd })).toEqual([]);
|
|
24
|
+
git('switch', '-q', '-c', 'feature');
|
|
25
|
+
fs.writeFileSync(path.join(cwd, 'src/rules.ts'), 'export function eligible(score: number) { return score > 10; }\n');
|
|
26
|
+
const first = await gate.run({ cwd });
|
|
27
|
+
const second = await gate.run({ cwd });
|
|
28
|
+
expect(first).toEqual(second);
|
|
29
|
+
expect(first).toHaveLength(1);
|
|
30
|
+
expect(first.some(finding => finding.details.includes("'>=' to '>'"))).toBe(true);
|
|
31
|
+
expect(fs.existsSync(path.join(cwd, '.rigour/logic-baseline.json'))).toBe(false);
|
|
32
|
+
git('add', '.');
|
|
33
|
+
git('commit', '-qm', 'change boundary');
|
|
34
|
+
git('switch', '-q', 'main');
|
|
35
|
+
git('merge', '-q', '--ff-only', 'feature');
|
|
36
|
+
expect(await gate.run({ cwd })).toEqual([]);
|
|
37
|
+
}, 30_000);
|
|
38
|
+
it('does not use a moving local snapshot when Git has no main reference', async () => {
|
|
39
|
+
git('switch', '-q', '-c', 'feature');
|
|
40
|
+
git('branch', '-D', 'main');
|
|
41
|
+
const gate = new LogicDriftGate();
|
|
42
|
+
expect(await gate.run({ cwd })).toEqual([]);
|
|
43
|
+
expect(fs.existsSync(path.join(cwd, '.rigour/logic-baseline.json'))).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
const SHELL_MODULES = new Set(['child_process', 'node:child_process']);
|
|
3
|
+
const EXEC_METHODS = new Set(['exec', 'execSync']);
|
|
4
|
+
const SPAWN_METHODS = new Set(['spawn', 'spawnSync']);
|
|
5
|
+
const USER_INPUT = /\b(?:req|request|query|params|body|input|user|argv)\b|process\.env/;
|
|
6
|
+
function shellModule(node) {
|
|
7
|
+
return ts.isStringLiteral(node) && SHELL_MODULES.has(node.text);
|
|
8
|
+
}
|
|
9
|
+
function isShellRequire(node) {
|
|
10
|
+
return ts.isCallExpression(node)
|
|
11
|
+
&& ts.isIdentifier(node.expression)
|
|
12
|
+
&& node.expression.text === 'require'
|
|
13
|
+
&& node.arguments.length === 1
|
|
14
|
+
&& shellModule(node.arguments[0]);
|
|
15
|
+
}
|
|
16
|
+
function collectBindings(source) {
|
|
17
|
+
const bindings = { functions: new Map(), namespaces: new Set() };
|
|
18
|
+
for (const statement of source.statements) {
|
|
19
|
+
if (ts.isImportDeclaration(statement) && shellModule(statement.moduleSpecifier)) {
|
|
20
|
+
const clause = statement.importClause;
|
|
21
|
+
if (clause?.name)
|
|
22
|
+
bindings.namespaces.add(clause.name.text);
|
|
23
|
+
const named = clause?.namedBindings;
|
|
24
|
+
if (named && ts.isNamespaceImport(named))
|
|
25
|
+
bindings.namespaces.add(named.name.text);
|
|
26
|
+
if (named && ts.isNamedImports(named)) {
|
|
27
|
+
for (const element of named.elements) {
|
|
28
|
+
const exported = element.propertyName?.text ?? element.name.text;
|
|
29
|
+
if (EXEC_METHODS.has(exported) || SPAWN_METHODS.has(exported)) {
|
|
30
|
+
bindings.functions.set(element.name.text, exported);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!ts.isVariableStatement(statement))
|
|
36
|
+
continue;
|
|
37
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
38
|
+
const initializer = declaration.initializer;
|
|
39
|
+
if (!initializer)
|
|
40
|
+
continue;
|
|
41
|
+
if (isShellRequire(initializer)) {
|
|
42
|
+
if (ts.isIdentifier(declaration.name))
|
|
43
|
+
bindings.namespaces.add(declaration.name.text);
|
|
44
|
+
if (ts.isObjectBindingPattern(declaration.name)) {
|
|
45
|
+
for (const element of declaration.name.elements) {
|
|
46
|
+
const exported = element.propertyName?.getText(source) ?? element.name.getText(source);
|
|
47
|
+
if ((EXEC_METHODS.has(exported) || SPAWN_METHODS.has(exported)) && ts.isIdentifier(element.name)) {
|
|
48
|
+
bindings.functions.set(element.name.text, exported);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (ts.isPropertyAccessExpression(initializer) && isShellRequire(initializer.expression)
|
|
54
|
+
&& ts.isIdentifier(declaration.name)) {
|
|
55
|
+
bindings.functions.set(declaration.name.text, initializer.name.text);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return bindings;
|
|
60
|
+
}
|
|
61
|
+
function shadowsImportedName(node, name) {
|
|
62
|
+
for (let scope = node.parent; scope && !ts.isSourceFile(scope); scope = scope.parent) {
|
|
63
|
+
if (ts.isFunctionLike(scope) && scope.parameters.some(param => ts.isIdentifier(param.name) && param.name.text === name)) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (!ts.isBlock(scope))
|
|
67
|
+
continue;
|
|
68
|
+
for (const statement of scope.statements) {
|
|
69
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === name)
|
|
70
|
+
return true;
|
|
71
|
+
if (!ts.isVariableStatement(statement))
|
|
72
|
+
continue;
|
|
73
|
+
if (statement.declarationList.declarations.some(decl => ts.isIdentifier(decl.name) && decl.name.text === name)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
function shellMethod(call, bindings) {
|
|
81
|
+
const expression = call.expression;
|
|
82
|
+
if (ts.isIdentifier(expression)) {
|
|
83
|
+
return shadowsImportedName(call, expression.text) ? undefined : bindings.functions.get(expression.text);
|
|
84
|
+
}
|
|
85
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
86
|
+
const receiver = expression.expression;
|
|
87
|
+
if (isShellRequire(receiver))
|
|
88
|
+
return expression.name.text;
|
|
89
|
+
if (ts.isIdentifier(receiver) && bindings.namespaces.has(receiver.text)
|
|
90
|
+
&& !shadowsImportedName(call, receiver.text))
|
|
91
|
+
return expression.name.text;
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
function shellEnabled(call) {
|
|
96
|
+
return call.arguments.some(arg => ts.isObjectLiteralExpression(arg)
|
|
97
|
+
&& arg.properties.some(prop => ts.isPropertyAssignment(prop)
|
|
98
|
+
&& (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))
|
|
99
|
+
&& prop.name.text === 'shell'
|
|
100
|
+
&& prop.initializer.kind === ts.SyntaxKind.TrueKeyword));
|
|
101
|
+
}
|
|
102
|
+
function taintedArgument(call, source) {
|
|
103
|
+
return call.arguments.some(arg => USER_INPUT.test(arg.getText(source)));
|
|
104
|
+
}
|
|
105
|
+
export function findUnsafeShellCalls(content, file) {
|
|
106
|
+
const kind = file.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS;
|
|
107
|
+
const source = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true, kind);
|
|
108
|
+
const bindings = collectBindings(source);
|
|
109
|
+
const calls = [];
|
|
110
|
+
const visit = (node) => {
|
|
111
|
+
if (ts.isCallExpression(node)) {
|
|
112
|
+
const method = shellMethod(node, bindings);
|
|
113
|
+
const executesShell = method && (EXEC_METHODS.has(method) || (SPAWN_METHODS.has(method) && shellEnabled(node)));
|
|
114
|
+
if (executesShell && taintedArgument(node, source)) {
|
|
115
|
+
calls.push({ line: source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1,
|
|
116
|
+
text: node.getText(source).slice(0, 120) });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
ts.forEachChild(node, visit);
|
|
120
|
+
};
|
|
121
|
+
visit(source);
|
|
122
|
+
return calls;
|
|
123
|
+
}
|
|
@@ -98,23 +98,7 @@ export const VULNERABILITY_PATTERNS = [
|
|
|
98
98
|
cwe: 'CWE-338',
|
|
99
99
|
languages: ['ts', 'js', 'tsx', 'jsx']
|
|
100
100
|
},
|
|
101
|
-
//
|
|
102
|
-
{
|
|
103
|
-
type: 'command_injection',
|
|
104
|
-
regex: /(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{[^}]*(?:req\.|query|params|body|input|user|argv|process\.env)[^}]*\}[^`]*`|[^)]*(?:req\.|query|params|body|input|user|argv|process\.env)[^)]*)\)/g,
|
|
105
|
-
severity: 'critical',
|
|
106
|
-
description: 'Potential command injection: shell execution with user input',
|
|
107
|
-
cwe: 'CWE-78',
|
|
108
|
-
languages: ['ts', 'js']
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
type: 'command_injection',
|
|
112
|
-
regex: /child_process.*\s*\.\s*(?:exec|spawn)\s*\([^)]*(?:req\.|query|params|body|input|user|argv|process\.env)/g,
|
|
113
|
-
severity: 'high',
|
|
114
|
-
description: 'child_process usage detected (verify input sanitization)',
|
|
115
|
-
cwe: 'CWE-78',
|
|
116
|
-
languages: ['ts', 'js']
|
|
117
|
-
},
|
|
101
|
+
// JavaScript shell calls are checked with receiver-aware syntax analysis.
|
|
118
102
|
// Python subprocess command injection
|
|
119
103
|
{
|
|
120
104
|
type: 'command_injection',
|
|
@@ -61,11 +61,6 @@ export declare class SecurityPatternsGate extends Gate {
|
|
|
61
61
|
* Known sanitizers: DOMPurify.sanitize(), sanitize(), xss(), escapeHtml(), htmlEncode().
|
|
62
62
|
*/
|
|
63
63
|
private isSanitizedAssignment;
|
|
64
|
-
/**
|
|
65
|
-
* Check if a shell execution call is using { shell: false } option (safe pattern).
|
|
66
|
-
* Also, spawn() without { shell: true } is safe by default — don't flag it.
|
|
67
|
-
*/
|
|
68
|
-
private isSafeShellCall;
|
|
69
64
|
}
|
|
70
65
|
/**
|
|
71
66
|
* Quick helper to check a single file for security issues
|
|
@@ -16,6 +16,7 @@ import { Gate } from './base.js';
|
|
|
16
16
|
import { FileScanner } from '../utils/scanner.js';
|
|
17
17
|
import { Logger } from '../utils/logger.js';
|
|
18
18
|
import { VULNERABILITY_PATTERNS } from './security-patterns-data.js';
|
|
19
|
+
import { findUnsafeShellCalls } from './security-command-execution.js';
|
|
19
20
|
import fs from 'fs-extra';
|
|
20
21
|
import path from 'path';
|
|
21
22
|
export class SecurityPatternsGate extends Gate {
|
|
@@ -127,7 +128,16 @@ export class SecurityPatternsGate extends Gate {
|
|
|
127
128
|
return false;
|
|
128
129
|
}
|
|
129
130
|
scanFileForVulnerabilities(content, file, ext, vulnerabilities) {
|
|
130
|
-
|
|
131
|
+
if (ext === 'ts' || ext === 'js') {
|
|
132
|
+
for (const call of findUnsafeShellCalls(content, file)) {
|
|
133
|
+
vulnerabilities.push({
|
|
134
|
+
type: 'command_injection', severity: 'critical', file,
|
|
135
|
+
line: call.line, match: call.text,
|
|
136
|
+
description: 'Potential command injection: shell execution with user input',
|
|
137
|
+
cwe: 'CWE-78',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
131
141
|
for (const pattern of VULNERABILITY_PATTERNS) {
|
|
132
142
|
// Check if pattern applies to this file type
|
|
133
143
|
if (!pattern.languages.includes('*') && !pattern.languages.includes(ext)) {
|
|
@@ -145,10 +155,6 @@ export class SecurityPatternsGate extends Gate {
|
|
|
145
155
|
if (pattern.type === 'xss' && this.isSanitizedAssignment(match[0])) {
|
|
146
156
|
continue;
|
|
147
157
|
}
|
|
148
|
-
// For command_injection: check if spawn/exec uses { shell: false } (safe)
|
|
149
|
-
if (pattern.type === 'command_injection' && this.isSafeShellCall(match[0], content, match.index)) {
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
158
|
// Find line number
|
|
153
159
|
const beforeMatch = content.slice(0, match.index);
|
|
154
160
|
const lineNumber = beforeMatch.split('\n').length;
|
|
@@ -244,20 +250,6 @@ export class SecurityPatternsGate extends Gate {
|
|
|
244
250
|
const rhs = matchText.includes('=') ? matchText.split('=').slice(1).join('=') : matchText;
|
|
245
251
|
return sanitizers.some(s => rhs.toLowerCase().includes(s.toLowerCase()));
|
|
246
252
|
}
|
|
247
|
-
/**
|
|
248
|
-
* Check if a shell execution call is using { shell: false } option (safe pattern).
|
|
249
|
-
* Also, spawn() without { shell: true } is safe by default — don't flag it.
|
|
250
|
-
*/
|
|
251
|
-
isSafeShellCall(matchText, fullContent, matchIndex) {
|
|
252
|
-
// Check surrounding context (100 chars after match) for shell: false
|
|
253
|
-
const context = fullContent.slice(matchIndex, matchIndex + matchText.length + 100);
|
|
254
|
-
if (/shell\s*:\s*false/.test(context))
|
|
255
|
-
return true;
|
|
256
|
-
// spawn() without shell: true is safe by default
|
|
257
|
-
if (/\bspawn(?:Sync)?\s*\(/.test(matchText) && !/shell\s*:\s*true/.test(context))
|
|
258
|
-
return true;
|
|
259
|
-
return false;
|
|
260
|
-
}
|
|
261
253
|
}
|
|
262
254
|
/**
|
|
263
255
|
* Quick helper to check a single file for security issues
|
|
@@ -105,14 +105,49 @@ describe('SecurityPatternsGate', () => {
|
|
|
105
105
|
});
|
|
106
106
|
});
|
|
107
107
|
describe('command injection detection', () => {
|
|
108
|
-
it('
|
|
108
|
+
it('detects imported shell execution with user input', async () => {
|
|
109
109
|
const filePath = path.join(testDir, 'shell.ts');
|
|
110
110
|
fs.writeFileSync(filePath, `
|
|
111
|
-
exec
|
|
111
|
+
import { exec as runShell } from 'node:child_process';
|
|
112
|
+
runShell(\`ls \${req.query.path}\`);
|
|
112
113
|
`);
|
|
113
114
|
const vulns = await checkSecurityPatterns(filePath);
|
|
114
115
|
expect(vulns.some(v => v.type === 'command_injection')).toBe(true);
|
|
115
116
|
});
|
|
117
|
+
it('does not treat RegExp.exec or unrelated methods as shell execution', async () => {
|
|
118
|
+
const filePath = path.join(testDir, 'template.ts');
|
|
119
|
+
fs.writeFileSync(filePath, `
|
|
120
|
+
const separator = HB_ELSE.exec(body);
|
|
121
|
+
const other = LIQUID_BRANCH.exec(user);
|
|
122
|
+
const local = { exec(value: string) { return value; } };
|
|
123
|
+
local.exec(request.body);
|
|
124
|
+
`);
|
|
125
|
+
const vulns = await checkSecurityPatterns(filePath);
|
|
126
|
+
expect(vulns.filter(v => v.type === 'command_injection')).toEqual([]);
|
|
127
|
+
});
|
|
128
|
+
it('detects namespace, require, and shell-enabled spawn calls', async () => {
|
|
129
|
+
const filePath = path.join(testDir, 'shell.js');
|
|
130
|
+
fs.writeFileSync(filePath, `
|
|
131
|
+
const cp = require('child_process');
|
|
132
|
+
const { exec: runShell } = require('node:child_process');
|
|
133
|
+
cp.exec(req.body.command);
|
|
134
|
+
runShell(user.command);
|
|
135
|
+
cp.spawn(process.env.SHELL, [input], { shell: true });
|
|
136
|
+
cp.spawn(process.env.SHELL, [input]);
|
|
137
|
+
`);
|
|
138
|
+
const vulns = await checkSecurityPatterns(filePath);
|
|
139
|
+
expect(vulns.filter(v => v.type === 'command_injection')).toHaveLength(3);
|
|
140
|
+
});
|
|
141
|
+
it('does not use an imported shell binding shadowed by a parameter', async () => {
|
|
142
|
+
const filePath = path.join(testDir, 'shadow.ts');
|
|
143
|
+
fs.writeFileSync(filePath, `
|
|
144
|
+
import { exec } from 'child_process';
|
|
145
|
+
function parse(exec: RegExp) { return exec.exec(body); }
|
|
146
|
+
function run(exec: (value: string) => void) { exec(user); }
|
|
147
|
+
`);
|
|
148
|
+
const vulns = await checkSecurityPatterns(filePath);
|
|
149
|
+
expect(vulns.filter(v => v.type === 'command_injection')).toEqual([]);
|
|
150
|
+
});
|
|
116
151
|
});
|
|
117
152
|
describe('severity filtering', () => {
|
|
118
153
|
it('should block on high severity by default', async () => {
|
|
@@ -6,10 +6,8 @@
|
|
|
6
6
|
* style which may differ from the project norm.
|
|
7
7
|
*
|
|
8
8
|
* What it checks:
|
|
9
|
-
* 1. Naming conventions —
|
|
9
|
+
* 1. Naming conventions — compare names with the same language conventions
|
|
10
10
|
* 2. Error handling patterns — try-catch vs .catch()/except/rescue consistency
|
|
11
|
-
* 3. Import style — named vs default vs wildcard import consistency
|
|
12
|
-
* 4. Quote style — single vs double quote consistency
|
|
13
11
|
*
|
|
14
12
|
* How it works:
|
|
15
13
|
* 1. First scan: sample source files → compute per-language style fingerprints → store baseline
|
|
@@ -17,7 +15,9 @@
|
|
|
17
15
|
* 3. If a file deviates >25% on any dimension → flag as style drift
|
|
18
16
|
*
|
|
19
17
|
* Baselines are per-language to avoid cross-language contamination
|
|
20
|
-
* (e.g., Python snake_case
|
|
18
|
+
* (e.g., Python snake_case should not flag JS camelCase).
|
|
19
|
+
* JS names mandated by frameworks are excluded from style comparison, and
|
|
20
|
+
* imports are never compared by a language-wide named/default ratio.
|
|
21
21
|
*/
|
|
22
22
|
import { Gate, GateContext } from './base.js';
|
|
23
23
|
import { Failure, Provenance } from '../types/index.js';
|
|
@@ -6,10 +6,8 @@
|
|
|
6
6
|
* style which may differ from the project norm.
|
|
7
7
|
*
|
|
8
8
|
* What it checks:
|
|
9
|
-
* 1. Naming conventions —
|
|
9
|
+
* 1. Naming conventions — compare names with the same language conventions
|
|
10
10
|
* 2. Error handling patterns — try-catch vs .catch()/except/rescue consistency
|
|
11
|
-
* 3. Import style — named vs default vs wildcard import consistency
|
|
12
|
-
* 4. Quote style — single vs double quote consistency
|
|
13
11
|
*
|
|
14
12
|
* How it works:
|
|
15
13
|
* 1. First scan: sample source files → compute per-language style fingerprints → store baseline
|
|
@@ -17,12 +15,15 @@
|
|
|
17
15
|
* 3. If a file deviates >25% on any dimension → flag as style drift
|
|
18
16
|
*
|
|
19
17
|
* Baselines are per-language to avoid cross-language contamination
|
|
20
|
-
* (e.g., Python snake_case
|
|
18
|
+
* (e.g., Python snake_case should not flag JS camelCase).
|
|
19
|
+
* JS names mandated by frameworks are excluded from style comparison, and
|
|
20
|
+
* imports are never compared by a language-wide named/default ratio.
|
|
21
21
|
*/
|
|
22
22
|
import { Gate } from './base.js';
|
|
23
23
|
import { FileScanner } from '../utils/scanner.js';
|
|
24
24
|
import { Logger } from '../utils/logger.js';
|
|
25
25
|
import { languageAdapters } from './language-adapters/index.js';
|
|
26
|
+
import { extractComparableJsNames } from './js-style-context.js';
|
|
26
27
|
import { TRY_CATCH_PATTERN, CATCH_PATTERN, RESULT_TYPE_PATTERN, NAMED_IMPORT_PATTERN, WILDCARD_IMPORT_PATTERN, SIDE_EFFECT_IMPORT_PATTERN, DEFAULT_IMPORT_PATTERN, countQuotes, } from './style-drift-rules.js';
|
|
27
28
|
import fs from 'fs-extra';
|
|
28
29
|
import path from 'path';
|
|
@@ -67,7 +68,7 @@ export class StyleDriftGate extends Gate {
|
|
|
67
68
|
try {
|
|
68
69
|
const raw = await fs.readJson(baselinePath);
|
|
69
70
|
// Handle migration from old single-fingerprint format
|
|
70
|
-
if (raw.version ===
|
|
71
|
+
if (raw.version === 3) {
|
|
71
72
|
baseline = raw;
|
|
72
73
|
}
|
|
73
74
|
else {
|
|
@@ -116,7 +117,7 @@ export class StyleDriftGate extends Gate {
|
|
|
116
117
|
const baseline = {
|
|
117
118
|
languages: {},
|
|
118
119
|
createdAt: new Date().toISOString(),
|
|
119
|
-
version:
|
|
120
|
+
version: 3,
|
|
120
121
|
};
|
|
121
122
|
for (const [langId, langFiles] of filesByLang) {
|
|
122
123
|
// Sample up to sample_size files per language
|
|
@@ -157,7 +158,9 @@ export class StyleDriftGate extends Gate {
|
|
|
157
158
|
const adapter = languageAdapters.getAdapter(filePath);
|
|
158
159
|
// ── Naming conventions (via adapter) ──
|
|
159
160
|
if (adapter) {
|
|
160
|
-
const namingPatterns = adapter.
|
|
161
|
+
const namingPatterns = adapter.id === 'js'
|
|
162
|
+
? extractComparableJsNames(content, filePath)
|
|
163
|
+
: adapter.extractNamingPatterns(content);
|
|
161
164
|
for (const pattern of namingPatterns) {
|
|
162
165
|
if (pattern.kind === 'function' || pattern.kind === 'method') {
|
|
163
166
|
fp.naming.functions[pattern.convention]++;
|
|
@@ -244,15 +247,8 @@ export class StyleDriftGate extends Gate {
|
|
|
244
247
|
suggestion: `Use ${errDev.baselinePredominant} error handling pattern to match project conventions.`,
|
|
245
248
|
});
|
|
246
249
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
deviations.push({
|
|
250
|
-
dimension: 'import style',
|
|
251
|
-
score: impDev.score,
|
|
252
|
-
detail: `file uses ${impDev.filePredominant}, project uses ${impDev.baselinePredominant}`,
|
|
253
|
-
suggestion: `Use ${impDev.baselinePredominant} imports to match project conventions.`,
|
|
254
|
-
});
|
|
255
|
-
}
|
|
250
|
+
// Import form depends on the imported module's exports. A language-wide
|
|
251
|
+
// named/default ratio cannot establish a valid replacement.
|
|
256
252
|
return deviations;
|
|
257
253
|
}
|
|
258
254
|
distributionDeviation(file, baseline) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { StyleDriftGate } from './style-drift.js';
|
|
6
|
+
describe('StyleDriftGate', () => {
|
|
7
|
+
let cwd;
|
|
8
|
+
beforeEach(() => { cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'style-drift-')); });
|
|
9
|
+
afterEach(() => { fs.rmSync(cwd, { recursive: true, force: true }); });
|
|
10
|
+
it('does not compare default imports against unrelated named imports', async () => {
|
|
11
|
+
const src = path.join(cwd, 'src');
|
|
12
|
+
fs.mkdirSync(src);
|
|
13
|
+
for (let i = 0; i < 5; i++) {
|
|
14
|
+
fs.writeFileSync(path.join(src, `named${i}.ts`), `import { value } from './shared';\nexport function helper${i}() { return value; }\n`);
|
|
15
|
+
}
|
|
16
|
+
fs.writeFileSync(path.join(src, 'mui.ts'), [
|
|
17
|
+
"import Box from '@mui/material/Box';",
|
|
18
|
+
"import TextField from '@mui/material/TextField';",
|
|
19
|
+
"import Button from '@mui/material/Button';",
|
|
20
|
+
].join('\n'));
|
|
21
|
+
const gate = new StyleDriftGate();
|
|
22
|
+
expect(await gate.run({ cwd })).toEqual([]);
|
|
23
|
+
const findings = await gate.run({ cwd });
|
|
24
|
+
expect(findings.some(finding => finding.details.includes('import style'))).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/core",
|
|
3
|
-
"version": "6.2.
|
|
3
|
+
"version": "6.2.4",
|
|
4
4
|
"description": "AI-native quality gate engine with local Bayesian learning. AST analysis, drift detection, Fix Packet generation, and agent self-healing across TypeScript, JavaScript, Python, Go, Ruby, and C#.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://rigour.run",
|
|
@@ -67,11 +67,11 @@
|
|
|
67
67
|
"pg": "^8.16.3",
|
|
68
68
|
"sqlite3": "^5.1.7",
|
|
69
69
|
"openai": "^4.104.0",
|
|
70
|
-
"@rigour-labs/brain-darwin-
|
|
71
|
-
"@rigour-labs/brain-darwin-
|
|
72
|
-
"@rigour-labs/brain-linux-
|
|
73
|
-
"@rigour-labs/brain-win-x64": "6.2.
|
|
74
|
-
"@rigour-labs/brain-linux-
|
|
70
|
+
"@rigour-labs/brain-darwin-arm64": "6.2.4",
|
|
71
|
+
"@rigour-labs/brain-darwin-x64": "6.2.4",
|
|
72
|
+
"@rigour-labs/brain-linux-x64": "6.2.4",
|
|
73
|
+
"@rigour-labs/brain-win-x64": "6.2.4",
|
|
74
|
+
"@rigour-labs/brain-linux-arm64": "6.2.4"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
77
|
"@types/fs-extra": "^11.0.4",
|