@rigour-labs/core 3.0.2 → 3.0.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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,320 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ const mockFindFiles = vi.hoisted(() => vi.fn());
3
+ const mockReadFile = vi.hoisted(() => vi.fn());
4
+ const mockPathExists = vi.hoisted(() => vi.fn());
5
+ vi.mock('../utils/scanner.js', () => ({
6
+ FileScanner: { findFiles: mockFindFiles },
7
+ }));
8
+ vi.mock('fs-extra', () => ({
9
+ default: {
10
+ readFile: mockReadFile,
11
+ pathExists: mockPathExists,
12
+ pathExistsSync: vi.fn().mockReturnValue(false),
13
+ readFileSync: vi.fn().mockReturnValue(''),
14
+ readJson: vi.fn().mockResolvedValue(null),
15
+ readdirSync: vi.fn().mockReturnValue([]),
16
+ },
17
+ }));
18
+ import { PhantomApisGate } from './phantom-apis.js';
19
+ describe('PhantomApisGate — Node.js', () => {
20
+ let gate;
21
+ beforeEach(() => {
22
+ gate = new PhantomApisGate();
23
+ vi.clearAllMocks();
24
+ });
25
+ it('should flag non-existent fs methods', async () => {
26
+ mockFindFiles.mockResolvedValue(['src/utils.ts']);
27
+ mockReadFile.mockResolvedValue(`
28
+ import fs from 'fs';
29
+ const data = fs.readFileAsync('test.txt');
30
+ const result = fs.writeFilePromise('out.txt', data);
31
+ `);
32
+ const failures = await gate.run({ cwd: '/project' });
33
+ expect(failures).toHaveLength(1);
34
+ expect(failures[0].details).toContain('readFileAsync');
35
+ expect(failures[0].details).toContain('writeFilePromise');
36
+ });
37
+ it('should NOT flag real fs methods', async () => {
38
+ mockFindFiles.mockResolvedValue(['src/utils.ts']);
39
+ mockReadFile.mockResolvedValue(`
40
+ import fs from 'fs';
41
+ const data = fs.readFileSync('test.txt', 'utf-8');
42
+ fs.writeFileSync('out.txt', data);
43
+ fs.existsSync('/tmp');
44
+ const stream = fs.createReadStream('big.txt');
45
+ `);
46
+ const failures = await gate.run({ cwd: '/project' });
47
+ expect(failures).toHaveLength(0);
48
+ });
49
+ it('should flag non-existent path methods', async () => {
50
+ mockFindFiles.mockResolvedValue(['src/paths.ts']);
51
+ mockReadFile.mockResolvedValue(`
52
+ import path from 'path';
53
+ const combined = path.combine('a', 'b');
54
+ const exists = path.exists('/tmp');
55
+ `);
56
+ const failures = await gate.run({ cwd: '/project' });
57
+ expect(failures).toHaveLength(1);
58
+ expect(failures[0].details).toContain('combine');
59
+ expect(failures[0].details).toContain('exists');
60
+ });
61
+ it('should NOT flag real path methods', async () => {
62
+ mockFindFiles.mockResolvedValue(['src/paths.ts']);
63
+ mockReadFile.mockResolvedValue(`
64
+ import path from 'path';
65
+ const p = path.join('a', 'b');
66
+ const ext = path.extname('file.txt');
67
+ const abs = path.resolve('.');
68
+ `);
69
+ const failures = await gate.run({ cwd: '/project' });
70
+ expect(failures).toHaveLength(0);
71
+ });
72
+ it('should flag non-existent crypto methods', async () => {
73
+ mockFindFiles.mockResolvedValue(['src/crypto.ts']);
74
+ mockReadFile.mockResolvedValue(`
75
+ import crypto from 'crypto';
76
+ const hash = crypto.generateHash('sha256', 'data');
77
+ const key = crypto.createKey('aes-256');
78
+ `);
79
+ const failures = await gate.run({ cwd: '/project' });
80
+ expect(failures).toHaveLength(1);
81
+ expect(failures[0].details).toContain('generateHash');
82
+ expect(failures[0].details).toContain('createKey');
83
+ });
84
+ it('should NOT flag real crypto methods', async () => {
85
+ mockFindFiles.mockResolvedValue(['src/crypto.ts']);
86
+ mockReadFile.mockResolvedValue(`
87
+ import crypto from 'crypto';
88
+ const hash = crypto.createHash('sha256');
89
+ const uuid = crypto.randomUUID();
90
+ `);
91
+ const failures = await gate.run({ cwd: '/project' });
92
+ expect(failures).toHaveLength(0);
93
+ });
94
+ it('should handle require() imports', async () => {
95
+ mockFindFiles.mockResolvedValue(['src/legacy.js']);
96
+ mockReadFile.mockResolvedValue(`
97
+ const fs = require('fs');
98
+ fs.readFileAsync('data.txt');
99
+ `);
100
+ const failures = await gate.run({ cwd: '/project' });
101
+ expect(failures).toHaveLength(1);
102
+ expect(failures[0].details).toContain('readFileAsync');
103
+ });
104
+ it('should handle node: protocol imports', async () => {
105
+ mockFindFiles.mockResolvedValue(['src/modern.ts']);
106
+ mockReadFile.mockResolvedValue(`
107
+ import os from 'node:os';
108
+ const info = os.cpuInfo();
109
+ `);
110
+ const failures = await gate.run({ cwd: '/project' });
111
+ expect(failures).toHaveLength(1);
112
+ expect(failures[0].details).toContain('cpuInfo');
113
+ });
114
+ it('should suggest closest real method', async () => {
115
+ mockFindFiles.mockResolvedValue(['src/typo.ts']);
116
+ mockReadFile.mockResolvedValue(`
117
+ import fs from 'fs';
118
+ fs.readFileSyn('data.txt');
119
+ `);
120
+ const failures = await gate.run({ cwd: '/project' });
121
+ expect(failures).toHaveLength(1);
122
+ expect(failures[0].details).toContain('readFileSync');
123
+ });
124
+ it('should not scan when disabled', async () => {
125
+ const disabled = new PhantomApisGate({ enabled: false });
126
+ const failures = await disabled.run({ cwd: '/project' });
127
+ expect(failures).toHaveLength(0);
128
+ expect(mockFindFiles).not.toHaveBeenCalled();
129
+ });
130
+ });
131
+ describe('PhantomApisGate — Python', () => {
132
+ let gate;
133
+ beforeEach(() => {
134
+ gate = new PhantomApisGate();
135
+ vi.clearAllMocks();
136
+ });
137
+ it('should flag non-existent os methods', async () => {
138
+ mockFindFiles.mockResolvedValue(['utils.py']);
139
+ mockReadFile.mockResolvedValue(`
140
+ import os
141
+ current = os.getCurrentDirectory()
142
+ files = os.listFiles('.')
143
+ `);
144
+ const failures = await gate.run({ cwd: '/project' });
145
+ expect(failures).toHaveLength(1);
146
+ expect(failures[0].details).toContain('getCurrentDirectory');
147
+ expect(failures[0].details).toContain('listFiles');
148
+ });
149
+ it('should NOT flag real os methods', async () => {
150
+ mockFindFiles.mockResolvedValue(['utils.py']);
151
+ mockReadFile.mockResolvedValue(`
152
+ import os
153
+ current = os.getcwd()
154
+ files = os.listdir('.')
155
+ exists = os.path
156
+ `);
157
+ const failures = await gate.run({ cwd: '/project' });
158
+ expect(failures).toHaveLength(0);
159
+ });
160
+ it('should flag non-existent json methods', async () => {
161
+ mockFindFiles.mockResolvedValue(['parser.py']);
162
+ mockReadFile.mockResolvedValue(`
163
+ import json
164
+ data = json.parse('{"key": "value"}')
165
+ result = json.stringify(data)
166
+ `);
167
+ const failures = await gate.run({ cwd: '/project' });
168
+ expect(failures).toHaveLength(1);
169
+ expect(failures[0].details).toContain('parse');
170
+ expect(failures[0].details).toContain('stringify');
171
+ });
172
+ it('should NOT flag real json methods', async () => {
173
+ mockFindFiles.mockResolvedValue(['parser.py']);
174
+ mockReadFile.mockResolvedValue(`
175
+ import json
176
+ data = json.loads('{"key": "value"}')
177
+ output = json.dumps(data)
178
+ `);
179
+ const failures = await gate.run({ cwd: '/project' });
180
+ expect(failures).toHaveLength(0);
181
+ });
182
+ it('should handle aliased imports', async () => {
183
+ mockFindFiles.mockResolvedValue(['utils.py']);
184
+ mockReadFile.mockResolvedValue(`
185
+ import os as operating_system
186
+ result = operating_system.getCurrentDirectory()
187
+ `);
188
+ const failures = await gate.run({ cwd: '/project' });
189
+ expect(failures).toHaveLength(1);
190
+ expect(failures[0].details).toContain('getCurrentDirectory');
191
+ });
192
+ it('should flag non-existent subprocess methods', async () => {
193
+ mockFindFiles.mockResolvedValue(['runner.py']);
194
+ mockReadFile.mockResolvedValue(`
195
+ import subprocess
196
+ result = subprocess.execute(['ls', '-la'])
197
+ `);
198
+ const failures = await gate.run({ cwd: '/project' });
199
+ expect(failures).toHaveLength(1);
200
+ expect(failures[0].details).toContain('execute');
201
+ });
202
+ });
203
+ describe('PhantomApisGate — Go', () => {
204
+ let gate;
205
+ beforeEach(() => {
206
+ gate = new PhantomApisGate();
207
+ vi.clearAllMocks();
208
+ });
209
+ it('should flag Python-style Go method names', async () => {
210
+ mockFindFiles.mockResolvedValue(['main.go']);
211
+ mockReadFile.mockResolvedValue(`
212
+ package main
213
+ import "strings"
214
+ func main() {
215
+ result := strings.includes("hello", "ell")
216
+ }
217
+ `);
218
+ const failures = await gate.run({ cwd: '/project' });
219
+ expect(failures).toHaveLength(1);
220
+ expect(failures[0].details).toContain('includes');
221
+ expect(failures[0].details).toContain('strings.Contains');
222
+ });
223
+ it('should flag JS-style JSON methods in Go', async () => {
224
+ mockFindFiles.mockResolvedValue(['parser.go']);
225
+ mockReadFile.mockResolvedValue(`
226
+ package main
227
+ import "encoding/json"
228
+ func parse() {
229
+ data := json.parse(raw)
230
+ out := json.stringify(data)
231
+ }
232
+ `);
233
+ const failures = await gate.run({ cwd: '/project' });
234
+ expect(failures).toHaveLength(1);
235
+ expect(failures[0].details).toContain('parse');
236
+ });
237
+ it('should flag os.Exists in Go', async () => {
238
+ mockFindFiles.mockResolvedValue(['files.go']);
239
+ mockReadFile.mockResolvedValue(`
240
+ package main
241
+ import "os"
242
+ func check() {
243
+ exists := os.Exists("/tmp/file")
244
+ }
245
+ `);
246
+ const failures = await gate.run({ cwd: '/project' });
247
+ expect(failures).toHaveLength(1);
248
+ expect(failures[0].details).toContain('Exists');
249
+ });
250
+ });
251
+ describe('PhantomApisGate — C#', () => {
252
+ let gate;
253
+ beforeEach(() => {
254
+ gate = new PhantomApisGate();
255
+ vi.clearAllMocks();
256
+ });
257
+ it('should flag Java-style method casing in C#', async () => {
258
+ mockFindFiles.mockResolvedValue(['Program.cs']);
259
+ mockReadFile.mockResolvedValue(`
260
+ using System;
261
+ class Program {
262
+ void Main() {
263
+ string s = "hello";
264
+ int len = s.length;
265
+ bool eq = s.equals("world");
266
+ string str = s.toString();
267
+ }
268
+ }
269
+ `);
270
+ const failures = await gate.run({ cwd: '/project' });
271
+ expect(failures.length).toBeGreaterThanOrEqual(1);
272
+ expect(failures[0].details).toMatch(/length|equals|toString/);
273
+ });
274
+ it('should flag Java collections in C#', async () => {
275
+ mockFindFiles.mockResolvedValue(['Service.cs']);
276
+ mockReadFile.mockResolvedValue(`
277
+ using System.Collections.Generic;
278
+ List<string> items = new ArrayList<string>();
279
+ HashMap<string, int> map = new HashMap<string, int>();
280
+ System.out.println("hello");
281
+ `);
282
+ const failures = await gate.run({ cwd: '/project' });
283
+ expect(failures.length).toBeGreaterThanOrEqual(1);
284
+ });
285
+ });
286
+ describe('PhantomApisGate — Java', () => {
287
+ let gate;
288
+ beforeEach(() => {
289
+ gate = new PhantomApisGate();
290
+ vi.clearAllMocks();
291
+ });
292
+ it('should flag JS/Python-style method names in Java', async () => {
293
+ mockFindFiles.mockResolvedValue(['Main.java']);
294
+ mockReadFile.mockResolvedValue(`
295
+ import java.util.List;
296
+ class Main {
297
+ void test() {
298
+ List<String> items = new ArrayList<>();
299
+ items.push("hello");
300
+ items.includes("test");
301
+ }
302
+ }
303
+ `);
304
+ const failures = await gate.run({ cwd: '/project' });
305
+ expect(failures.length).toBeGreaterThanOrEqual(1);
306
+ expect(failures[0].details).toMatch(/push|includes/);
307
+ });
308
+ it('should flag new List() in Java', async () => {
309
+ mockFindFiles.mockResolvedValue(['Service.java']);
310
+ mockReadFile.mockResolvedValue(`
311
+ import java.util.*;
312
+ class Service {
313
+ List<String> items = new List<String>();
314
+ Map<String, Integer> map = new Map<String, Integer>();
315
+ }
316
+ `);
317
+ const failures = await gate.run({ cwd: '/project' });
318
+ expect(failures.length).toBeGreaterThanOrEqual(1);
319
+ });
320
+ });
@@ -18,6 +18,9 @@ import { HallucinatedImportsGate } from './hallucinated-imports.js';
18
18
  import { InconsistentErrorHandlingGate } from './inconsistent-error-handling.js';
19
19
  import { ContextWindowArtifactsGate } from './context-window-artifacts.js';
20
20
  import { PromiseSafetyGate } from './promise-safety.js';
21
+ import { PhantomApisGate } from './phantom-apis.js';
22
+ import { DeprecatedApisGate } from './deprecated-apis.js';
23
+ import { TestQualityGate } from './test-quality.js';
21
24
  import { execa } from 'execa';
22
25
  import { Logger } from '../utils/logger.js';
23
26
  export class GateRunner {
@@ -78,6 +81,16 @@ export class GateRunner {
78
81
  if (this.config.gates.promise_safety?.enabled !== false) {
79
82
  this.gates.push(new PromiseSafetyGate(this.config.gates.promise_safety));
80
83
  }
84
+ // v3.1+ Extended Hallucination Detection
85
+ if (this.config.gates.phantom_apis?.enabled !== false) {
86
+ this.gates.push(new PhantomApisGate(this.config.gates.phantom_apis));
87
+ }
88
+ if (this.config.gates.deprecated_apis?.enabled !== false) {
89
+ this.gates.push(new DeprecatedApisGate(this.config.gates.deprecated_apis));
90
+ }
91
+ if (this.config.gates.test_quality?.enabled !== false) {
92
+ this.gates.push(new TestQualityGate(this.config.gates.test_quality));
93
+ }
81
94
  // Environment Alignment Gate (Should be prioritized)
82
95
  if (this.config.gates.environment?.enabled) {
83
96
  this.gates.unshift(new EnvironmentGate(this.config.gates));
@@ -162,20 +175,34 @@ export class GateRunner {
162
175
  }
163
176
  const score = Math.max(0, 100 - totalDeduction);
164
177
  // Two-score system: separate AI health from structural quality
178
+ // IMPORTANT: Only ai-drift affects ai_health_score, only traditional affects structural_score.
179
+ // Security and governance affect the overall score but NOT the sub-scores,
180
+ // preventing security criticals from incorrectly zeroing structural_score.
165
181
  let aiDeduction = 0;
166
- let aiCount = 0;
167
182
  let structuralDeduction = 0;
168
- let structuralCount = 0;
183
+ const provenanceCounts = {
184
+ 'ai-drift': 0,
185
+ 'traditional': 0,
186
+ 'security': 0,
187
+ 'governance': 0,
188
+ };
169
189
  for (const f of failures) {
170
190
  const sev = (f.severity || 'medium');
171
191
  const weight = SEVERITY_WEIGHTS[sev] ?? 5;
172
- if (f.provenance === 'ai-drift') {
173
- aiDeduction += weight;
174
- aiCount++;
175
- }
176
- else {
177
- structuralDeduction += weight;
178
- structuralCount++;
192
+ const prov = f.provenance || 'traditional';
193
+ provenanceCounts[prov] = (provenanceCounts[prov] || 0) + 1;
194
+ switch (prov) {
195
+ case 'ai-drift':
196
+ aiDeduction += weight;
197
+ break;
198
+ case 'traditional':
199
+ structuralDeduction += weight;
200
+ break;
201
+ // security and governance contribute to overall score (totalDeduction)
202
+ // but do NOT pollute the sub-scores
203
+ case 'security':
204
+ case 'governance':
205
+ break;
179
206
  }
180
207
  }
181
208
  return {
@@ -188,12 +215,7 @@ export class GateRunner {
188
215
  ai_health_score: Math.max(0, 100 - aiDeduction),
189
216
  structural_score: Math.max(0, 100 - structuralDeduction),
190
217
  severity_breakdown: severityBreakdown,
191
- provenance_breakdown: {
192
- 'ai-drift': aiCount,
193
- traditional: structuralCount - failures.filter(f => f.provenance === 'security' || f.provenance === 'governance').length,
194
- security: failures.filter(f => f.provenance === 'security').length,
195
- governance: failures.filter(f => f.provenance === 'governance').length,
196
- },
218
+ provenance_breakdown: provenanceCounts,
197
219
  },
198
220
  };
199
221
  }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * AI Test Quality Gate
3
+ *
4
+ * Detects AI-generated test anti-patterns that create false confidence.
5
+ * AI models generate tests that look comprehensive but actually test
6
+ * the AI's own assumptions rather than the developer's intent.
7
+ *
8
+ * Detected anti-patterns:
9
+ * 1. Empty test bodies — tests with no assertions
10
+ * 2. Tautological assertions — expect(true).toBe(true), assert True
11
+ * 3. Mock-everything — tests that mock every dependency (test nothing real)
12
+ * 4. Missing error path tests — only happy path tested
13
+ * 5. Shallow snapshot abuse — snapshot tests with no semantic assertions
14
+ * 6. Assertion-free async — async tests that never await/assert
15
+ *
16
+ * Supported test frameworks:
17
+ * JS/TS — Jest, Vitest, Mocha, Jasmine, Node test runner
18
+ * Python — pytest, unittest
19
+ * Go — testing package (t.Run, table-driven tests)
20
+ * Java — JUnit 4/5, TestNG
21
+ * Kotlin — JUnit 5, kotlin.test
22
+ *
23
+ * @since v3.0.0
24
+ * @since v3.0.3 — Go, Java, Kotlin support added
25
+ */
26
+ import { Gate, GateContext } from './base.js';
27
+ import { Failure, Provenance } from '../types/index.js';
28
+ export interface TestQualityIssue {
29
+ file: string;
30
+ line: number;
31
+ pattern: string;
32
+ reason: string;
33
+ }
34
+ export interface TestQualityConfig {
35
+ enabled?: boolean;
36
+ check_empty_tests?: boolean;
37
+ check_tautological?: boolean;
38
+ check_mock_heavy?: boolean;
39
+ check_snapshot_abuse?: boolean;
40
+ check_assertion_free_async?: boolean;
41
+ max_mocks_per_test?: number;
42
+ ignore_patterns?: string[];
43
+ }
44
+ export declare class TestQualityGate extends Gate {
45
+ private config;
46
+ constructor(config?: TestQualityConfig);
47
+ protected get provenance(): Provenance;
48
+ run(context: GateContext): Promise<Failure[]>;
49
+ private checkJSTestQuality;
50
+ private analyzeJSTestBlock;
51
+ private checkPythonTestQuality;
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
+ }