@rigour-labs/core 3.0.5 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/deep/fact-extractor.d.ts +80 -0
- package/dist/deep/fact-extractor.js +626 -0
- package/dist/deep/index.d.ts +14 -0
- package/dist/deep/index.js +12 -0
- package/dist/deep/prompts.d.ts +22 -0
- package/dist/deep/prompts.js +374 -0
- package/dist/deep/verifier.d.ts +16 -0
- package/dist/deep/verifier.js +388 -0
- package/dist/gates/deep-analysis.d.ts +28 -0
- package/dist/gates/deep-analysis.js +302 -0
- package/dist/gates/deprecated-apis-rules-lang.d.ts +21 -0
- package/dist/gates/deprecated-apis-rules-lang.js +311 -0
- package/dist/gates/deprecated-apis-rules-node.d.ts +19 -0
- package/dist/gates/deprecated-apis-rules-node.js +199 -0
- package/dist/gates/deprecated-apis-rules.d.ts +6 -0
- package/dist/gates/deprecated-apis-rules.js +6 -0
- package/dist/gates/deprecated-apis.js +1 -502
- package/dist/gates/hallucinated-imports-lang.d.ts +16 -0
- package/dist/gates/hallucinated-imports-lang.js +374 -0
- package/dist/gates/hallucinated-imports-stdlib.d.ts +12 -0
- package/dist/gates/hallucinated-imports-stdlib.js +228 -0
- package/dist/gates/hallucinated-imports.d.ts +0 -98
- package/dist/gates/hallucinated-imports.js +10 -678
- package/dist/gates/phantom-apis-data.d.ts +33 -0
- package/dist/gates/phantom-apis-data.js +398 -0
- package/dist/gates/phantom-apis.js +1 -393
- package/dist/gates/phantom-apis.test.js +52 -0
- package/dist/gates/promise-safety-helpers.d.ts +19 -0
- package/dist/gates/promise-safety-helpers.js +101 -0
- package/dist/gates/promise-safety-rules.d.ts +7 -0
- package/dist/gates/promise-safety-rules.js +19 -0
- package/dist/gates/promise-safety.d.ts +1 -21
- package/dist/gates/promise-safety.js +51 -257
- package/dist/gates/runner.d.ts +4 -2
- package/dist/gates/runner.js +46 -1
- package/dist/gates/test-quality-lang.d.ts +30 -0
- package/dist/gates/test-quality-lang.js +188 -0
- package/dist/gates/test-quality.d.ts +0 -14
- package/dist/gates/test-quality.js +13 -186
- package/dist/index.d.ts +10 -0
- package/dist/index.js +12 -2
- package/dist/inference/cloud-provider.d.ts +34 -0
- package/dist/inference/cloud-provider.js +126 -0
- package/dist/inference/index.d.ts +17 -0
- package/dist/inference/index.js +23 -0
- package/dist/inference/model-manager.d.ts +26 -0
- package/dist/inference/model-manager.js +106 -0
- package/dist/inference/sidecar-provider.d.ts +15 -0
- package/dist/inference/sidecar-provider.js +153 -0
- package/dist/inference/types.d.ts +77 -0
- package/dist/inference/types.js +19 -0
- package/dist/pattern-index/indexer-helpers.d.ts +38 -0
- package/dist/pattern-index/indexer-helpers.js +111 -0
- package/dist/pattern-index/indexer-lang.d.ts +13 -0
- package/dist/pattern-index/indexer-lang.js +244 -0
- package/dist/pattern-index/indexer-ts.d.ts +22 -0
- package/dist/pattern-index/indexer-ts.js +258 -0
- package/dist/pattern-index/indexer.d.ts +4 -106
- package/dist/pattern-index/indexer.js +58 -707
- package/dist/pattern-index/staleness-data.d.ts +6 -0
- package/dist/pattern-index/staleness-data.js +262 -0
- package/dist/pattern-index/staleness.js +1 -258
- package/dist/settings.d.ts +104 -0
- package/dist/settings.js +186 -0
- package/dist/storage/db.d.ts +16 -0
- package/dist/storage/db.js +132 -0
- package/dist/storage/findings.d.ts +14 -0
- package/dist/storage/findings.js +38 -0
- package/dist/storage/index.d.ts +9 -0
- package/dist/storage/index.js +8 -0
- package/dist/storage/patterns.d.ts +35 -0
- package/dist/storage/patterns.js +62 -0
- package/dist/storage/scans.d.ts +42 -0
- package/dist/storage/scans.js +55 -0
- package/dist/templates/index.d.ts +12 -16
- package/dist/templates/index.js +11 -527
- package/dist/templates/paradigms.d.ts +2 -0
- package/dist/templates/paradigms.js +46 -0
- package/dist/templates/presets.d.ts +14 -0
- package/dist/templates/presets.js +227 -0
- package/dist/templates/universal-config.d.ts +2 -0
- package/dist/templates/universal-config.js +190 -0
- package/dist/types/index.d.ts +438 -15
- package/dist/types/index.js +41 -1
- package/package.json +6 -2
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language-specific test quality checks for Go and Java/Kotlin.
|
|
3
|
+
* Extracted from test-quality.ts to keep it under 500 lines.
|
|
4
|
+
*/
|
|
5
|
+
export interface TestQualityIssue {
|
|
6
|
+
file: string;
|
|
7
|
+
line: number;
|
|
8
|
+
pattern: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
}
|
|
11
|
+
export interface TestQualityConfig {
|
|
12
|
+
check_empty_tests?: boolean;
|
|
13
|
+
check_tautological?: boolean;
|
|
14
|
+
check_mock_heavy?: boolean;
|
|
15
|
+
max_mocks_per_test?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Go test quality checks.
|
|
19
|
+
* Go tests use func TestXxx(t *testing.T) pattern.
|
|
20
|
+
* Assertions via t.Fatal, t.Error, t.Fatalf, t.Errorf, t.Fail, t.FailNow.
|
|
21
|
+
* Also checks for t.Run subtests and table-driven patterns.
|
|
22
|
+
*/
|
|
23
|
+
export declare function checkGoTestQuality(content: string, file: string, issues: TestQualityIssue[], config: TestQualityConfig): void;
|
|
24
|
+
/**
|
|
25
|
+
* Java/Kotlin test quality checks.
|
|
26
|
+
* JUnit 4: @Test + Assert.assertEquals, assertTrue, etc.
|
|
27
|
+
* JUnit 5: @Test + Assertions.assertEquals, assertThrows, etc.
|
|
28
|
+
* Kotlin: @Test + kotlin.test assertEquals, etc.
|
|
29
|
+
*/
|
|
30
|
+
export declare function checkJavaKotlinTestQuality(content: string, file: string, ext: string, issues: TestQualityIssue[], config: TestQualityConfig): void;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language-specific test quality checks for Go and Java/Kotlin.
|
|
3
|
+
* Extracted from test-quality.ts to keep it under 500 lines.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Go test quality checks.
|
|
7
|
+
* Go tests use func TestXxx(t *testing.T) pattern.
|
|
8
|
+
* Assertions via t.Fatal, t.Error, t.Fatalf, t.Errorf, t.Fail, t.FailNow.
|
|
9
|
+
* Also checks for t.Run subtests and table-driven patterns.
|
|
10
|
+
*/
|
|
11
|
+
export function checkGoTestQuality(content, file, issues, config) {
|
|
12
|
+
const lines = content.split('\n');
|
|
13
|
+
let inTestFunc = false;
|
|
14
|
+
let testStartLine = 0;
|
|
15
|
+
let braceDepth = 0;
|
|
16
|
+
let hasAssertion = false;
|
|
17
|
+
let testContent = '';
|
|
18
|
+
for (let i = 0; i < lines.length; i++) {
|
|
19
|
+
const line = lines[i];
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
// Detect test function: func TestXxx(t *testing.T) {
|
|
22
|
+
const testMatch = trimmed.match(/^func\s+(Test\w+|Benchmark\w+)\s*\(/);
|
|
23
|
+
if (testMatch && !inTestFunc) {
|
|
24
|
+
inTestFunc = true;
|
|
25
|
+
testStartLine = i + 1;
|
|
26
|
+
braceDepth = 0;
|
|
27
|
+
hasAssertion = false;
|
|
28
|
+
testContent = '';
|
|
29
|
+
// Count braces on this line
|
|
30
|
+
for (const ch of line) {
|
|
31
|
+
if (ch === '{')
|
|
32
|
+
braceDepth++;
|
|
33
|
+
if (ch === '}')
|
|
34
|
+
braceDepth--;
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (inTestFunc) {
|
|
39
|
+
testContent += line + '\n';
|
|
40
|
+
for (const ch of line) {
|
|
41
|
+
if (ch === '{')
|
|
42
|
+
braceDepth++;
|
|
43
|
+
if (ch === '}')
|
|
44
|
+
braceDepth--;
|
|
45
|
+
}
|
|
46
|
+
// Go test assertions: t.Fatal, t.Error, t.Fatalf, t.Errorf, t.Fail, t.FailNow
|
|
47
|
+
// Also: assert/require from testify, t.Run for subtests
|
|
48
|
+
if (/\bt\.\s*(?:Fatal|Error|Fatalf|Errorf|Fail|FailNow|Log|Logf|Skip|Skipf|Helper)\s*\(/.test(line) ||
|
|
49
|
+
/\bt\.Run\s*\(/.test(line) ||
|
|
50
|
+
/\bassert\.\w+\s*\(/.test(line) || /\brequire\.\w+\s*\(/.test(line) ||
|
|
51
|
+
/\bif\b.*\bt\./.test(line)) {
|
|
52
|
+
hasAssertion = true;
|
|
53
|
+
}
|
|
54
|
+
// Tautological: if true { t.Fatal... } or assert.True(t, true)
|
|
55
|
+
if (config.check_tautological) {
|
|
56
|
+
if (/assert\.True\s*\(\s*\w+\s*,\s*true\s*\)/.test(line) ||
|
|
57
|
+
/assert\.Equal\s*\(\s*\w+\s*,\s*(\d+|"[^"]*")\s*,\s*\1\s*\)/.test(line)) {
|
|
58
|
+
issues.push({
|
|
59
|
+
file, line: i + 1, pattern: 'tautological-assertion',
|
|
60
|
+
reason: 'Tautological assertion — comparing a constant to itself proves nothing',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// End of function
|
|
65
|
+
if (braceDepth === 0 && testContent.trim()) {
|
|
66
|
+
const meaningful = testContent.split('\n').filter(l => {
|
|
67
|
+
const t = l.trim();
|
|
68
|
+
return t && t !== '{' && t !== '}' && !t.startsWith('//');
|
|
69
|
+
});
|
|
70
|
+
if (config.check_empty_tests && meaningful.length === 0) {
|
|
71
|
+
issues.push({
|
|
72
|
+
file, line: testStartLine, pattern: 'empty-test',
|
|
73
|
+
reason: 'Empty test function — no test logic',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else if (config.check_empty_tests && !hasAssertion && meaningful.length > 0) {
|
|
77
|
+
issues.push({
|
|
78
|
+
file, line: testStartLine, pattern: 'no-assertion',
|
|
79
|
+
reason: 'Test has no assertions (t.Error, t.Fatal, assert.*) — executes code but never verifies',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
inTestFunc = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Java/Kotlin test quality checks.
|
|
89
|
+
* JUnit 4: @Test + Assert.assertEquals, assertTrue, etc.
|
|
90
|
+
* JUnit 5: @Test + Assertions.assertEquals, assertThrows, etc.
|
|
91
|
+
* Kotlin: @Test + kotlin.test assertEquals, etc.
|
|
92
|
+
*/
|
|
93
|
+
export function checkJavaKotlinTestQuality(content, file, ext, issues, config) {
|
|
94
|
+
const lines = content.split('\n');
|
|
95
|
+
const isKotlin = ext === '.kt';
|
|
96
|
+
let inTestMethod = false;
|
|
97
|
+
let testStartLine = 0;
|
|
98
|
+
let braceDepth = 0;
|
|
99
|
+
let hasAssertion = false;
|
|
100
|
+
let mockCount = 0;
|
|
101
|
+
for (let i = 0; i < lines.length; i++) {
|
|
102
|
+
const line = lines[i];
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
// Detect @Test annotation (next non-empty line is the method)
|
|
105
|
+
if (trimmed === '@Test' || /^@Test\s*(\(|$)/.test(trimmed)) {
|
|
106
|
+
// Look for the method signature on this or next lines
|
|
107
|
+
for (let j = i + 1; j < Math.min(i + 3, lines.length); j++) {
|
|
108
|
+
const methodLine = lines[j].trim();
|
|
109
|
+
const methodMatch = isKotlin
|
|
110
|
+
? methodLine.match(/^(?:fun|suspend\s+fun)\s+(\w+)\s*\(/)
|
|
111
|
+
: methodLine.match(/^(?:public\s+|private\s+|protected\s+)?(?:static\s+)?void\s+(\w+)\s*\(/);
|
|
112
|
+
if (methodMatch) {
|
|
113
|
+
inTestMethod = true;
|
|
114
|
+
testStartLine = j + 1;
|
|
115
|
+
braceDepth = 0;
|
|
116
|
+
hasAssertion = false;
|
|
117
|
+
mockCount = 0;
|
|
118
|
+
// Count braces
|
|
119
|
+
for (const ch of lines[j]) {
|
|
120
|
+
if (ch === '{')
|
|
121
|
+
braceDepth++;
|
|
122
|
+
if (ch === '}')
|
|
123
|
+
braceDepth--;
|
|
124
|
+
}
|
|
125
|
+
i = j;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (inTestMethod) {
|
|
132
|
+
for (const ch of line) {
|
|
133
|
+
if (ch === '{')
|
|
134
|
+
braceDepth++;
|
|
135
|
+
if (ch === '}')
|
|
136
|
+
braceDepth--;
|
|
137
|
+
}
|
|
138
|
+
// JUnit 4/5 assertions
|
|
139
|
+
if (/\b(?:assert(?:Equals|True|False|NotNull|Null|That|Throws|DoesNotThrow|Same|NotSame|ArrayEquals)|assertEquals|assertTrue|assertFalse|assertNotNull|assertNull|assertThrows)\s*\(/.test(line)) {
|
|
140
|
+
hasAssertion = true;
|
|
141
|
+
}
|
|
142
|
+
// Kotlin test assertions
|
|
143
|
+
if (isKotlin && /\b(?:assertEquals|assertTrue|assertFalse|assertNotNull|assertNull|assertFailsWith|assertIs|assertContains|expect)\s*[({]/.test(line)) {
|
|
144
|
+
hasAssertion = true;
|
|
145
|
+
}
|
|
146
|
+
// Hamcrest / AssertJ
|
|
147
|
+
if (/\bassertThat\s*\(/.test(line) || /\.should\w*\(/.test(line)) {
|
|
148
|
+
hasAssertion = true;
|
|
149
|
+
}
|
|
150
|
+
// Verify (Mockito)
|
|
151
|
+
if (/\bverify\s*\(/.test(line)) {
|
|
152
|
+
hasAssertion = true;
|
|
153
|
+
}
|
|
154
|
+
// Mock counting
|
|
155
|
+
if (/\b(?:mock|spy|when|doReturn|doThrow|doNothing|Mockito\.\w+)\s*\(/.test(line) ||
|
|
156
|
+
/@Mock\b/.test(line) || /@InjectMocks\b/.test(line)) {
|
|
157
|
+
mockCount++;
|
|
158
|
+
}
|
|
159
|
+
// Tautological
|
|
160
|
+
if (config.check_tautological) {
|
|
161
|
+
if (/assertEquals\s*\(\s*true\s*,\s*true\s*\)/.test(line) ||
|
|
162
|
+
/assertTrue\s*\(\s*true\s*\)/.test(line) ||
|
|
163
|
+
/assertEquals\s*\(\s*(\d+)\s*,\s*\1\s*\)/.test(line)) {
|
|
164
|
+
issues.push({
|
|
165
|
+
file, line: i + 1, pattern: 'tautological-assertion',
|
|
166
|
+
reason: 'Tautological assertion — comparing a constant to itself proves nothing',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// End of method
|
|
171
|
+
if (braceDepth === 0) {
|
|
172
|
+
if (config.check_empty_tests && !hasAssertion) {
|
|
173
|
+
issues.push({
|
|
174
|
+
file, line: testStartLine, pattern: 'no-assertion',
|
|
175
|
+
reason: 'Test has no assertions — executes code but never verifies results',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (config.check_mock_heavy && mockCount > config.max_mocks_per_test) {
|
|
179
|
+
issues.push({
|
|
180
|
+
file, line: testStartLine, pattern: 'mock-heavy',
|
|
181
|
+
reason: `Test uses ${mockCount} mocks (max: ${config.max_mocks_per_test}) — may be testing mocks, not real behavior`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
inTestMethod = false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -50,18 +50,4 @@ export declare class TestQualityGate extends Gate {
|
|
|
50
50
|
private analyzeJSTestBlock;
|
|
51
51
|
private checkPythonTestQuality;
|
|
52
52
|
private analyzePythonTestBlock;
|
|
53
|
-
/**
|
|
54
|
-
* Go test quality checks.
|
|
55
|
-
* Go tests use func TestXxx(t *testing.T) pattern.
|
|
56
|
-
* Assertions via t.Fatal, t.Error, t.Fatalf, t.Errorf, t.Fail, t.FailNow.
|
|
57
|
-
* Also checks for t.Run subtests and table-driven patterns.
|
|
58
|
-
*/
|
|
59
|
-
private checkGoTestQuality;
|
|
60
|
-
/**
|
|
61
|
-
* Java/Kotlin test quality checks.
|
|
62
|
-
* JUnit 4: @Test + Assert.assertEquals, assertTrue, etc.
|
|
63
|
-
* JUnit 5: @Test + Assertions.assertEquals, assertThrows, etc.
|
|
64
|
-
* Kotlin: @Test + kotlin.test assertEquals, etc.
|
|
65
|
-
*/
|
|
66
|
-
private checkJavaKotlinTestQuality;
|
|
67
53
|
}
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { Gate } from './base.js';
|
|
27
27
|
import { FileScanner } from '../utils/scanner.js';
|
|
28
28
|
import { Logger } from '../utils/logger.js';
|
|
29
|
+
import { checkGoTestQuality, checkJavaKotlinTestQuality } from './test-quality-lang.js';
|
|
29
30
|
import fs from 'fs-extra';
|
|
30
31
|
import path from 'path';
|
|
31
32
|
export class TestQualityGate extends Gate {
|
|
@@ -76,10 +77,20 @@ export class TestQualityGate extends Gate {
|
|
|
76
77
|
this.checkPythonTestQuality(content, file, issues);
|
|
77
78
|
}
|
|
78
79
|
else if (ext === '.go') {
|
|
79
|
-
|
|
80
|
+
checkGoTestQuality(content, file, issues, {
|
|
81
|
+
check_empty_tests: this.config.check_empty_tests,
|
|
82
|
+
check_tautological: this.config.check_tautological,
|
|
83
|
+
check_mock_heavy: this.config.check_mock_heavy,
|
|
84
|
+
max_mocks_per_test: this.config.max_mocks_per_test,
|
|
85
|
+
});
|
|
80
86
|
}
|
|
81
87
|
else if (ext === '.java' || ext === '.kt') {
|
|
82
|
-
|
|
88
|
+
checkJavaKotlinTestQuality(content, file, ext, issues, {
|
|
89
|
+
check_empty_tests: this.config.check_empty_tests,
|
|
90
|
+
check_tautological: this.config.check_tautological,
|
|
91
|
+
check_mock_heavy: this.config.check_mock_heavy,
|
|
92
|
+
max_mocks_per_test: this.config.max_mocks_per_test,
|
|
93
|
+
});
|
|
83
94
|
}
|
|
84
95
|
}
|
|
85
96
|
catch { /* skip */ }
|
|
@@ -325,188 +336,4 @@ export class TestQualityGate extends Gate {
|
|
|
325
336
|
});
|
|
326
337
|
}
|
|
327
338
|
}
|
|
328
|
-
/**
|
|
329
|
-
* Go test quality checks.
|
|
330
|
-
* Go tests use func TestXxx(t *testing.T) pattern.
|
|
331
|
-
* Assertions via t.Fatal, t.Error, t.Fatalf, t.Errorf, t.Fail, t.FailNow.
|
|
332
|
-
* Also checks for t.Run subtests and table-driven patterns.
|
|
333
|
-
*/
|
|
334
|
-
checkGoTestQuality(content, file, issues) {
|
|
335
|
-
const lines = content.split('\n');
|
|
336
|
-
let inTestFunc = false;
|
|
337
|
-
let testStartLine = 0;
|
|
338
|
-
let braceDepth = 0;
|
|
339
|
-
let hasAssertion = false;
|
|
340
|
-
let testContent = '';
|
|
341
|
-
for (let i = 0; i < lines.length; i++) {
|
|
342
|
-
const line = lines[i];
|
|
343
|
-
const trimmed = line.trim();
|
|
344
|
-
// Detect test function: func TestXxx(t *testing.T) {
|
|
345
|
-
const testMatch = trimmed.match(/^func\s+(Test\w+|Benchmark\w+)\s*\(/);
|
|
346
|
-
if (testMatch && !inTestFunc) {
|
|
347
|
-
inTestFunc = true;
|
|
348
|
-
testStartLine = i + 1;
|
|
349
|
-
braceDepth = 0;
|
|
350
|
-
hasAssertion = false;
|
|
351
|
-
testContent = '';
|
|
352
|
-
// Count braces on this line
|
|
353
|
-
for (const ch of line) {
|
|
354
|
-
if (ch === '{')
|
|
355
|
-
braceDepth++;
|
|
356
|
-
if (ch === '}')
|
|
357
|
-
braceDepth--;
|
|
358
|
-
}
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
if (inTestFunc) {
|
|
362
|
-
testContent += line + '\n';
|
|
363
|
-
for (const ch of line) {
|
|
364
|
-
if (ch === '{')
|
|
365
|
-
braceDepth++;
|
|
366
|
-
if (ch === '}')
|
|
367
|
-
braceDepth--;
|
|
368
|
-
}
|
|
369
|
-
// Go test assertions: t.Fatal, t.Error, t.Fatalf, t.Errorf, t.Fail, t.FailNow
|
|
370
|
-
// Also: assert/require from testify, t.Run for subtests
|
|
371
|
-
if (/\bt\.\s*(?:Fatal|Error|Fatalf|Errorf|Fail|FailNow|Log|Logf|Skip|Skipf|Helper)\s*\(/.test(line) ||
|
|
372
|
-
/\bt\.Run\s*\(/.test(line) ||
|
|
373
|
-
/\bassert\.\w+\s*\(/.test(line) || /\brequire\.\w+\s*\(/.test(line) ||
|
|
374
|
-
/\bif\b.*\bt\./.test(line)) {
|
|
375
|
-
hasAssertion = true;
|
|
376
|
-
}
|
|
377
|
-
// Tautological: if true { t.Fatal... } or assert.True(t, true)
|
|
378
|
-
if (this.config.check_tautological) {
|
|
379
|
-
if (/assert\.True\s*\(\s*\w+\s*,\s*true\s*\)/.test(line) ||
|
|
380
|
-
/assert\.Equal\s*\(\s*\w+\s*,\s*(\d+|"[^"]*")\s*,\s*\1\s*\)/.test(line)) {
|
|
381
|
-
issues.push({
|
|
382
|
-
file, line: i + 1, pattern: 'tautological-assertion',
|
|
383
|
-
reason: 'Tautological assertion — comparing a constant to itself proves nothing',
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
// End of function
|
|
388
|
-
if (braceDepth === 0 && testContent.trim()) {
|
|
389
|
-
const meaningful = testContent.split('\n').filter(l => {
|
|
390
|
-
const t = l.trim();
|
|
391
|
-
return t && t !== '{' && t !== '}' && !t.startsWith('//');
|
|
392
|
-
});
|
|
393
|
-
if (this.config.check_empty_tests && meaningful.length === 0) {
|
|
394
|
-
issues.push({
|
|
395
|
-
file, line: testStartLine, pattern: 'empty-test',
|
|
396
|
-
reason: 'Empty test function — no test logic',
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
else if (this.config.check_empty_tests && !hasAssertion && meaningful.length > 0) {
|
|
400
|
-
issues.push({
|
|
401
|
-
file, line: testStartLine, pattern: 'no-assertion',
|
|
402
|
-
reason: 'Test has no assertions (t.Error, t.Fatal, assert.*) — executes code but never verifies',
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
inTestFunc = false;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Java/Kotlin test quality checks.
|
|
412
|
-
* JUnit 4: @Test + Assert.assertEquals, assertTrue, etc.
|
|
413
|
-
* JUnit 5: @Test + Assertions.assertEquals, assertThrows, etc.
|
|
414
|
-
* Kotlin: @Test + kotlin.test assertEquals, etc.
|
|
415
|
-
*/
|
|
416
|
-
checkJavaKotlinTestQuality(content, file, ext, issues) {
|
|
417
|
-
const lines = content.split('\n');
|
|
418
|
-
const isKotlin = ext === '.kt';
|
|
419
|
-
let inTestMethod = false;
|
|
420
|
-
let testStartLine = 0;
|
|
421
|
-
let braceDepth = 0;
|
|
422
|
-
let hasAssertion = false;
|
|
423
|
-
let mockCount = 0;
|
|
424
|
-
for (let i = 0; i < lines.length; i++) {
|
|
425
|
-
const line = lines[i];
|
|
426
|
-
const trimmed = line.trim();
|
|
427
|
-
// Detect @Test annotation (next non-empty line is the method)
|
|
428
|
-
if (trimmed === '@Test' || /^@Test\s*(\(|$)/.test(trimmed)) {
|
|
429
|
-
// Look for the method signature on this or next lines
|
|
430
|
-
for (let j = i + 1; j < Math.min(i + 3, lines.length); j++) {
|
|
431
|
-
const methodLine = lines[j].trim();
|
|
432
|
-
const methodMatch = isKotlin
|
|
433
|
-
? methodLine.match(/^(?:fun|suspend\s+fun)\s+(\w+)\s*\(/)
|
|
434
|
-
: methodLine.match(/^(?:public\s+|private\s+|protected\s+)?(?:static\s+)?void\s+(\w+)\s*\(/);
|
|
435
|
-
if (methodMatch) {
|
|
436
|
-
inTestMethod = true;
|
|
437
|
-
testStartLine = j + 1;
|
|
438
|
-
braceDepth = 0;
|
|
439
|
-
hasAssertion = false;
|
|
440
|
-
mockCount = 0;
|
|
441
|
-
// Count braces
|
|
442
|
-
for (const ch of lines[j]) {
|
|
443
|
-
if (ch === '{')
|
|
444
|
-
braceDepth++;
|
|
445
|
-
if (ch === '}')
|
|
446
|
-
braceDepth--;
|
|
447
|
-
}
|
|
448
|
-
i = j;
|
|
449
|
-
break;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
if (inTestMethod) {
|
|
455
|
-
for (const ch of line) {
|
|
456
|
-
if (ch === '{')
|
|
457
|
-
braceDepth++;
|
|
458
|
-
if (ch === '}')
|
|
459
|
-
braceDepth--;
|
|
460
|
-
}
|
|
461
|
-
// JUnit 4/5 assertions
|
|
462
|
-
if (/\b(?:assert(?:Equals|True|False|NotNull|Null|That|Throws|DoesNotThrow|Same|NotSame|ArrayEquals)|assertEquals|assertTrue|assertFalse|assertNotNull|assertNull|assertThrows)\s*\(/.test(line)) {
|
|
463
|
-
hasAssertion = true;
|
|
464
|
-
}
|
|
465
|
-
// Kotlin test assertions
|
|
466
|
-
if (isKotlin && /\b(?:assertEquals|assertTrue|assertFalse|assertNotNull|assertNull|assertFailsWith|assertIs|assertContains|expect)\s*[({]/.test(line)) {
|
|
467
|
-
hasAssertion = true;
|
|
468
|
-
}
|
|
469
|
-
// Hamcrest / AssertJ
|
|
470
|
-
if (/\bassertThat\s*\(/.test(line) || /\.should\w*\(/.test(line)) {
|
|
471
|
-
hasAssertion = true;
|
|
472
|
-
}
|
|
473
|
-
// Verify (Mockito)
|
|
474
|
-
if (/\bverify\s*\(/.test(line)) {
|
|
475
|
-
hasAssertion = true;
|
|
476
|
-
}
|
|
477
|
-
// Mock counting
|
|
478
|
-
if (/\b(?:mock|spy|when|doReturn|doThrow|doNothing|Mockito\.\w+)\s*\(/.test(line) ||
|
|
479
|
-
/@Mock\b/.test(line) || /@InjectMocks\b/.test(line)) {
|
|
480
|
-
mockCount++;
|
|
481
|
-
}
|
|
482
|
-
// Tautological
|
|
483
|
-
if (this.config.check_tautological) {
|
|
484
|
-
if (/assertEquals\s*\(\s*true\s*,\s*true\s*\)/.test(line) ||
|
|
485
|
-
/assertTrue\s*\(\s*true\s*\)/.test(line) ||
|
|
486
|
-
/assertEquals\s*\(\s*(\d+)\s*,\s*\1\s*\)/.test(line)) {
|
|
487
|
-
issues.push({
|
|
488
|
-
file, line: i + 1, pattern: 'tautological-assertion',
|
|
489
|
-
reason: 'Tautological assertion — comparing a constant to itself proves nothing',
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
// End of method
|
|
494
|
-
if (braceDepth === 0) {
|
|
495
|
-
if (this.config.check_empty_tests && !hasAssertion) {
|
|
496
|
-
issues.push({
|
|
497
|
-
file, line: testStartLine, pattern: 'no-assertion',
|
|
498
|
-
reason: 'Test has no assertions — executes code but never verifies results',
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
if (this.config.check_mock_heavy && mockCount > this.config.max_mocks_per_test) {
|
|
502
|
-
issues.push({
|
|
503
|
-
file, line: testStartLine, pattern: 'mock-heavy',
|
|
504
|
-
reason: `Test uses ${mockCount} mocks (max: ${this.config.max_mocks_per_test}) — may be testing mocks, not real behavior`,
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
inTestMethod = false;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
339
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,3 +9,13 @@ export { RetryLoopBreakerGate } from './gates/retry-loop-breaker.js';
|
|
|
9
9
|
export * from './utils/logger.js';
|
|
10
10
|
export * from './services/score-history.js';
|
|
11
11
|
export * from './hooks/index.js';
|
|
12
|
+
export { loadSettings, saveSettings, getSettingsPath, resolveDeepOptions, getProviderKey, getAgentConfig, getCliPreferences, updateProviderKey, removeProviderKey } from './settings.js';
|
|
13
|
+
export type { RigourSettings, ResolvedDeepOptions, CLIDeepOptions } from './settings.js';
|
|
14
|
+
export { DeepAnalysisGate } from './gates/deep-analysis.js';
|
|
15
|
+
export { createProvider } from './inference/index.js';
|
|
16
|
+
export type { InferenceProvider, DeepFinding, DeepAnalysisResult, ModelTier } from './inference/types.js';
|
|
17
|
+
export { MODELS } from './inference/types.js';
|
|
18
|
+
export { isModelCached, getModelsDir, getModelInfo } from './inference/model-manager.js';
|
|
19
|
+
export { extractFacts, factsToPromptString } from './deep/fact-extractor.js';
|
|
20
|
+
export { openDatabase, isSQLiteAvailable, insertScan, insertFindings, getRecentScans, getScoreTrendFromDB, getTopIssues, reinforcePattern, getStrongPatterns } from './storage/index.js';
|
|
21
|
+
export type { RigourDB } from './storage/index.js';
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,17 @@ export { RetryLoopBreakerGate } from './gates/retry-loop-breaker.js';
|
|
|
9
9
|
export * from './utils/logger.js';
|
|
10
10
|
export * from './services/score-history.js';
|
|
11
11
|
export * from './hooks/index.js';
|
|
12
|
+
// Settings Module (Global user config at ~/.rigour/settings.json)
|
|
13
|
+
export { loadSettings, saveSettings, getSettingsPath, resolveDeepOptions, getProviderKey, getAgentConfig, getCliPreferences, updateProviderKey, removeProviderKey } from './settings.js';
|
|
14
|
+
// Deep Analysis Pipeline (v4.0+)
|
|
15
|
+
export { DeepAnalysisGate } from './gates/deep-analysis.js';
|
|
16
|
+
export { createProvider } from './inference/index.js';
|
|
17
|
+
export { MODELS } from './inference/types.js';
|
|
18
|
+
export { isModelCached, getModelsDir, getModelInfo } from './inference/model-manager.js';
|
|
19
|
+
export { extractFacts, factsToPromptString } from './deep/fact-extractor.js';
|
|
20
|
+
// Storage (SQLite Brain)
|
|
21
|
+
export { openDatabase, isSQLiteAvailable, insertScan, insertFindings, getRecentScans, getScoreTrendFromDB, getTopIssues, reinforcePattern, getStrongPatterns } from './storage/index.js';
|
|
12
22
|
// Pattern Index is intentionally NOT exported here to prevent
|
|
13
|
-
// native dependency issues (sharp/transformers) from leaking into
|
|
14
|
-
// non-AI parts of the system.
|
|
23
|
+
// native dependency issues (sharp/transformers) from leaking into
|
|
24
|
+
// non-AI parts of the system.
|
|
15
25
|
// Import from @rigour-labs/core/pattern-index instead.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud API Provider — runs inference via ANY cloud LLM API.
|
|
3
|
+
*
|
|
4
|
+
* The moat is local-first. But if a user brings their own key,
|
|
5
|
+
* we don't block them. No limitations. Support EVERY provider:
|
|
6
|
+
*
|
|
7
|
+
* - 'claude'/'anthropic' → Anthropic SDK (native)
|
|
8
|
+
* - Everything else → OpenAI-compatible SDK (works with OpenAI, Gemini, Groq,
|
|
9
|
+
* Mistral, Together, Fireworks, Perplexity, DeepSeek, self-hosted vLLM,
|
|
10
|
+
* Ollama, LM Studio, any OpenAI-compatible endpoint)
|
|
11
|
+
*
|
|
12
|
+
* User provides: api_key + provider name + optional base_url + optional model_name
|
|
13
|
+
* We figure out the rest. Their key, their choice.
|
|
14
|
+
*/
|
|
15
|
+
import type { InferenceProvider, InferenceOptions } from './types.js';
|
|
16
|
+
export declare class CloudProvider implements InferenceProvider {
|
|
17
|
+
readonly name: string;
|
|
18
|
+
private client;
|
|
19
|
+
private providerName;
|
|
20
|
+
private apiKey;
|
|
21
|
+
private baseUrl?;
|
|
22
|
+
private modelName;
|
|
23
|
+
private isClaude;
|
|
24
|
+
constructor(providerName: string, apiKey: string, options?: {
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
modelName?: string;
|
|
27
|
+
});
|
|
28
|
+
isAvailable(): Promise<boolean>;
|
|
29
|
+
setup(onProgress?: (message: string) => void): Promise<void>;
|
|
30
|
+
analyze(prompt: string, options?: InferenceOptions): Promise<string>;
|
|
31
|
+
private analyzeClaude;
|
|
32
|
+
private analyzeOpenAICompat;
|
|
33
|
+
dispose(): void;
|
|
34
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/** Default models per provider (user can override via model_name) */
|
|
2
|
+
const DEFAULT_MODELS = {
|
|
3
|
+
claude: 'claude-opus-4-6',
|
|
4
|
+
anthropic: 'claude-sonnet-4-6',
|
|
5
|
+
openai: 'gpt-4o-mini',
|
|
6
|
+
gemini: 'gemini-3-flash',
|
|
7
|
+
groq: 'llama-3.1-70b-versatile',
|
|
8
|
+
mistral: 'mistral-large-latest',
|
|
9
|
+
together: 'meta-llama/Llama-3.1-70B-Instruct-Turbo',
|
|
10
|
+
fireworks: 'accounts/fireworks/models/llama-v3p1-70b-instruct',
|
|
11
|
+
deepseek: 'deepseek-coder',
|
|
12
|
+
perplexity: 'llama-3.1-sonar-large-128k-online',
|
|
13
|
+
ollama: 'qwen2.5-coder:7b',
|
|
14
|
+
lmstudio: 'qwen2.5-coder-7b-instruct',
|
|
15
|
+
};
|
|
16
|
+
/** Default base URLs per provider */
|
|
17
|
+
const DEFAULT_BASE_URLS = {
|
|
18
|
+
openai: 'https://api.openai.com/v1',
|
|
19
|
+
gemini: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
|
20
|
+
groq: 'https://api.groq.com/openai/v1',
|
|
21
|
+
mistral: 'https://api.mistral.ai/v1',
|
|
22
|
+
together: 'https://api.together.xyz/v1',
|
|
23
|
+
fireworks: 'https://api.fireworks.ai/inference/v1',
|
|
24
|
+
deepseek: 'https://api.deepseek.com/v1',
|
|
25
|
+
perplexity: 'https://api.perplexity.ai',
|
|
26
|
+
ollama: 'http://localhost:11434/v1',
|
|
27
|
+
lmstudio: 'http://localhost:1234/v1',
|
|
28
|
+
};
|
|
29
|
+
export class CloudProvider {
|
|
30
|
+
name;
|
|
31
|
+
client = null;
|
|
32
|
+
providerName;
|
|
33
|
+
apiKey;
|
|
34
|
+
baseUrl;
|
|
35
|
+
modelName;
|
|
36
|
+
isClaude;
|
|
37
|
+
constructor(providerName, apiKey, options) {
|
|
38
|
+
if (!apiKey || apiKey.trim().length === 0) {
|
|
39
|
+
throw new Error(`API key cannot be empty for provider "${providerName}"`);
|
|
40
|
+
}
|
|
41
|
+
this.providerName = providerName.toLowerCase();
|
|
42
|
+
this.apiKey = apiKey.trim();
|
|
43
|
+
this.baseUrl = options?.baseUrl;
|
|
44
|
+
this.modelName = options?.modelName || DEFAULT_MODELS[this.providerName] || 'gpt-4o-mini';
|
|
45
|
+
this.isClaude = this.providerName === 'claude' || this.providerName === 'anthropic';
|
|
46
|
+
this.name = `cloud-${this.providerName}`;
|
|
47
|
+
}
|
|
48
|
+
async isAvailable() {
|
|
49
|
+
return !!this.apiKey;
|
|
50
|
+
}
|
|
51
|
+
async setup(onProgress) {
|
|
52
|
+
if (this.isClaude) {
|
|
53
|
+
try {
|
|
54
|
+
const { default: Anthropic } = await import('@anthropic-ai/sdk');
|
|
55
|
+
this.client = new Anthropic({ apiKey: this.apiKey });
|
|
56
|
+
onProgress?.(`✓ ${this.providerName} API connected (model: ${this.modelName})`);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new Error('Claude API SDK not installed. Run: npm install @anthropic-ai/sdk');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// OpenAI-compatible SDK — works with literally everything.
|
|
64
|
+
// OpenAI, Groq, Mistral, Together, Fireworks, DeepSeek, Perplexity,
|
|
65
|
+
// Gemini, Ollama, LM Studio, vLLM, any OpenAI-compatible endpoint.
|
|
66
|
+
// No limitations. User's key, user's choice.
|
|
67
|
+
try {
|
|
68
|
+
const { default: OpenAI } = await import('openai');
|
|
69
|
+
const baseURL = this.baseUrl || DEFAULT_BASE_URLS[this.providerName] || undefined;
|
|
70
|
+
this.client = new OpenAI({
|
|
71
|
+
apiKey: this.apiKey,
|
|
72
|
+
...(baseURL ? { baseURL } : {}),
|
|
73
|
+
});
|
|
74
|
+
onProgress?.(`✓ ${this.providerName} API connected (model: ${this.modelName})`);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
throw new Error('OpenAI SDK not installed (used for all OpenAI-compatible APIs). Run: npm install openai');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async analyze(prompt, options) {
|
|
82
|
+
if (!this.client) {
|
|
83
|
+
throw new Error('Provider not set up. Call setup() first.');
|
|
84
|
+
}
|
|
85
|
+
if (this.isClaude) {
|
|
86
|
+
return this.analyzeClaude(prompt, options);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
return this.analyzeOpenAICompat(prompt, options);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async analyzeClaude(prompt, options) {
|
|
93
|
+
const response = await this.client.messages.create({
|
|
94
|
+
model: this.modelName,
|
|
95
|
+
max_tokens: options?.maxTokens || 2048,
|
|
96
|
+
temperature: options?.temperature || 0.1,
|
|
97
|
+
messages: [
|
|
98
|
+
{ role: 'user', content: prompt }
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
const textBlock = response.content.find((b) => b.type === 'text');
|
|
102
|
+
if (!textBlock?.text) {
|
|
103
|
+
throw new Error(`Empty response from ${this.providerName} API (model: ${this.modelName}). Response had ${response.content.length} blocks but no text.`);
|
|
104
|
+
}
|
|
105
|
+
return textBlock.text;
|
|
106
|
+
}
|
|
107
|
+
async analyzeOpenAICompat(prompt, options) {
|
|
108
|
+
const response = await this.client.chat.completions.create({
|
|
109
|
+
model: this.modelName,
|
|
110
|
+
max_tokens: options?.maxTokens || 2048,
|
|
111
|
+
temperature: options?.temperature || 0.1,
|
|
112
|
+
messages: [
|
|
113
|
+
{ role: 'user', content: prompt }
|
|
114
|
+
],
|
|
115
|
+
...(options?.jsonMode ? { response_format: { type: 'json_object' } } : {}),
|
|
116
|
+
});
|
|
117
|
+
const content = response.choices[0]?.message?.content;
|
|
118
|
+
if (!content) {
|
|
119
|
+
throw new Error(`Empty response from ${this.providerName} API (model: ${this.modelName}). No content in choices.`);
|
|
120
|
+
}
|
|
121
|
+
return content;
|
|
122
|
+
}
|
|
123
|
+
dispose() {
|
|
124
|
+
this.client = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inference provider factory and exports.
|
|
3
|
+
*/
|
|
4
|
+
export type { InferenceProvider, InferenceOptions, DeepFinding, DeepAnalysisResult, ModelTier, ModelInfo } from './types.js';
|
|
5
|
+
export { MODELS } from './types.js';
|
|
6
|
+
export { SidecarProvider } from './sidecar-provider.js';
|
|
7
|
+
export { CloudProvider } from './cloud-provider.js';
|
|
8
|
+
export { ensureModel, isModelCached, getModelPath, getModelInfo, downloadModel, getModelsDir } from './model-manager.js';
|
|
9
|
+
import type { InferenceProvider } from './types.js';
|
|
10
|
+
import type { DeepOptions } from '../types/index.js';
|
|
11
|
+
/**
|
|
12
|
+
* Create the appropriate inference provider based on options.
|
|
13
|
+
*
|
|
14
|
+
* - No API key → SidecarProvider (local llama.cpp binary)
|
|
15
|
+
* - API key + any provider → CloudProvider (no restrictions, user's key, user's choice)
|
|
16
|
+
*/
|
|
17
|
+
export declare function createProvider(options: DeepOptions): InferenceProvider;
|