@rigour-labs/core 6.2.3 → 6.2.5

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 (100) hide show
  1. package/dist/deep/code-context.d.ts +28 -0
  2. package/dist/deep/code-context.js +93 -0
  3. package/dist/deep/code-context.test.d.ts +1 -0
  4. package/dist/deep/code-context.test.js +48 -0
  5. package/dist/deep/code-pass.d.ts +15 -0
  6. package/dist/deep/code-pass.js +44 -0
  7. package/dist/deep/code-review-prompt.d.ts +17 -0
  8. package/dist/deep/code-review-prompt.js +30 -0
  9. package/dist/deep/code-review-prompt.test.d.ts +1 -0
  10. package/dist/deep/code-review-prompt.test.js +20 -0
  11. package/dist/deep/code-verifier.d.ts +19 -0
  12. package/dist/deep/code-verifier.js +48 -0
  13. package/dist/deep/code-verifier.test.d.ts +1 -0
  14. package/dist/deep/code-verifier.test.js +46 -0
  15. package/dist/deep/fact-extractor.d.ts +7 -1
  16. package/dist/deep/fact-extractor.js +16 -8
  17. package/dist/deep/fact-extractor.test.js +34 -1
  18. package/dist/deep/facts-pass.d.ts +24 -0
  19. package/dist/deep/facts-pass.js +69 -0
  20. package/dist/deep/index.d.ts +6 -1
  21. package/dist/deep/index.js +5 -1
  22. package/dist/deep/parse-findings.d.ts +12 -0
  23. package/dist/deep/parse-findings.js +73 -0
  24. package/dist/deep/parse-findings.test.d.ts +1 -0
  25. package/dist/deep/parse-findings.test.js +25 -0
  26. package/dist/deep/prompts.d.ts +1 -1
  27. package/dist/deep/prompts.js +7 -8
  28. package/dist/deep/prompts.test.js +15 -0
  29. package/dist/gates/context.js +7 -0
  30. package/dist/gates/deep-analysis.d.ts +31 -8
  31. package/dist/gates/deep-analysis.js +165 -306
  32. package/dist/gates/deep-analysis.test.d.ts +1 -0
  33. package/dist/gates/deep-analysis.test.js +86 -0
  34. package/dist/gates/deep-runner.d.ts +15 -0
  35. package/dist/gates/deep-runner.js +65 -0
  36. package/dist/gates/dependency.js +4 -0
  37. package/dist/gates/hallucinated-imports/index.js +13 -9
  38. package/dist/gates/hallucinated-imports.test.js +10 -0
  39. package/dist/gates/js-style-context.d.ts +3 -0
  40. package/dist/gates/js-style-context.js +65 -0
  41. package/dist/gates/js-style-context.test.d.ts +1 -0
  42. package/dist/gates/js-style-context.test.js +35 -0
  43. package/dist/gates/logic-drift-git-base.d.ts +7 -0
  44. package/dist/gates/logic-drift-git-base.js +41 -0
  45. package/dist/gates/logic-drift.d.ts +3 -3
  46. package/dist/gates/logic-drift.js +39 -7
  47. package/dist/gates/logic-drift.test.d.ts +1 -0
  48. package/dist/gates/logic-drift.test.js +45 -0
  49. package/dist/gates/runner.js +5 -40
  50. package/dist/gates/runner.test.js +37 -25
  51. package/dist/gates/scoped-gates.test.d.ts +1 -0
  52. package/dist/gates/scoped-gates.test.js +52 -0
  53. package/dist/gates/security-command-execution.d.ts +5 -0
  54. package/dist/gates/security-command-execution.js +123 -0
  55. package/dist/gates/security-patterns-data.js +1 -17
  56. package/dist/gates/security-patterns.d.ts +0 -5
  57. package/dist/gates/security-patterns.js +11 -19
  58. package/dist/gates/security-patterns.test.js +37 -2
  59. package/dist/gates/style-drift.d.ts +4 -4
  60. package/dist/gates/style-drift.js +15 -16
  61. package/dist/gates/style-drift.test.d.ts +1 -0
  62. package/dist/gates/style-drift.test.js +26 -0
  63. package/dist/index.d.ts +6 -1
  64. package/dist/index.js +6 -1
  65. package/dist/inference/http-download.d.ts +22 -0
  66. package/dist/inference/http-download.js +132 -0
  67. package/dist/inference/http-download.test.d.ts +1 -0
  68. package/dist/inference/http-download.test.js +109 -0
  69. package/dist/inference/index.d.ts +5 -1
  70. package/dist/inference/index.js +2 -1
  71. package/dist/inference/llama-engine.d.ts +27 -0
  72. package/dist/inference/llama-engine.js +152 -0
  73. package/dist/inference/llama-engine.test.d.ts +1 -0
  74. package/dist/inference/llama-engine.test.js +51 -0
  75. package/dist/inference/llama-process.d.ts +72 -0
  76. package/dist/inference/llama-process.js +126 -0
  77. package/dist/inference/llama-process.test.d.ts +1 -0
  78. package/dist/inference/llama-process.test.js +61 -0
  79. package/dist/inference/model-download.test.d.ts +1 -0
  80. package/dist/inference/model-download.test.js +73 -0
  81. package/dist/inference/model-manager.d.ts +14 -1
  82. package/dist/inference/model-manager.js +65 -80
  83. package/dist/inference/sidecar-provider.d.ts +19 -10
  84. package/dist/inference/sidecar-provider.js +115 -343
  85. package/dist/templates/universal-config.js +0 -2
  86. package/dist/types/index.d.ts +58 -14
  87. package/dist/types/index.js +9 -2
  88. package/dist/utils/deep-status.d.ts +7 -0
  89. package/dist/utils/deep-status.js +11 -0
  90. package/dist/utils/diff.d.ts +3 -0
  91. package/dist/utils/diff.js +52 -0
  92. package/dist/utils/diff.test.d.ts +1 -0
  93. package/dist/utils/diff.test.js +38 -0
  94. package/dist/utils/scanner.js +13 -2
  95. package/dist/utils/scanner.test.js +22 -8
  96. package/dist/utils/scope.d.ts +15 -0
  97. package/dist/utils/scope.js +51 -0
  98. package/dist/utils/scope.test.d.ts +1 -0
  99. package/dist/utils/scope.test.js +38 -0
  100. package/package.json +6 -6
@@ -0,0 +1,52 @@
1
+ import fs from 'fs-extra';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { ContextGate } from './context.js';
6
+ import { DependencyGate } from './dependency.js';
7
+ import { LogicDriftGate } from './logic-drift.js';
8
+ import { StyleDriftGate } from './style-drift.js';
9
+ describe('gates on a scoped run', () => {
10
+ let cwd;
11
+ beforeEach(async () => { cwd = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-scoped-')); });
12
+ afterEach(async () => { await fs.remove(cwd); });
13
+ it('context-drift reports only findings that cite a file in scope', async () => {
14
+ await fs.outputFile(path.join(cwd, 'src/a.ts'), 'export const a = process.env.API_URL_PROD;\n');
15
+ await fs.outputFile(path.join(cwd, 'src/b.ts'), 'export const b = process.env.API_URL_STAGING;\n');
16
+ const gate = new ContextGate({ context: { enabled: true } });
17
+ const record = { anchors: [{ type: 'env', id: 'API_URL', confidence: 1 }] };
18
+ const unscoped = await gate.run({ cwd, record });
19
+ const scoped = await gate.run({ cwd, record, patterns: ['src/a.ts'] });
20
+ expect(unscoped.flatMap(f => f.files)).toEqual(expect.arrayContaining(['src/a.ts', 'src/b.ts']));
21
+ expect(scoped.length).toBeGreaterThan(0);
22
+ expect(scoped.every(f => f.files?.includes('src/a.ts'))).toBe(true);
23
+ });
24
+ it('dependency-guardian checks the manifest only when package.json is in scope', async () => {
25
+ await fs.outputJson(path.join(cwd, 'package.json'), { dependencies: { 'left-pad': '1.0.0' } });
26
+ await fs.outputFile(path.join(cwd, 'src/a.ts'), "import pad from 'left-pad';\n");
27
+ const gate = new DependencyGate({ gates: { dependencies: { forbid: ['left-pad'] } } });
28
+ expect(await gate.run({ cwd, patterns: ['src/a.ts'] })).toEqual([]);
29
+ const withManifest = await gate.run({ cwd, patterns: ['package.json'] });
30
+ expect(withManifest.some(f => f.title === 'Forbidden Dependency')).toBe(true);
31
+ });
32
+ it('logic-drift never creates or rewrites its baseline from a scoped run', async () => {
33
+ const baseline = path.join(cwd, '.rigour/logic-baseline.json');
34
+ await fs.outputFile(path.join(cwd, 'src/rules.ts'), 'export function eligible(score: number) { return score >= 10; }\n');
35
+ await fs.outputFile(path.join(cwd, 'src/other.ts'), 'export function other(n: number) { return n > 1; }\n');
36
+ const gate = new LogicDriftGate();
37
+ expect(await gate.run({ cwd, patterns: ['src/rules.ts'] })).toEqual([]);
38
+ expect(await fs.pathExists(baseline)).toBe(false);
39
+ await gate.run({ cwd });
40
+ const before = await fs.readFile(baseline, 'utf-8');
41
+ await fs.outputFile(path.join(cwd, 'src/rules.ts'), 'export function eligible(score: number) { return score > 10; }\n');
42
+ const scoped = await gate.run({ cwd, patterns: ['src/rules.ts'] });
43
+ expect(scoped.some(f => f.details.includes("'>=' to '>'"))).toBe(true);
44
+ expect(await fs.readFile(baseline, 'utf-8')).toBe(before);
45
+ });
46
+ it('style-drift does not create its baseline from a scoped run', async () => {
47
+ await fs.outputFile(path.join(cwd, 'src/a.ts'), 'export function doThing() { return 1; }\n');
48
+ const gate = new StyleDriftGate();
49
+ expect(await gate.run({ cwd, patterns: ['src/a.ts'] })).toEqual([]);
50
+ expect(await fs.pathExists(path.join(cwd, '.rigour/style-baseline.json'))).toBe(false);
51
+ });
52
+ });
@@ -0,0 +1,5 @@
1
+ export interface UnsafeShellCall {
2
+ line: number;
3
+ text: string;
4
+ }
5
+ export declare function findUnsafeShellCalls(content: string, file: string): UnsafeShellCall[];
@@ -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
- // Command Injection
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
- const lines = content.split('\n');
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('should detect exec with user input', async () => {
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(\`ls \${req.query.path}\`);
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 — camelCase vs snake_case vs PascalCase consistency
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 shouldn't flag JS camelCase).
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 — camelCase vs snake_case vs PascalCase consistency
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,16 @@
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 shouldn't flag JS camelCase).
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';
27
+ import { isScoped } from '../utils/scope.js';
26
28
  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
29
  import fs from 'fs-extra';
28
30
  import path from 'path';
@@ -67,7 +69,7 @@ export class StyleDriftGate extends Gate {
67
69
  try {
68
70
  const raw = await fs.readJson(baselinePath);
69
71
  // Handle migration from old single-fingerprint format
70
- if (raw.version === 2) {
72
+ if (raw.version === 3) {
71
73
  baseline = raw;
72
74
  }
73
75
  else {
@@ -78,6 +80,8 @@ export class StyleDriftGate extends Gate {
78
80
  Logger.debug('Failed to load style baseline, will create new one');
79
81
  }
80
82
  }
83
+ if (!baseline && isScoped(context.patterns))
84
+ return []; // never baseline from a subset of files
81
85
  if (!baseline) {
82
86
  // First scan: create per-language baseline
83
87
  baseline = await this.computePerLanguageBaseline(context, filesByLang);
@@ -116,7 +120,7 @@ export class StyleDriftGate extends Gate {
116
120
  const baseline = {
117
121
  languages: {},
118
122
  createdAt: new Date().toISOString(),
119
- version: 2,
123
+ version: 3,
120
124
  };
121
125
  for (const [langId, langFiles] of filesByLang) {
122
126
  // Sample up to sample_size files per language
@@ -157,7 +161,9 @@ export class StyleDriftGate extends Gate {
157
161
  const adapter = languageAdapters.getAdapter(filePath);
158
162
  // ── Naming conventions (via adapter) ──
159
163
  if (adapter) {
160
- const namingPatterns = adapter.extractNamingPatterns(content);
164
+ const namingPatterns = adapter.id === 'js'
165
+ ? extractComparableJsNames(content, filePath)
166
+ : adapter.extractNamingPatterns(content);
161
167
  for (const pattern of namingPatterns) {
162
168
  if (pattern.kind === 'function' || pattern.kind === 'method') {
163
169
  fp.naming.functions[pattern.convention]++;
@@ -244,15 +250,8 @@ export class StyleDriftGate extends Gate {
244
250
  suggestion: `Use ${errDev.baselinePredominant} error handling pattern to match project conventions.`,
245
251
  });
246
252
  }
247
- const impDev = this.distributionDeviation(file.importStyle, baseline.importStyle);
248
- if (impDev.score > 0 && this.hasSignificantData(file.importStyle)) {
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
- }
253
+ // Import form depends on the imported module's exports. A language-wide
254
+ // named/default ratio cannot establish a valid replacement.
256
255
  return deviations;
257
256
  }
258
257
  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/dist/index.d.ts CHANGED
@@ -9,6 +9,9 @@ export { RetryLoopBreakerGate } from './gates/retry-loop-breaker.js';
9
9
  export { SideEffectAnalysisGate } from './gates/side-effect-analysis/index.js';
10
10
  export { FrontendSecretExposureGate } from './gates/frontend-secret-exposure.js';
11
11
  export * from './utils/logger.js';
12
+ export { normalizeScopePatterns, isScoped } from './utils/scope.js';
13
+ export { deepAnalysisError } from './utils/deep-status.js';
14
+ export { parseDiff, changedLinesByFile } from './utils/diff.js';
12
15
  export { FileScanner } from './utils/scanner.js';
13
16
  export * from './services/score-history.js';
14
17
  export * from './hooks/index.js';
@@ -18,7 +21,9 @@ export { DeepAnalysisGate } from './gates/deep-analysis.js';
18
21
  export { createProvider } from './inference/index.js';
19
22
  export type { InferenceProvider, DeepFinding, DeepAnalysisResult, ModelTier } from './inference/types.js';
20
23
  export { MODELS } from './inference/types.js';
21
- export { isModelCached, getModelsDir, getModelInfo } from './inference/model-manager.js';
24
+ export { isModelCached, getModelsDir, getModelInfo, getCachedModel, ensureModel } from './inference/model-manager.js';
25
+ export { installLlamaEngine, probeBinary, managedEnginePath, LLAMA_RELEASE_TAG } from './inference/llama-engine.js';
26
+ export { SidecarProvider } from './inference/sidecar-provider.js';
22
27
  export { extractFacts, factsToPromptString } from './deep/fact-extractor.js';
23
28
  export { openDatabase, isSQLiteAvailable, compactDatabase, getDatabaseSize, resetDatabase, insertScan, insertFindings, getRecentScans, getScoreTrendFromDB, getTopIssues, reinforcePattern, getStrongPatterns } from './storage/index.js';
24
29
  export type { RigourDB, CompactResult } from './storage/index.js';
package/dist/index.js CHANGED
@@ -9,6 +9,9 @@ export { RetryLoopBreakerGate } from './gates/retry-loop-breaker.js';
9
9
  export { SideEffectAnalysisGate } from './gates/side-effect-analysis/index.js';
10
10
  export { FrontendSecretExposureGate } from './gates/frontend-secret-exposure.js';
11
11
  export * from './utils/logger.js';
12
+ export { normalizeScopePatterns, isScoped } from './utils/scope.js';
13
+ export { deepAnalysisError } from './utils/deep-status.js';
14
+ export { parseDiff, changedLinesByFile } from './utils/diff.js';
12
15
  export { FileScanner } from './utils/scanner.js';
13
16
  export * from './services/score-history.js';
14
17
  export * from './hooks/index.js';
@@ -18,7 +21,9 @@ export { loadSettings, saveSettings, getSettingsPath, resolveDeepOptions, getPro
18
21
  export { DeepAnalysisGate } from './gates/deep-analysis.js';
19
22
  export { createProvider } from './inference/index.js';
20
23
  export { MODELS } from './inference/types.js';
21
- export { isModelCached, getModelsDir, getModelInfo } from './inference/model-manager.js';
24
+ export { isModelCached, getModelsDir, getModelInfo, getCachedModel, ensureModel } from './inference/model-manager.js';
25
+ export { installLlamaEngine, probeBinary, managedEnginePath, LLAMA_RELEASE_TAG } from './inference/llama-engine.js';
26
+ export { SidecarProvider } from './inference/sidecar-provider.js';
22
27
  export { extractFacts, factsToPromptString } from './deep/fact-extractor.js';
23
28
  // Storage (SQLite Brain)
24
29
  export { openDatabase, isSQLiteAvailable, compactDatabase, getDatabaseSize, resetDatabase, insertScan, insertFindings, getRecentScans, getScoreTrendFromDB, getTopIssues, reinforcePattern, getStrongPatterns } from './storage/index.js';
@@ -0,0 +1,22 @@
1
+ import { type Readable } from 'stream';
2
+ export interface HttpResponse {
3
+ statusCode: number;
4
+ headers: Record<string, string | string[] | undefined>;
5
+ body: Readable;
6
+ }
7
+ export type HttpGet = (url: string, headers: Record<string, string>, timeoutMs: number) => Promise<HttpResponse>;
8
+ export interface DownloadOptions {
9
+ onProgress?: (downloadedBytes: number, totalBytes: number | null) => void;
10
+ stallTimeoutMs?: number;
11
+ /** Resume from an existing partial file at `destPath` (default true). */
12
+ resume?: boolean;
13
+ /** Transport override for tests. */
14
+ get?: HttpGet;
15
+ }
16
+ export interface DownloadResult {
17
+ sha256: string;
18
+ bytes: number;
19
+ /** ETag seen on any hop; Hugging Face exposes the LFS SHA-256 as x-linked-etag. */
20
+ etag: string | null;
21
+ }
22
+ export declare function downloadToFile(url: string, destPath: string, options?: DownloadOptions): Promise<DownloadResult>;