@rigour-labs/core 4.3.2 → 4.3.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/context-window-artifacts.js +2 -1
- package/dist/gates/deprecated-apis.js +3 -1
- package/dist/gates/duplication-drift.js +2 -1
- package/dist/gates/frontend-secret-exposure.d.ts +41 -9
- package/dist/gates/frontend-secret-exposure.js +315 -135
- package/dist/gates/hallucinated-imports.js +3 -1
- package/dist/gates/inconsistent-error-handling.js +2 -1
- package/dist/gates/phantom-apis.js +3 -1
- package/dist/gates/promise-safety-helpers.js +5 -5
- package/dist/gates/promise-safety.js +5 -4
- package/dist/gates/runner.js +2 -2
- package/dist/gates/runner.test.js +5 -5
- package/dist/gates/security-patterns.js +2 -1
- package/dist/gates/side-effect-analysis.js +2 -1
- package/dist/gates/test-quality.js +10 -8
- package/dist/inference/cloud-provider.js +6 -6
- package/dist/inference/index.js +3 -1
- package/dist/inference/model-manager.d.ts +9 -1
- package/dist/inference/model-manager.js +59 -2
- package/dist/inference/sidecar-provider.js +1 -1
- package/dist/inference/types.d.ts +21 -6
- package/dist/inference/types.js +55 -29
- package/dist/types/index.d.ts +7 -7
- package/dist/types/index.js +1 -1
- package/package.json +6 -6
|
@@ -36,9 +36,10 @@ export class ContextWindowArtifactsGate extends Gate {
|
|
|
36
36
|
if (!this.config.enabled)
|
|
37
37
|
return [];
|
|
38
38
|
const failures = [];
|
|
39
|
+
const scanPatterns = context.patterns || ['**/*.{ts,js,tsx,jsx,py}'];
|
|
39
40
|
const files = await FileScanner.findFiles({
|
|
40
41
|
cwd: context.cwd,
|
|
41
|
-
patterns:
|
|
42
|
+
patterns: scanPatterns,
|
|
42
43
|
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*', '**/*.min.*'],
|
|
43
44
|
});
|
|
44
45
|
Logger.info(`Context Window Artifacts: Scanning ${files.length} files`);
|
|
@@ -48,9 +48,11 @@ export class DeprecatedApisGate extends Gate {
|
|
|
48
48
|
return [];
|
|
49
49
|
const failures = [];
|
|
50
50
|
const deprecated = [];
|
|
51
|
+
const defaultPatterns = ['**/*.{ts,js,tsx,jsx,py,go,cs,java,kt}'];
|
|
52
|
+
const scanPatterns = context.patterns || defaultPatterns;
|
|
51
53
|
const files = await FileScanner.findFiles({
|
|
52
54
|
cwd: context.cwd,
|
|
53
|
-
patterns:
|
|
55
|
+
patterns: scanPatterns,
|
|
54
56
|
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
|
|
55
57
|
'**/*.test.*', '**/*.spec.*', '**/__tests__/**',
|
|
56
58
|
'**/.venv/**', '**/venv/**', '**/vendor/**', '**/__pycache__/**',
|
|
@@ -33,9 +33,10 @@ export class DuplicationDriftGate extends Gate {
|
|
|
33
33
|
return [];
|
|
34
34
|
const failures = [];
|
|
35
35
|
const functions = [];
|
|
36
|
+
const scanPatterns = context.patterns || ['**/*.{ts,js,tsx,jsx,py}'];
|
|
36
37
|
const files = await FileScanner.findFiles({
|
|
37
38
|
cwd: context.cwd,
|
|
38
|
-
patterns:
|
|
39
|
+
patterns: scanPatterns,
|
|
39
40
|
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*'],
|
|
40
41
|
});
|
|
41
42
|
Logger.info(`Duplication Drift: Scanning ${files.length} files`);
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend Secret Exposure Gate
|
|
3
|
+
*
|
|
4
|
+
* Detects server-side secret keys (Stripe, OpenAI, AWS, etc.) referenced
|
|
5
|
+
* in frontend-accessible files where they would be bundled into client JS.
|
|
6
|
+
*
|
|
7
|
+
* The "vibe-coded startup" attack pattern:
|
|
8
|
+
* process.env.STRIPE_SECRET_KEY inside a React component
|
|
9
|
+
* → bundled by webpack/Vite → visible in DevTools → $2,500 in fraudulent charges
|
|
10
|
+
*
|
|
11
|
+
* Two detection modes:
|
|
12
|
+
* 1. Env-var name detection: process.env.VARNAME / import.meta.env.VARNAME
|
|
13
|
+
* where VARNAME matches secret_env_name_patterns and lacks a safe public prefix.
|
|
14
|
+
* 2. Literal key detection: actual live API key values embedded in source
|
|
15
|
+
* (sk_live_..., sk-proj-..., AKIA..., ghp_..., etc.)
|
|
16
|
+
*
|
|
17
|
+
* @since v4.4.0
|
|
18
|
+
* @see CWE-312 Cleartext Storage of Sensitive Information
|
|
19
|
+
*/
|
|
1
20
|
import { Gate, GateContext } from './base.js';
|
|
2
21
|
import { Failure, Provenance } from '../types/index.js';
|
|
3
22
|
export interface FrontendSecretExposureConfig {
|
|
@@ -12,16 +31,29 @@ export interface FrontendSecretExposureConfig {
|
|
|
12
31
|
allowlist_env_names?: string[];
|
|
13
32
|
}
|
|
14
33
|
export declare class FrontendSecretExposureGate extends Gate {
|
|
15
|
-
private
|
|
16
|
-
private
|
|
34
|
+
private cfg;
|
|
35
|
+
private secretNamePatterns;
|
|
36
|
+
private safePrefixPatterns;
|
|
37
|
+
private frontendPathPatterns;
|
|
38
|
+
private serverPathPatterns;
|
|
17
39
|
constructor(config?: FrontendSecretExposureConfig);
|
|
18
40
|
protected get provenance(): Provenance;
|
|
19
41
|
run(context: GateContext): Promise<Failure[]>;
|
|
20
|
-
|
|
21
|
-
private
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
42
|
+
/** Classify a file path as frontend, server, or ambiguous based on config patterns. */
|
|
43
|
+
private classifyFile;
|
|
44
|
+
/** Test/fixture files are excluded — they legitimately contain dummy keys. */
|
|
45
|
+
private isTestFile;
|
|
46
|
+
/**
|
|
47
|
+
* Check whether a var name looks like a server-side secret:
|
|
48
|
+
* - Matches at least one secret_env_name_pattern
|
|
49
|
+
* - Does NOT start with a safe public prefix
|
|
50
|
+
* - Is NOT in the user allowlist
|
|
51
|
+
*/
|
|
52
|
+
private isSecretVar;
|
|
53
|
+
/** Scan one file for env-var references and literal key values. */
|
|
54
|
+
private scanFile;
|
|
55
|
+
private makeEnvRefExposure;
|
|
56
|
+
/** Filters out trivial dummy/placeholder text to reduce false positives. */
|
|
57
|
+
private isDummyValue;
|
|
58
|
+
private toFailures;
|
|
27
59
|
}
|
|
@@ -1,174 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend Secret Exposure Gate
|
|
3
|
+
*
|
|
4
|
+
* Detects server-side secret keys (Stripe, OpenAI, AWS, etc.) referenced
|
|
5
|
+
* in frontend-accessible files where they would be bundled into client JS.
|
|
6
|
+
*
|
|
7
|
+
* The "vibe-coded startup" attack pattern:
|
|
8
|
+
* process.env.STRIPE_SECRET_KEY inside a React component
|
|
9
|
+
* → bundled by webpack/Vite → visible in DevTools → $2,500 in fraudulent charges
|
|
10
|
+
*
|
|
11
|
+
* Two detection modes:
|
|
12
|
+
* 1. Env-var name detection: process.env.VARNAME / import.meta.env.VARNAME
|
|
13
|
+
* where VARNAME matches secret_env_name_patterns and lacks a safe public prefix.
|
|
14
|
+
* 2. Literal key detection: actual live API key values embedded in source
|
|
15
|
+
* (sk_live_..., sk-proj-..., AKIA..., ghp_..., etc.)
|
|
16
|
+
*
|
|
17
|
+
* @since v4.4.0
|
|
18
|
+
* @see CWE-312 Cleartext Storage of Sensitive Information
|
|
19
|
+
*/
|
|
1
20
|
import { Gate } from './base.js';
|
|
2
21
|
import { FileScanner } from '../utils/scanner.js';
|
|
3
22
|
import { Logger } from '../utils/logger.js';
|
|
4
23
|
import fs from 'fs-extra';
|
|
5
24
|
import path from 'path';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Literal API key patterns — actual values that are always dangerous in frontend code.
|
|
27
|
+
* These bypass the env-name heuristic and fire regardless of variable naming.
|
|
28
|
+
*/
|
|
29
|
+
const LITERAL_KEY_PATTERNS = [
|
|
30
|
+
{
|
|
31
|
+
label: 'Stripe Live Secret Key',
|
|
32
|
+
regex: /\bsk_live_[a-zA-Z0-9]{24,}\b/g,
|
|
33
|
+
severity: 'critical',
|
|
34
|
+
hint: 'Live Stripe secret key (sk_live_...) exposed in client bundle. ' +
|
|
35
|
+
'Attackers can charge cards without a frontend. Move to server-side only.',
|
|
36
|
+
cwe: 'CWE-312',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: 'Stripe Live Restricted Key',
|
|
40
|
+
regex: /\brk_live_[a-zA-Z0-9]{24,}\b/g,
|
|
41
|
+
severity: 'critical',
|
|
42
|
+
hint: 'Live Stripe restricted key (rk_live_...) must never appear in frontend code.',
|
|
43
|
+
cwe: 'CWE-312',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
label: 'OpenAI API Key',
|
|
47
|
+
regex: /\bsk-proj-[a-zA-Z0-9_-]{40,}\b/g,
|
|
48
|
+
severity: 'critical',
|
|
49
|
+
hint: 'OpenAI project API key (sk-proj-...) exposed in client bundle. ' +
|
|
50
|
+
'Attackers can make unlimited API calls at your expense.',
|
|
51
|
+
cwe: 'CWE-312',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: 'OpenAI Legacy API Key',
|
|
55
|
+
regex: /\bsk-[a-zA-Z0-9]{48,}\b/g,
|
|
56
|
+
severity: 'critical',
|
|
57
|
+
hint: 'OpenAI API key (sk-...) exposed in client bundle. Move to server-side proxy.',
|
|
58
|
+
cwe: 'CWE-312',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
label: 'AWS Access Key ID',
|
|
62
|
+
regex: /\bAKIA[A-Z2-7]{16}\b/g,
|
|
63
|
+
severity: 'critical',
|
|
64
|
+
hint: 'AWS Access Key ID exposed in client bundle. This grants AWS API access.',
|
|
65
|
+
cwe: 'CWE-312',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
label: 'GitHub Personal Access Token',
|
|
69
|
+
regex: /\bghp_[a-zA-Z0-9]{36,}\b/g,
|
|
70
|
+
severity: 'critical',
|
|
71
|
+
hint: 'GitHub PAT (ghp_...) in frontend code. Grants repo access to anyone who views bundle.',
|
|
72
|
+
cwe: 'CWE-312',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: 'GitHub Fine-Grained PAT',
|
|
76
|
+
regex: /\bgithub_pat_[a-zA-Z0-9_]{55,}\b/g,
|
|
77
|
+
severity: 'critical',
|
|
78
|
+
hint: 'GitHub fine-grained PAT (github_pat_...) must never appear in frontend code.',
|
|
79
|
+
cwe: 'CWE-312',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
label: 'Anthropic API Key',
|
|
83
|
+
regex: /\bsk-ant-api\d{2}-[a-zA-Z0-9_-]{90,}\b/g,
|
|
84
|
+
severity: 'critical',
|
|
85
|
+
hint: 'Anthropic API key exposed in client bundle. Move to a server-side proxy.',
|
|
86
|
+
cwe: 'CWE-312',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
label: 'SendGrid API Key',
|
|
90
|
+
regex: /\bSG\.[a-zA-Z0-9_-]{22,}\.[a-zA-Z0-9_-]{43,}\b/g,
|
|
91
|
+
severity: 'critical',
|
|
92
|
+
hint: 'SendGrid API key exposed in client bundle. Attackers can send spam as you.',
|
|
93
|
+
cwe: 'CWE-312',
|
|
94
|
+
},
|
|
34
95
|
];
|
|
96
|
+
/** Severity ordering for threshold comparison */
|
|
97
|
+
const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
98
|
+
/**
|
|
99
|
+
* Extracts the env var name from a single-reference line.
|
|
100
|
+
* process.env.VARNAME → "VARNAME"
|
|
101
|
+
* process.env["VARNAME"] → "VARNAME"
|
|
102
|
+
* import.meta.env.VARNAME → "VARNAME"
|
|
103
|
+
* Returns null if no match.
|
|
104
|
+
*/
|
|
105
|
+
function extractEnvVarName(line, checkProcessEnv, checkImportMetaEnv) {
|
|
106
|
+
if (checkProcessEnv) {
|
|
107
|
+
const dot = line.match(/process\.env\.([A-Z][A-Z0-9_]*)/);
|
|
108
|
+
if (dot)
|
|
109
|
+
return dot[1];
|
|
110
|
+
const bracket = line.match(/process\.env\[['"]([A-Z][A-Z0-9_]*)['"]\]/);
|
|
111
|
+
if (bracket)
|
|
112
|
+
return bracket[1];
|
|
113
|
+
}
|
|
114
|
+
if (checkImportMetaEnv) {
|
|
115
|
+
const vite = line.match(/import\.meta\.env\.([A-Z_][A-Z0-9_]*)/);
|
|
116
|
+
if (vite)
|
|
117
|
+
return vite[1];
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Finds all var names from destructuring:
|
|
123
|
+
* const { STRIPE_SECRET_KEY, OPENAI_KEY } = process.env
|
|
124
|
+
*/
|
|
125
|
+
function extractDestructuredEnvVars(line) {
|
|
126
|
+
if (!/=\s*process\.env\b/.test(line))
|
|
127
|
+
return [];
|
|
128
|
+
const brace = line.match(/\{\s*([^}]+)\s*\}/);
|
|
129
|
+
if (!brace)
|
|
130
|
+
return [];
|
|
131
|
+
return brace[1]
|
|
132
|
+
.split(',')
|
|
133
|
+
.map(s => s.split(':')[0].trim()) // handle "SECRET_KEY: renamed"
|
|
134
|
+
.filter(s => /^[A-Z_][A-Z0-9_]*$/.test(s));
|
|
135
|
+
}
|
|
35
136
|
export class FrontendSecretExposureGate extends Gate {
|
|
36
|
-
|
|
37
|
-
|
|
137
|
+
cfg;
|
|
138
|
+
secretNamePatterns;
|
|
139
|
+
safePrefixPatterns;
|
|
140
|
+
frontendPathPatterns;
|
|
141
|
+
serverPathPatterns;
|
|
38
142
|
constructor(config = {}) {
|
|
39
|
-
super('frontend-secret-exposure', 'Frontend Secret Exposure
|
|
40
|
-
this.
|
|
143
|
+
super('frontend-secret-exposure', 'Frontend Secret Exposure');
|
|
144
|
+
this.cfg = {
|
|
41
145
|
enabled: config.enabled ?? true,
|
|
42
146
|
block_on_severity: config.block_on_severity ?? 'high',
|
|
43
147
|
check_process_env: config.check_process_env ?? true,
|
|
44
148
|
check_import_meta_env: config.check_import_meta_env ?? true,
|
|
45
|
-
secret_env_name_patterns: config.secret_env_name_patterns ??
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
149
|
+
secret_env_name_patterns: config.secret_env_name_patterns ?? [
|
|
150
|
+
'(?:^|_)(?:secret|private)(?:_|$)',
|
|
151
|
+
'(?:^|_)(?:token|api[_-]?key|access[_-]?key|client[_-]?secret|signing|webhook)(?:_|$)',
|
|
152
|
+
'(?:^|_)(?:db[_-]?url|database[_-]?url|connection[_-]?string)(?:_|$)',
|
|
153
|
+
],
|
|
154
|
+
safe_public_prefixes: config.safe_public_prefixes ?? [
|
|
155
|
+
'NEXT_PUBLIC_', 'VITE_', 'PUBLIC_', 'NUXT_PUBLIC_', 'REACT_APP_',
|
|
156
|
+
],
|
|
157
|
+
frontend_path_patterns: config.frontend_path_patterns ?? [
|
|
158
|
+
'(^|/)pages/(?!api/)',
|
|
159
|
+
'(^|/)components/',
|
|
160
|
+
'(^|/)src/components/',
|
|
161
|
+
'(^|/)src/views/',
|
|
162
|
+
'(^|/)src/app/',
|
|
163
|
+
'(^|/)app/(?!api/)',
|
|
164
|
+
'(^|/)views/',
|
|
165
|
+
'(^|/)public/',
|
|
166
|
+
],
|
|
167
|
+
server_path_patterns: config.server_path_patterns ?? [
|
|
168
|
+
'(^|/)pages/api/',
|
|
169
|
+
'(^|/)src/pages/api/',
|
|
170
|
+
'(^|/)app/api/',
|
|
171
|
+
'(^|/)src/app/api/',
|
|
172
|
+
'\\.server\\.(?:ts|tsx|js|jsx|mjs|cjs)$',
|
|
173
|
+
],
|
|
49
174
|
allowlist_env_names: config.allowlist_env_names ?? [],
|
|
50
175
|
};
|
|
176
|
+
this.secretNamePatterns = this.cfg.secret_env_name_patterns.map(p => new RegExp(p, 'i'));
|
|
177
|
+
// Escape prefix strings to safe regex literals
|
|
178
|
+
this.safePrefixPatterns = this.cfg.safe_public_prefixes.map(prefix => new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
|
|
179
|
+
this.frontendPathPatterns = this.cfg.frontend_path_patterns.map(p => new RegExp(p));
|
|
180
|
+
this.serverPathPatterns = this.cfg.server_path_patterns.map(p => new RegExp(p));
|
|
51
181
|
}
|
|
52
182
|
get provenance() { return 'security'; }
|
|
53
183
|
async run(context) {
|
|
54
|
-
if (!this.
|
|
184
|
+
if (!this.cfg.enabled)
|
|
55
185
|
return [];
|
|
186
|
+
const scanPatterns = context.patterns || ['**/*.{ts,tsx,js,jsx,mjs,cjs,vue,svelte}'];
|
|
56
187
|
const files = await FileScanner.findFiles({
|
|
57
188
|
cwd: context.cwd,
|
|
58
|
-
patterns:
|
|
59
|
-
ignore: [
|
|
189
|
+
patterns: scanPatterns,
|
|
190
|
+
ignore: [
|
|
191
|
+
...(context.ignore || []),
|
|
192
|
+
'**/node_modules/**', '**/dist/**', '**/build/**',
|
|
193
|
+
'**/.next/**', '**/coverage/**', '**/out/**',
|
|
194
|
+
],
|
|
60
195
|
});
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
Logger.info(`Frontend Secret Exposure Gate:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
196
|
+
// Skip test/fixture files — they routinely use dummy keys
|
|
197
|
+
const sourceFiles = files.filter(f => !this.isTestFile(f));
|
|
198
|
+
Logger.info(`Frontend Secret Exposure Gate: scanning ${sourceFiles.length} files`);
|
|
199
|
+
const exposures = [];
|
|
200
|
+
for (const file of sourceFiles) {
|
|
201
|
+
const fileContext = this.classifyFile(file);
|
|
202
|
+
if (fileContext === 'server')
|
|
203
|
+
continue;
|
|
67
204
|
try {
|
|
68
|
-
|
|
205
|
+
const fullPath = path.join(context.cwd, file);
|
|
206
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
207
|
+
// Skip files guarded by Next.js server-only import
|
|
208
|
+
if (/import\s+['"]server-only['"]/.test(content))
|
|
209
|
+
continue;
|
|
210
|
+
this.scanFile(content, file, fileContext, exposures);
|
|
69
211
|
}
|
|
70
212
|
catch {
|
|
71
|
-
|
|
213
|
+
// Unreadable — skip silently
|
|
72
214
|
}
|
|
73
|
-
if (!this.isClientBundled(file, content))
|
|
74
|
-
continue;
|
|
75
|
-
findings.push(...this.findEnvExposures(file, content));
|
|
76
215
|
}
|
|
77
|
-
|
|
78
|
-
const threshold = this.severityOrder[this.config.block_on_severity];
|
|
79
|
-
return findings
|
|
80
|
-
.filter(f => this.severityOrder[f.severity] <= threshold)
|
|
81
|
-
.map(f => this.createFailure(`Potential frontend secret exposure: ${f.source}.${f.envVar} is referenced in client-bundled code.`, [f.file], 'Move secret usage to server-only code (API route/server action) and expose only public-safe values.', 'Security: Frontend Secret Exposure', f.line, f.line, f.severity));
|
|
216
|
+
return this.toFailures(exposures);
|
|
82
217
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
return
|
|
89
|
-
if (
|
|
90
|
-
return
|
|
91
|
-
return
|
|
218
|
+
/** Classify a file path as frontend, server, or ambiguous based on config patterns. */
|
|
219
|
+
classifyFile(file) {
|
|
220
|
+
const n = file.replace(/\\/g, '/');
|
|
221
|
+
// Server paths take priority over frontend paths
|
|
222
|
+
if (this.serverPathPatterns.some(p => p.test(n)))
|
|
223
|
+
return 'server';
|
|
224
|
+
if (this.frontendPathPatterns.some(p => p.test(n)))
|
|
225
|
+
return 'frontend';
|
|
226
|
+
return 'ambiguous';
|
|
227
|
+
}
|
|
228
|
+
/** Test/fixture files are excluded — they legitimately contain dummy keys. */
|
|
229
|
+
isTestFile(file) {
|
|
230
|
+
const n = file.replace(/\\/g, '/');
|
|
231
|
+
return /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(n)
|
|
232
|
+
|| /\/__tests__\//.test(n)
|
|
233
|
+
|| /\/(?:fixtures|mocks|stubs)\//.test(n);
|
|
92
234
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Check whether a var name looks like a server-side secret:
|
|
237
|
+
* - Matches at least one secret_env_name_pattern
|
|
238
|
+
* - Does NOT start with a safe public prefix
|
|
239
|
+
* - Is NOT in the user allowlist
|
|
240
|
+
*/
|
|
241
|
+
isSecretVar(varName) {
|
|
242
|
+
if (this.cfg.allowlist_env_names.includes(varName))
|
|
96
243
|
return false;
|
|
97
|
-
if (this.
|
|
244
|
+
if (this.safePrefixPatterns.some(p => p.test(varName)))
|
|
98
245
|
return false;
|
|
99
|
-
|
|
100
|
-
return true;
|
|
101
|
-
if (this.matchesAnyPattern(normalized, this.config.frontend_path_patterns))
|
|
102
|
-
return true;
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
isServerOnlyContent(content) {
|
|
106
|
-
if (/from\s+['"]server-only['"]/.test(content))
|
|
107
|
-
return true;
|
|
108
|
-
if (/import\s+['"]server-only['"]/.test(content))
|
|
109
|
-
return true;
|
|
110
|
-
if (/export\s+async\s+function\s+getServerSideProps\s*\(/.test(content))
|
|
111
|
-
return true;
|
|
112
|
-
if (/export\s+async\s+function\s+getStaticProps\s*\(/.test(content))
|
|
113
|
-
return true;
|
|
114
|
-
if (/['"]use server['"]/.test(content))
|
|
115
|
-
return true;
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
findEnvExposures(file, content) {
|
|
119
|
-
const findings = [];
|
|
120
|
-
if (this.config.check_process_env) {
|
|
121
|
-
const processEnvRegex = /process\.env\.([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
122
|
-
findings.push(...this.collectMatches(file, content, processEnvRegex, 'process.env'));
|
|
123
|
-
}
|
|
124
|
-
if (this.config.check_import_meta_env) {
|
|
125
|
-
const importMetaRegex = /import\.meta\.env\.([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
126
|
-
findings.push(...this.collectMatches(file, content, importMetaRegex, 'import.meta.env'));
|
|
127
|
-
}
|
|
128
|
-
return findings;
|
|
246
|
+
return this.secretNamePatterns.some(p => p.test(varName));
|
|
129
247
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
for (
|
|
134
|
-
const
|
|
135
|
-
|
|
248
|
+
/** Scan one file for env-var references and literal key values. */
|
|
249
|
+
scanFile(content, file, fileContext, out) {
|
|
250
|
+
const lines = content.split('\n');
|
|
251
|
+
for (let i = 0; i < lines.length; i++) {
|
|
252
|
+
const line = lines[i];
|
|
253
|
+
const lineNum = i + 1;
|
|
254
|
+
// Skip comment lines to avoid flagging docs/examples
|
|
255
|
+
const trimmed = line.trimStart();
|
|
256
|
+
if (trimmed.startsWith('//') ||
|
|
257
|
+
trimmed.startsWith('*') ||
|
|
258
|
+
trimmed.startsWith('#') ||
|
|
259
|
+
trimmed.startsWith('<!--'))
|
|
136
260
|
continue;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (this.config.allowlist_env_names.includes(envVar))
|
|
152
|
-
return false;
|
|
153
|
-
if (this.config.safe_public_prefixes.some(prefix => envVar.startsWith(prefix)))
|
|
154
|
-
return false;
|
|
155
|
-
return this.config.secret_env_name_patterns.some(pattern => {
|
|
156
|
-
try {
|
|
157
|
-
return new RegExp(pattern, 'i').test(envVar);
|
|
261
|
+
// 1. Env-var name detection (process.env / import.meta.env)
|
|
262
|
+
if (this.cfg.check_process_env || this.cfg.check_import_meta_env) {
|
|
263
|
+
const single = extractEnvVarName(line, this.cfg.check_process_env, this.cfg.check_import_meta_env);
|
|
264
|
+
if (single && this.isSecretVar(single)) {
|
|
265
|
+
out.push(this.makeEnvRefExposure(file, lineNum, line, single, fileContext));
|
|
266
|
+
}
|
|
267
|
+
// Destructuring: const { STRIPE_SECRET_KEY } = process.env
|
|
268
|
+
if (this.cfg.check_process_env) {
|
|
269
|
+
for (const varName of extractDestructuredEnvVars(line)) {
|
|
270
|
+
if (this.isSecretVar(varName)) {
|
|
271
|
+
out.push(this.makeEnvRefExposure(file, lineNum, line, varName, fileContext));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
158
275
|
}
|
|
159
|
-
|
|
160
|
-
|
|
276
|
+
// 2. Literal API key detection
|
|
277
|
+
for (const pattern of LITERAL_KEY_PATTERNS) {
|
|
278
|
+
pattern.regex.lastIndex = 0;
|
|
279
|
+
let m;
|
|
280
|
+
while ((m = pattern.regex.exec(line)) !== null) {
|
|
281
|
+
// Skip dummy/placeholder values and test-mode keys
|
|
282
|
+
if (this.isDummyValue(m[0]))
|
|
283
|
+
continue;
|
|
284
|
+
if (/(?:sk_test_|pk_test_|_test_|_sandbox_)/i.test(m[0]))
|
|
285
|
+
continue;
|
|
286
|
+
out.push({
|
|
287
|
+
file,
|
|
288
|
+
line: lineNum,
|
|
289
|
+
lineText: line.trim().slice(0, 80),
|
|
290
|
+
varName: m[0].slice(0, 24) + (m[0].length > 24 ? '...' : ''),
|
|
291
|
+
kind: 'literal',
|
|
292
|
+
serviceLabel: pattern.label,
|
|
293
|
+
hint: pattern.hint,
|
|
294
|
+
cwe: pattern.cwe,
|
|
295
|
+
severity: pattern.severity,
|
|
296
|
+
fileContext,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
161
299
|
}
|
|
162
|
-
}
|
|
300
|
+
}
|
|
163
301
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
302
|
+
makeEnvRefExposure(file, line, lineText, varName, fileContext) {
|
|
303
|
+
// Definite frontend → critical; ambiguous → high (may be a shared util)
|
|
304
|
+
const severity = fileContext === 'frontend' ? 'critical' : 'high';
|
|
305
|
+
return {
|
|
306
|
+
file,
|
|
307
|
+
line,
|
|
308
|
+
lineText: lineText.trim().slice(0, 80),
|
|
309
|
+
varName,
|
|
310
|
+
kind: 'env-ref',
|
|
311
|
+
serviceLabel: 'Secret Env Var',
|
|
312
|
+
hint: `\`${varName}\` looks like a server-side secret but is referenced in a ` +
|
|
313
|
+
`client-bundled file. Use an API route (Next.js /api, Remix action, ` +
|
|
314
|
+
`Nuxt server route) to proxy calls instead of exposing the secret to the browser.`,
|
|
315
|
+
cwe: 'CWE-312',
|
|
316
|
+
severity,
|
|
317
|
+
fileContext,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/** Filters out trivial dummy/placeholder text to reduce false positives. */
|
|
321
|
+
isDummyValue(text) {
|
|
322
|
+
return /(?:example|placeholder|your[_-]|xxx+|dummy|fake|changeme)/i.test(text);
|
|
323
|
+
}
|
|
324
|
+
toFailures(exposures) {
|
|
325
|
+
if (exposures.length === 0)
|
|
326
|
+
return [];
|
|
327
|
+
const blockThreshold = SEVERITY_ORDER[this.cfg.block_on_severity];
|
|
328
|
+
// Deduplicate: same file + line + varName from parallel patterns
|
|
329
|
+
const seen = new Set();
|
|
330
|
+
const unique = exposures.filter(e => {
|
|
331
|
+
const key = `${e.file}:${e.line}:${e.varName}`;
|
|
332
|
+
if (seen.has(key))
|
|
170
333
|
return false;
|
|
171
|
-
|
|
334
|
+
seen.add(key);
|
|
335
|
+
return true;
|
|
172
336
|
});
|
|
337
|
+
// Sort critical first
|
|
338
|
+
unique.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
|
|
339
|
+
const failures = [];
|
|
340
|
+
for (const exp of unique) {
|
|
341
|
+
if (SEVERITY_ORDER[exp.severity] > blockThreshold)
|
|
342
|
+
continue;
|
|
343
|
+
const ctx = exp.fileContext === 'frontend' ? '[definite frontend]' : '[possible frontend]';
|
|
344
|
+
const kind = exp.kind === 'literal' ? 'literal key' : 'env var ref';
|
|
345
|
+
failures.push(this.createFailure(`[${exp.cwe}] ${exp.serviceLabel} (${kind}) in ${ctx} file — ` +
|
|
346
|
+
`\`${exp.varName}\` at line ${exp.line}`, [exp.file], exp.hint, `Security: Frontend Secret Exposure — ${exp.serviceLabel}`, exp.line, exp.line, exp.severity));
|
|
347
|
+
}
|
|
348
|
+
if (unique.length > 0 && failures.length === 0) {
|
|
349
|
+
Logger.info(`Frontend Secret Exposure: ${unique.length} issue(s) below ` +
|
|
350
|
+
`${this.cfg.block_on_severity} threshold`);
|
|
351
|
+
}
|
|
352
|
+
return failures;
|
|
173
353
|
}
|
|
174
354
|
}
|
|
@@ -47,9 +47,11 @@ export class HallucinatedImportsGate extends Gate {
|
|
|
47
47
|
return [];
|
|
48
48
|
const failures = [];
|
|
49
49
|
const hallucinated = [];
|
|
50
|
+
const defaultPatterns = ['**/*.{ts,js,tsx,jsx,py,go,rb,cs,rs,java,kt}'];
|
|
51
|
+
const scanPatterns = context.patterns || defaultPatterns;
|
|
50
52
|
const files = await FileScanner.findFiles({
|
|
51
53
|
cwd: context.cwd,
|
|
52
|
-
patterns:
|
|
54
|
+
patterns: scanPatterns,
|
|
53
55
|
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
|
|
54
56
|
'**/examples/**',
|
|
55
57
|
'**/studio-dist/**', '**/.next/**', '**/coverage/**',
|
|
@@ -41,9 +41,10 @@ export class InconsistentErrorHandlingGate extends Gate {
|
|
|
41
41
|
return [];
|
|
42
42
|
const failures = [];
|
|
43
43
|
const handlers = [];
|
|
44
|
+
const scanPatterns = context.patterns || ['**/*.{ts,js,tsx,jsx}'];
|
|
44
45
|
const files = await FileScanner.findFiles({
|
|
45
46
|
cwd: context.cwd,
|
|
46
|
-
patterns:
|
|
47
|
+
patterns: scanPatterns,
|
|
47
48
|
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*'],
|
|
48
49
|
});
|
|
49
50
|
Logger.info(`Inconsistent Error Handling: Scanning ${files.length} files`);
|
|
@@ -45,9 +45,11 @@ export class PhantomApisGate extends Gate {
|
|
|
45
45
|
return [];
|
|
46
46
|
const failures = [];
|
|
47
47
|
const phantoms = [];
|
|
48
|
+
const defaultPatterns = ['**/*.{ts,js,tsx,jsx,py,go,cs,java,kt}'];
|
|
49
|
+
const scanPatterns = context.patterns || defaultPatterns;
|
|
48
50
|
const files = await FileScanner.findFiles({
|
|
49
51
|
cwd: context.cwd,
|
|
50
|
-
patterns:
|
|
52
|
+
patterns: scanPatterns,
|
|
51
53
|
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
|
|
52
54
|
'**/*.test.*', '**/*.spec.*', '**/__tests__/**',
|
|
53
55
|
'**/.venv/**', '**/venv/**', '**/vendor/**', '**/__pycache__/**',
|
|
@@ -39,7 +39,7 @@ export function extractIndentedBody(content, startIdx) {
|
|
|
39
39
|
}
|
|
40
40
|
export function isInsideTryBlock(lines, lineIdx) {
|
|
41
41
|
let braceDepth = 0;
|
|
42
|
-
for (let j = lineIdx - 1; j >= Math.max(0, lineIdx -
|
|
42
|
+
for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - 100); j--) {
|
|
43
43
|
const prevLine = stripStrings(lines[j]);
|
|
44
44
|
for (const ch of prevLine) {
|
|
45
45
|
if (ch === '}')
|
|
@@ -56,7 +56,7 @@ export function isInsideTryBlock(lines, lineIdx) {
|
|
|
56
56
|
}
|
|
57
57
|
export function isInsidePythonTry(lines, lineIdx) {
|
|
58
58
|
const lineIndent = lines[lineIdx].length - lines[lineIdx].trimStart().length;
|
|
59
|
-
for (let j = lineIdx - 1; j >= Math.max(0, lineIdx -
|
|
59
|
+
for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - 100); j--) {
|
|
60
60
|
const trimmed = lines[j].trim();
|
|
61
61
|
if (trimmed === '')
|
|
62
62
|
continue;
|
|
@@ -71,7 +71,7 @@ export function isInsidePythonTry(lines, lineIdx) {
|
|
|
71
71
|
return false;
|
|
72
72
|
}
|
|
73
73
|
export function isInsideRubyRescue(lines, lineIdx) {
|
|
74
|
-
for (let j = lineIdx - 1; j >= Math.max(0, lineIdx -
|
|
74
|
+
for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - 100); j--) {
|
|
75
75
|
const trimmed = lines[j].trim();
|
|
76
76
|
if (trimmed === 'begin')
|
|
77
77
|
return true;
|
|
@@ -83,14 +83,14 @@ export function isInsideRubyRescue(lines, lineIdx) {
|
|
|
83
83
|
return false;
|
|
84
84
|
}
|
|
85
85
|
export function hasCatchAhead(lines, idx) {
|
|
86
|
-
for (let j = idx; j < Math.min(idx +
|
|
86
|
+
for (let j = idx; j < Math.min(idx + 50, lines.length); j++) {
|
|
87
87
|
if (/\.catch\s*\(/.test(lines[j]))
|
|
88
88
|
return true;
|
|
89
89
|
}
|
|
90
90
|
return false;
|
|
91
91
|
}
|
|
92
92
|
export function hasStatusCheckAhead(lines, idx) {
|
|
93
|
-
for (let j = idx; j < Math.min(idx +
|
|
93
|
+
for (let j = idx; j < Math.min(idx + 50, lines.length); j++) {
|
|
94
94
|
if (/\.\s*ok\b/.test(lines[j]) || /\.status(?:Text)?\b/.test(lines[j]))
|
|
95
95
|
return true;
|
|
96
96
|
}
|
|
@@ -36,9 +36,10 @@ export class PromiseSafetyGate extends Gate {
|
|
|
36
36
|
return [];
|
|
37
37
|
const violations = [];
|
|
38
38
|
const allPatterns = Object.values(LANG_GLOBS).flat();
|
|
39
|
+
const scanPatterns = context.patterns || allPatterns;
|
|
39
40
|
const files = await FileScanner.findFiles({
|
|
40
41
|
cwd: context.cwd,
|
|
41
|
-
patterns:
|
|
42
|
+
patterns: scanPatterns,
|
|
42
43
|
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
|
|
43
44
|
'**/*.test.*', '**/*.spec.*', '**/vendor/**', '**/__pycache__/**',
|
|
44
45
|
'**/bin/Debug/**', '**/bin/Release/**', '**/obj/**', '**/venv/**', '**/.venv/**'],
|
|
@@ -87,7 +88,7 @@ export class PromiseSafetyGate extends Gate {
|
|
|
87
88
|
if (!/\.then\s*\(/.test(line))
|
|
88
89
|
continue;
|
|
89
90
|
let hasCatch = false;
|
|
90
|
-
for (let j = i; j < Math.min(i +
|
|
91
|
+
for (let j = i; j < Math.min(i + 50, lines.length); j++) {
|
|
91
92
|
const lookahead = this.sanitizeLine(lines[j]);
|
|
92
93
|
if (/\.catch\s*\(/.test(lookahead)) {
|
|
93
94
|
hasCatch = true;
|
|
@@ -209,7 +210,7 @@ export class PromiseSafetyGate extends Gate {
|
|
|
209
210
|
if (!httpPatterns.test(lines[i]) || isInsidePythonTry(lines, i))
|
|
210
211
|
continue;
|
|
211
212
|
let hasCheck = false;
|
|
212
|
-
for (let j = i; j < Math.min(i +
|
|
213
|
+
for (let j = i; j < Math.min(i + 50, lines.length); j++) {
|
|
213
214
|
if (/raise_for_status|status_code/.test(lines[j])) {
|
|
214
215
|
hasCheck = true;
|
|
215
216
|
break;
|
|
@@ -317,7 +318,7 @@ export class PromiseSafetyGate extends Gate {
|
|
|
317
318
|
for (let i = 0; i < lines.length; i++) {
|
|
318
319
|
if (/\.(?:GetAsync|PostAsync|SendAsync)\s*\(/.test(lines[i]) && !isInsideTryBlock(lines, i)) {
|
|
319
320
|
let hasCheck = false;
|
|
320
|
-
for (let j = i; j < Math.min(i +
|
|
321
|
+
for (let j = i; j < Math.min(i + 50, lines.length); j++) {
|
|
321
322
|
if (/EnsureSuccess|IsSuccessStatusCode|StatusCode/.test(lines[j])) {
|
|
322
323
|
hasCheck = true;
|
|
323
324
|
break;
|
package/dist/gates/runner.js
CHANGED
|
@@ -199,13 +199,13 @@ export class GateRunner {
|
|
|
199
199
|
}
|
|
200
200
|
const isLocalDeepExecution = !deepOptions.apiKey || (deepOptions.provider || '').toLowerCase() === 'local';
|
|
201
201
|
const deepTier = isLocalDeepExecution
|
|
202
|
-
? (deepOptions.pro ? '
|
|
202
|
+
? (deepOptions.pro ? 'deep' : 'lite')
|
|
203
203
|
: 'cloud';
|
|
204
204
|
deepStats = {
|
|
205
205
|
enabled: true,
|
|
206
206
|
tier: deepTier,
|
|
207
207
|
model: isLocalDeepExecution
|
|
208
|
-
? (deepOptions.pro ? 'Qwen2.5-Coder-1.5B' : '
|
|
208
|
+
? (deepOptions.pro ? 'Qwen2.5-Coder-1.5B' : 'Qwen3.5-0.8B')
|
|
209
209
|
: (deepOptions.modelName || deepOptions.provider || 'cloud'),
|
|
210
210
|
total_ms: Date.now() - deepSetupStart,
|
|
211
211
|
findings_count: deepFailures.length,
|
|
@@ -25,7 +25,7 @@ describe('GateRunner deep stats execution mode', () => {
|
|
|
25
25
|
},
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
|
-
it('reports local
|
|
28
|
+
it('reports local lite tier when provider=local even with apiKey', async () => {
|
|
29
29
|
vi.spyOn(DeepAnalysisGate.prototype, 'run').mockResolvedValue([]);
|
|
30
30
|
const runner = createRunner();
|
|
31
31
|
const report = await runner.run(testDir, undefined, {
|
|
@@ -34,10 +34,10 @@ describe('GateRunner deep stats execution mode', () => {
|
|
|
34
34
|
provider: 'local',
|
|
35
35
|
pro: false,
|
|
36
36
|
});
|
|
37
|
-
expect(report.stats.deep?.tier).toBe('
|
|
38
|
-
expect(report.stats.deep?.model).toBe('
|
|
37
|
+
expect(report.stats.deep?.tier).toBe('lite');
|
|
38
|
+
expect(report.stats.deep?.model).toBe('Qwen3.5-0.8B');
|
|
39
39
|
});
|
|
40
|
-
it('reports local
|
|
40
|
+
it('reports local deep tier when provider=local and pro=true', async () => {
|
|
41
41
|
vi.spyOn(DeepAnalysisGate.prototype, 'run').mockResolvedValue([]);
|
|
42
42
|
const runner = createRunner();
|
|
43
43
|
const report = await runner.run(testDir, undefined, {
|
|
@@ -46,7 +46,7 @@ describe('GateRunner deep stats execution mode', () => {
|
|
|
46
46
|
provider: 'local',
|
|
47
47
|
pro: true,
|
|
48
48
|
});
|
|
49
|
-
expect(report.stats.deep?.tier).toBe('
|
|
49
|
+
expect(report.stats.deep?.tier).toBe('deep');
|
|
50
50
|
expect(report.stats.deep?.model).toBe('Qwen2.5-Coder-1.5B');
|
|
51
51
|
});
|
|
52
52
|
it('reports cloud tier/model for cloud providers', async () => {
|
|
@@ -251,9 +251,10 @@ export class SecurityPatternsGate extends Gate {
|
|
|
251
251
|
}
|
|
252
252
|
const failures = [];
|
|
253
253
|
const vulnerabilities = [];
|
|
254
|
+
const scanPatterns = context.patterns || ['**/*.{ts,js,tsx,jsx,py,java,go}'];
|
|
254
255
|
const files = await FileScanner.findFiles({
|
|
255
256
|
cwd: context.cwd,
|
|
256
|
-
patterns:
|
|
257
|
+
patterns: scanPatterns,
|
|
257
258
|
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/coverage/**'],
|
|
258
259
|
});
|
|
259
260
|
const scanFiles = files.filter(file => !this.shouldSkipSecurityFile(file));
|
|
@@ -73,9 +73,10 @@ export class SideEffectAnalysisGate extends Gate {
|
|
|
73
73
|
if (!this.cfg.enabled)
|
|
74
74
|
return [];
|
|
75
75
|
const violations = [];
|
|
76
|
+
const scanPatterns = context.patterns || FILE_GLOBS;
|
|
76
77
|
const files = await FileScanner.findFiles({
|
|
77
78
|
cwd: context.cwd,
|
|
78
|
-
patterns:
|
|
79
|
+
patterns: scanPatterns,
|
|
79
80
|
ignore: [
|
|
80
81
|
...(context.ignore || []),
|
|
81
82
|
'**/node_modules/**', '**/dist/**', '**/build/**',
|
|
@@ -50,16 +50,18 @@ export class TestQualityGate extends Gate {
|
|
|
50
50
|
return [];
|
|
51
51
|
const failures = [];
|
|
52
52
|
const issues = [];
|
|
53
|
+
const defaultPatterns = [
|
|
54
|
+
'**/*.test.{ts,js,tsx,jsx}', '**/*.spec.{ts,js,tsx,jsx}',
|
|
55
|
+
'**/__tests__/**/*.{ts,js,tsx,jsx}',
|
|
56
|
+
'**/test_*.py', '**/*_test.py', '**/tests/**/*.py',
|
|
57
|
+
'**/*_test.go',
|
|
58
|
+
'**/*Test.java', '**/*Tests.java', '**/src/test/**/*.java',
|
|
59
|
+
'**/*Test.kt', '**/*Tests.kt', '**/src/test/**/*.kt',
|
|
60
|
+
];
|
|
61
|
+
const scanPatterns = context.patterns || defaultPatterns;
|
|
53
62
|
const files = await FileScanner.findFiles({
|
|
54
63
|
cwd: context.cwd,
|
|
55
|
-
patterns:
|
|
56
|
-
'**/*.test.{ts,js,tsx,jsx}', '**/*.spec.{ts,js,tsx,jsx}',
|
|
57
|
-
'**/__tests__/**/*.{ts,js,tsx,jsx}',
|
|
58
|
-
'**/test_*.py', '**/*_test.py', '**/tests/**/*.py',
|
|
59
|
-
'**/*_test.go',
|
|
60
|
-
'**/*Test.java', '**/*Tests.java', '**/src/test/**/*.java',
|
|
61
|
-
'**/*Test.kt', '**/*Tests.kt', '**/src/test/**/*.kt',
|
|
62
|
-
],
|
|
64
|
+
patterns: scanPatterns,
|
|
63
65
|
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
|
|
64
66
|
'**/.venv/**', '**/venv/**', '**/vendor/**',
|
|
65
67
|
'**/target/**', '**/.gradle/**', '**/out/**'],
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
const DEFAULT_MODELS = {
|
|
3
3
|
claude: 'claude-opus-4-6',
|
|
4
4
|
anthropic: 'claude-sonnet-4-6',
|
|
5
|
-
openai: 'gpt-
|
|
6
|
-
gemini: 'gemini-
|
|
7
|
-
groq: 'llama-3.
|
|
5
|
+
openai: 'gpt-5-mini',
|
|
6
|
+
gemini: 'gemini-2.5-flash',
|
|
7
|
+
groq: 'llama-3.3-70b-versatile',
|
|
8
8
|
mistral: 'mistral-large-latest',
|
|
9
|
-
together: 'meta-llama/Llama-
|
|
10
|
-
fireworks: 'accounts/fireworks/models/llama-
|
|
11
|
-
deepseek: 'deepseek-
|
|
9
|
+
together: 'meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8',
|
|
10
|
+
fireworks: 'accounts/fireworks/models/llama-v3p3-70b-instruct',
|
|
11
|
+
deepseek: 'deepseek-v4',
|
|
12
12
|
perplexity: 'llama-3.1-sonar-large-128k-online',
|
|
13
13
|
ollama: 'qwen2.5-coder:7b',
|
|
14
14
|
lmstudio: 'qwen2.5-coder-7b-instruct',
|
package/dist/inference/index.js
CHANGED
|
@@ -18,6 +18,8 @@ export function createProvider(options) {
|
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
20
|
// Default: local sidecar
|
|
21
|
-
|
|
21
|
+
// deep = Qwen2.5-Coder-1.5B (full power, company-hosted)
|
|
22
|
+
// lite = Qwen3.5-0.8B (lightweight, default CLI sidecar)
|
|
23
|
+
const tier = options.pro ? 'deep' : 'lite';
|
|
22
24
|
return new SidecarProvider(tier);
|
|
23
25
|
}
|
|
@@ -13,9 +13,17 @@ export declare function getModelPath(tier: ModelTier): string;
|
|
|
13
13
|
* Get model info for a tier.
|
|
14
14
|
*/
|
|
15
15
|
export declare function getModelInfo(tier: ModelTier): ModelInfo;
|
|
16
|
+
/**
|
|
17
|
+
* Check HuggingFace for a newer model version (like antivirus signature updates).
|
|
18
|
+
* Reads latest_version.json from the RLAIF dataset repo. Non-blocking — if the
|
|
19
|
+
* check fails (offline, HF down), we silently use the cached/bundled version.
|
|
20
|
+
*
|
|
21
|
+
* Results are cached locally for 24 hours to avoid hammering HF on every run.
|
|
22
|
+
*/
|
|
23
|
+
export declare function checkForUpdates(onProgress?: (message: string, percent?: number) => void): Promise<string>;
|
|
16
24
|
/**
|
|
17
25
|
* Download a model from HuggingFace CDN.
|
|
18
|
-
*
|
|
26
|
+
* Checks for updates first, then tries fine-tuned model, falls back to stock Qwen.
|
|
19
27
|
*/
|
|
20
28
|
export declare function downloadModel(tier: ModelTier, onProgress?: (message: string, percent?: number) => void): Promise<string>;
|
|
21
29
|
/**
|
|
@@ -6,8 +6,11 @@ import path from 'path';
|
|
|
6
6
|
import fs from 'fs-extra';
|
|
7
7
|
import { createHash } from 'crypto';
|
|
8
8
|
import { RIGOUR_DIR } from '../storage/db.js';
|
|
9
|
-
import { MODELS, FALLBACK_MODELS } from './types.js';
|
|
9
|
+
import { MODELS, FALLBACK_MODELS, VERSION_CHECK_URL, BUNDLED_MODEL_VERSION, updateModelVersion } from './types.js';
|
|
10
10
|
const MODELS_DIR = path.join(RIGOUR_DIR, 'models');
|
|
11
|
+
const VERSION_CACHE_PATH = path.join(MODELS_DIR, '.latest_version.json');
|
|
12
|
+
const VERSION_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // Check once per day
|
|
13
|
+
const VERSION_CHECK_TIMEOUT_MS = 5000; // 5s timeout — don't block startup
|
|
11
14
|
const SHA256_RE = /^[a-f0-9]{64}$/i;
|
|
12
15
|
function getModelMetadataPath(filename) {
|
|
13
16
|
return path.join(MODELS_DIR, filename + '.meta.json');
|
|
@@ -173,12 +176,66 @@ async function downloadFromUrl(tier, model, onProgress) {
|
|
|
173
176
|
throw error;
|
|
174
177
|
}
|
|
175
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Check HuggingFace for a newer model version (like antivirus signature updates).
|
|
181
|
+
* Reads latest_version.json from the RLAIF dataset repo. Non-blocking — if the
|
|
182
|
+
* check fails (offline, HF down), we silently use the cached/bundled version.
|
|
183
|
+
*
|
|
184
|
+
* Results are cached locally for 24 hours to avoid hammering HF on every run.
|
|
185
|
+
*/
|
|
186
|
+
export async function checkForUpdates(onProgress) {
|
|
187
|
+
fs.ensureDirSync(MODELS_DIR);
|
|
188
|
+
// Check local version cache first — avoid network on every run
|
|
189
|
+
try {
|
|
190
|
+
if (await fs.pathExists(VERSION_CACHE_PATH)) {
|
|
191
|
+
const cached = await fs.readJson(VERSION_CACHE_PATH);
|
|
192
|
+
const age = Date.now() - new Date(cached.checkedAt).getTime();
|
|
193
|
+
if (age < VERSION_CHECK_INTERVAL_MS && cached.version) {
|
|
194
|
+
const v = String(cached.version);
|
|
195
|
+
updateModelVersion(v);
|
|
196
|
+
return v;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// Corrupted cache — proceed to network check
|
|
202
|
+
}
|
|
203
|
+
// Fetch latest version from HuggingFace (with timeout)
|
|
204
|
+
try {
|
|
205
|
+
const controller = new AbortController();
|
|
206
|
+
const timeout = setTimeout(() => controller.abort(), VERSION_CHECK_TIMEOUT_MS);
|
|
207
|
+
const response = await fetch(VERSION_CHECK_URL, { signal: controller.signal });
|
|
208
|
+
clearTimeout(timeout);
|
|
209
|
+
if (response.ok) {
|
|
210
|
+
const data = await response.json();
|
|
211
|
+
const latestVersion = String(data.version || BUNDLED_MODEL_VERSION);
|
|
212
|
+
// Cache the result locally
|
|
213
|
+
await fs.writeJson(VERSION_CACHE_PATH, {
|
|
214
|
+
version: latestVersion,
|
|
215
|
+
checkedAt: new Date().toISOString(),
|
|
216
|
+
source: 'huggingface',
|
|
217
|
+
}, { spaces: 2 }).catch(() => { });
|
|
218
|
+
// Update in-memory model definitions
|
|
219
|
+
updateModelVersion(latestVersion);
|
|
220
|
+
if (latestVersion !== BUNDLED_MODEL_VERSION) {
|
|
221
|
+
onProgress?.(`Model update available: v${latestVersion}`, 0);
|
|
222
|
+
}
|
|
223
|
+
return latestVersion;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// Offline / HF down / timeout — use bundled version silently
|
|
228
|
+
}
|
|
229
|
+
return BUNDLED_MODEL_VERSION;
|
|
230
|
+
}
|
|
176
231
|
/**
|
|
177
232
|
* Download a model from HuggingFace CDN.
|
|
178
|
-
*
|
|
233
|
+
* Checks for updates first, then tries fine-tuned model, falls back to stock Qwen.
|
|
179
234
|
*/
|
|
180
235
|
export async function downloadModel(tier, onProgress) {
|
|
181
236
|
fs.ensureDirSync(MODELS_DIR);
|
|
237
|
+
// Check for newer model version (non-blocking, cached 24h)
|
|
238
|
+
await checkForUpdates(onProgress);
|
|
182
239
|
if (await isModelCached(tier)) {
|
|
183
240
|
onProgress?.(`Model ${MODELS[tier].name} already cached`, 100);
|
|
184
241
|
return getModelPath(tier);
|
|
@@ -60,8 +60,12 @@ export interface DeepAnalysisResult {
|
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
62
|
* Available model tiers.
|
|
63
|
+
*
|
|
64
|
+
* - deep: Qwen2.5-Coder-1.5B fine-tuned — full power, company-hosted
|
|
65
|
+
* - lite: Qwen2.5-Coder-0.5B fine-tuned — lightweight, ships as default CLI sidecar
|
|
66
|
+
* - legacy: Qwen2.5-Coder-0.5B fine-tuned — previous default, reproducibility
|
|
63
67
|
*/
|
|
64
|
-
export type ModelTier = 'deep' | '
|
|
68
|
+
export type ModelTier = 'deep' | 'lite' | 'legacy';
|
|
65
69
|
/**
|
|
66
70
|
* Model info for download/caching.
|
|
67
71
|
*/
|
|
@@ -74,13 +78,24 @@ export interface ModelInfo {
|
|
|
74
78
|
sizeHuman: string;
|
|
75
79
|
}
|
|
76
80
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
81
|
+
* Minimum bundled model version — used as fallback when the auto-update
|
|
82
|
+
* check fails (offline, HF down, first run). The RLAIF training pipeline
|
|
83
|
+
* publishes new versions to HuggingFace and updates latest_version.json.
|
|
84
|
+
* At startup, model-manager checks HF for the latest version and downloads
|
|
85
|
+
* it automatically (like antivirus signature updates).
|
|
80
86
|
*/
|
|
81
|
-
export declare const
|
|
82
|
-
/**
|
|
87
|
+
export declare const BUNDLED_MODEL_VERSION = "1";
|
|
88
|
+
/** HuggingFace dataset repo where latest_version.json lives */
|
|
89
|
+
export declare const VERSION_CHECK_URL = "https://huggingface.co/datasets/rigour-labs/rigour-rlaif-data/resolve/main/latest_version.json";
|
|
90
|
+
/** Build model info for a given tier and version */
|
|
91
|
+
export declare function buildModelInfo(tier: ModelTier, version: string): ModelInfo;
|
|
92
|
+
/** Current model definitions — initialized with bundled version, updated at runtime */
|
|
83
93
|
export declare const MODELS: Record<ModelTier, ModelInfo>;
|
|
94
|
+
/**
|
|
95
|
+
* Update MODELS in-place to point to a newer version.
|
|
96
|
+
* Called by model-manager after checking latest_version.json.
|
|
97
|
+
*/
|
|
98
|
+
export declare function updateModelVersion(version: string): void;
|
|
84
99
|
/**
|
|
85
100
|
* Fallback stock models — used when fine-tuned model is not yet
|
|
86
101
|
* available on HuggingFace (initial setup / first-time users).
|
package/dist/inference/types.js
CHANGED
|
@@ -1,28 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Minimum bundled model version — used as fallback when the auto-update
|
|
3
|
+
* check fails (offline, HF down, first run). The RLAIF training pipeline
|
|
4
|
+
* publishes new versions to HuggingFace and updates latest_version.json.
|
|
5
|
+
* At startup, model-manager checks HF for the latest version and downloads
|
|
6
|
+
* it automatically (like antivirus signature updates).
|
|
5
7
|
*/
|
|
6
|
-
export const
|
|
7
|
-
/**
|
|
8
|
+
export const BUNDLED_MODEL_VERSION = '1';
|
|
9
|
+
/** HuggingFace dataset repo where latest_version.json lives */
|
|
10
|
+
export const VERSION_CHECK_URL = 'https://huggingface.co/datasets/rigour-labs/rigour-rlaif-data/resolve/main/latest_version.json';
|
|
11
|
+
/** Build model info for a given tier and version */
|
|
12
|
+
export function buildModelInfo(tier, version) {
|
|
13
|
+
const meta = {
|
|
14
|
+
deep: { base: 'Qwen2.5-Coder-1.5B', size: 900_000_000, sizeH: '900MB' },
|
|
15
|
+
lite: { base: 'Qwen2.5-Coder-0.5B', size: 500_000_000, sizeH: '500MB' },
|
|
16
|
+
legacy: { base: 'Qwen2.5-Coder-0.5B', size: 350_000_000, sizeH: '350MB' },
|
|
17
|
+
};
|
|
18
|
+
const m = meta[tier];
|
|
19
|
+
return {
|
|
20
|
+
tier,
|
|
21
|
+
name: `Rigour-${tier[0].toUpperCase() + tier.slice(1)}-v${version} (${m.base} fine-tuned)`,
|
|
22
|
+
filename: `rigour-${tier}-v${version}-q4_k_m.gguf`,
|
|
23
|
+
url: `https://huggingface.co/rigour-labs/rigour-${tier}-v${version}-gguf/resolve/main/rigour-${tier}-v${version}-q4_k_m.gguf`,
|
|
24
|
+
sizeBytes: m.size,
|
|
25
|
+
sizeHuman: m.sizeH,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Current model definitions — initialized with bundled version, updated at runtime */
|
|
8
29
|
export const MODELS = {
|
|
9
|
-
deep:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
filename: `rigour-deep-v${MODEL_VERSION}-q4_k_m.gguf`,
|
|
13
|
-
url: `https://huggingface.co/rigour-labs/rigour-deep-v1-gguf/resolve/main/rigour-deep-v${MODEL_VERSION}-q4_k_m.gguf`,
|
|
14
|
-
sizeBytes: 350_000_000,
|
|
15
|
-
sizeHuman: '350MB',
|
|
16
|
-
},
|
|
17
|
-
pro: {
|
|
18
|
-
tier: 'pro',
|
|
19
|
-
name: 'Rigour-Pro-v1 (Qwen2.5-Coder-1.5B fine-tuned)',
|
|
20
|
-
filename: `rigour-pro-v${MODEL_VERSION}-q4_k_m.gguf`,
|
|
21
|
-
url: `https://huggingface.co/rigour-labs/rigour-pro-v1-gguf/resolve/main/rigour-pro-v${MODEL_VERSION}-q4_k_m.gguf`,
|
|
22
|
-
sizeBytes: 900_000_000,
|
|
23
|
-
sizeHuman: '900MB',
|
|
24
|
-
},
|
|
30
|
+
deep: buildModelInfo('deep', BUNDLED_MODEL_VERSION),
|
|
31
|
+
lite: buildModelInfo('lite', BUNDLED_MODEL_VERSION),
|
|
32
|
+
legacy: buildModelInfo('legacy', BUNDLED_MODEL_VERSION),
|
|
25
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* Update MODELS in-place to point to a newer version.
|
|
36
|
+
* Called by model-manager after checking latest_version.json.
|
|
37
|
+
*/
|
|
38
|
+
export function updateModelVersion(version) {
|
|
39
|
+
for (const tier of ['deep', 'lite', 'legacy']) {
|
|
40
|
+
const updated = buildModelInfo(tier, version);
|
|
41
|
+
MODELS[tier] = updated;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
26
44
|
/**
|
|
27
45
|
* Fallback stock models — used when fine-tuned model is not yet
|
|
28
46
|
* available on HuggingFace (initial setup / first-time users).
|
|
@@ -30,18 +48,26 @@ export const MODELS = {
|
|
|
30
48
|
export const FALLBACK_MODELS = {
|
|
31
49
|
deep: {
|
|
32
50
|
tier: 'deep',
|
|
33
|
-
name: 'Qwen2.5-Coder-0.5B-Instruct (stock)',
|
|
34
|
-
filename: 'qwen2.5-coder-0.5b-instruct-q4_k_m.gguf',
|
|
35
|
-
url: 'https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct-GGUF/resolve/main/qwen2.5-coder-0.5b-instruct-q4_k_m.gguf',
|
|
36
|
-
sizeBytes: 350_000_000,
|
|
37
|
-
sizeHuman: '350MB',
|
|
38
|
-
},
|
|
39
|
-
pro: {
|
|
40
|
-
tier: 'pro',
|
|
41
51
|
name: 'Qwen2.5-Coder-1.5B-Instruct (stock)',
|
|
42
52
|
filename: 'qwen2.5-coder-1.5b-instruct-q4_k_m.gguf',
|
|
43
53
|
url: 'https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF/resolve/main/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf',
|
|
44
54
|
sizeBytes: 900_000_000,
|
|
45
55
|
sizeHuman: '900MB',
|
|
46
56
|
},
|
|
57
|
+
lite: {
|
|
58
|
+
tier: 'lite',
|
|
59
|
+
name: 'Qwen2.5-Coder-0.5B-Instruct (stock)',
|
|
60
|
+
filename: 'qwen2.5-coder-0.5b-instruct-q4_k_m.gguf',
|
|
61
|
+
url: 'https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct-GGUF/resolve/main/qwen2.5-coder-0.5b-instruct-q4_k_m.gguf',
|
|
62
|
+
sizeBytes: 500_000_000,
|
|
63
|
+
sizeHuman: '500MB',
|
|
64
|
+
},
|
|
65
|
+
legacy: {
|
|
66
|
+
tier: 'legacy',
|
|
67
|
+
name: 'Qwen2.5-Coder-0.5B-Instruct (stock)',
|
|
68
|
+
filename: 'qwen2.5-coder-0.5b-instruct-q4_k_m.gguf',
|
|
69
|
+
url: 'https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct-GGUF/resolve/main/qwen2.5-coder-0.5b-instruct-q4_k_m.gguf',
|
|
70
|
+
sizeBytes: 350_000_000,
|
|
71
|
+
sizeHuman: '350MB',
|
|
72
|
+
},
|
|
47
73
|
};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -2748,7 +2748,7 @@ export declare const ReportSchema: z.ZodObject<{
|
|
|
2748
2748
|
}>>;
|
|
2749
2749
|
deep: z.ZodOptional<z.ZodObject<{
|
|
2750
2750
|
enabled: z.ZodBoolean;
|
|
2751
|
-
tier: z.ZodOptional<z.ZodEnum<["deep", "
|
|
2751
|
+
tier: z.ZodOptional<z.ZodEnum<["deep", "lite", "legacy", "cloud"]>>;
|
|
2752
2752
|
model: z.ZodOptional<z.ZodString>;
|
|
2753
2753
|
total_ms: z.ZodOptional<z.ZodNumber>;
|
|
2754
2754
|
files_analyzed: z.ZodOptional<z.ZodNumber>;
|
|
@@ -2756,7 +2756,7 @@ export declare const ReportSchema: z.ZodObject<{
|
|
|
2756
2756
|
findings_verified: z.ZodOptional<z.ZodNumber>;
|
|
2757
2757
|
}, "strip", z.ZodTypeAny, {
|
|
2758
2758
|
enabled: boolean;
|
|
2759
|
-
tier?: "deep" | "
|
|
2759
|
+
tier?: "deep" | "lite" | "legacy" | "cloud" | undefined;
|
|
2760
2760
|
model?: string | undefined;
|
|
2761
2761
|
total_ms?: number | undefined;
|
|
2762
2762
|
files_analyzed?: number | undefined;
|
|
@@ -2764,7 +2764,7 @@ export declare const ReportSchema: z.ZodObject<{
|
|
|
2764
2764
|
findings_verified?: number | undefined;
|
|
2765
2765
|
}, {
|
|
2766
2766
|
enabled: boolean;
|
|
2767
|
-
tier?: "deep" | "
|
|
2767
|
+
tier?: "deep" | "lite" | "legacy" | "cloud" | undefined;
|
|
2768
2768
|
model?: string | undefined;
|
|
2769
2769
|
total_ms?: number | undefined;
|
|
2770
2770
|
files_analyzed?: number | undefined;
|
|
@@ -2775,7 +2775,7 @@ export declare const ReportSchema: z.ZodObject<{
|
|
|
2775
2775
|
duration_ms: number;
|
|
2776
2776
|
deep?: {
|
|
2777
2777
|
enabled: boolean;
|
|
2778
|
-
tier?: "deep" | "
|
|
2778
|
+
tier?: "deep" | "lite" | "legacy" | "cloud" | undefined;
|
|
2779
2779
|
model?: string | undefined;
|
|
2780
2780
|
total_ms?: number | undefined;
|
|
2781
2781
|
files_analyzed?: number | undefined;
|
|
@@ -2798,7 +2798,7 @@ export declare const ReportSchema: z.ZodObject<{
|
|
|
2798
2798
|
duration_ms: number;
|
|
2799
2799
|
deep?: {
|
|
2800
2800
|
enabled: boolean;
|
|
2801
|
-
tier?: "deep" | "
|
|
2801
|
+
tier?: "deep" | "lite" | "legacy" | "cloud" | undefined;
|
|
2802
2802
|
model?: string | undefined;
|
|
2803
2803
|
total_ms?: number | undefined;
|
|
2804
2804
|
files_analyzed?: number | undefined;
|
|
@@ -2823,7 +2823,7 @@ export declare const ReportSchema: z.ZodObject<{
|
|
|
2823
2823
|
duration_ms: number;
|
|
2824
2824
|
deep?: {
|
|
2825
2825
|
enabled: boolean;
|
|
2826
|
-
tier?: "deep" | "
|
|
2826
|
+
tier?: "deep" | "lite" | "legacy" | "cloud" | undefined;
|
|
2827
2827
|
model?: string | undefined;
|
|
2828
2828
|
total_ms?: number | undefined;
|
|
2829
2829
|
files_analyzed?: number | undefined;
|
|
@@ -2865,7 +2865,7 @@ export declare const ReportSchema: z.ZodObject<{
|
|
|
2865
2865
|
duration_ms: number;
|
|
2866
2866
|
deep?: {
|
|
2867
2867
|
enabled: boolean;
|
|
2868
|
-
tier?: "deep" | "
|
|
2868
|
+
tier?: "deep" | "lite" | "legacy" | "cloud" | undefined;
|
|
2869
2869
|
model?: string | undefined;
|
|
2870
2870
|
total_ms?: number | undefined;
|
|
2871
2871
|
files_analyzed?: number | undefined;
|
package/dist/types/index.js
CHANGED
|
@@ -383,7 +383,7 @@ export const ReportSchema = z.object({
|
|
|
383
383
|
}).optional(),
|
|
384
384
|
deep: z.object({
|
|
385
385
|
enabled: z.boolean(),
|
|
386
|
-
tier: z.enum(['deep', '
|
|
386
|
+
tier: z.enum(['deep', 'lite', 'legacy', 'cloud']).optional(),
|
|
387
387
|
model: z.string().optional(),
|
|
388
388
|
total_ms: z.number().optional(),
|
|
389
389
|
files_analyzed: z.number().optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/core",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.4",
|
|
4
4
|
"description": "Deterministic quality gate engine for AI-generated code. AST analysis, drift detection, and Fix Packet generation across TypeScript, JavaScript, Python, Go, Ruby, and C#.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://rigour.run",
|
|
@@ -59,11 +59,11 @@
|
|
|
59
59
|
"@xenova/transformers": "^2.17.2",
|
|
60
60
|
"better-sqlite3": "^11.0.0",
|
|
61
61
|
"openai": "^4.104.0",
|
|
62
|
-
"@rigour-labs/brain-darwin-arm64": "4.3.
|
|
63
|
-
"@rigour-labs/brain-
|
|
64
|
-
"@rigour-labs/brain-
|
|
65
|
-
"@rigour-labs/brain-
|
|
66
|
-
"@rigour-labs/brain-
|
|
62
|
+
"@rigour-labs/brain-darwin-arm64": "4.3.4",
|
|
63
|
+
"@rigour-labs/brain-darwin-x64": "4.3.4",
|
|
64
|
+
"@rigour-labs/brain-linux-x64": "4.3.4",
|
|
65
|
+
"@rigour-labs/brain-linux-arm64": "4.3.4",
|
|
66
|
+
"@rigour-labs/brain-win-x64": "4.3.4"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@types/better-sqlite3": "^7.6.12",
|