@rigour-labs/core 3.0.1 → 3.0.2

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.
@@ -46,10 +46,26 @@ export declare class HallucinatedImportsGate extends Gate {
46
46
  private isNodeBuiltin;
47
47
  private isPythonStdlib;
48
48
  /**
49
- * Check Go imports — verify relative/project package paths exist
50
- * Go stdlib packages are skipped; only project-relative imports are checked
49
+ * Check Go imports — verify project-relative package paths exist.
50
+ *
51
+ * Strategy:
52
+ * 1. Skip Go standard library (comprehensive list of 150+ packages)
53
+ * 2. Skip external modules (any path containing a dot → domain name)
54
+ * 3. Parse go.mod for the project module path
55
+ * 4. Only flag imports that match the project module prefix but don't resolve
56
+ *
57
+ * @since v3.0.1 — fixed false positives on Go stdlib (encoding/json, net/http, etc.)
51
58
  */
52
59
  private checkGoImports;
60
+ /**
61
+ * Comprehensive Go standard library package list.
62
+ * Includes all packages from Go 1.22+ (latest stable).
63
+ * Go stdlib is identified by having NO dots in the import path.
64
+ * We maintain an explicit list for packages with slashes (e.g. encoding/json).
65
+ *
66
+ * @since v3.0.1
67
+ */
68
+ private isGoStdlib;
53
69
  /**
54
70
  * Check Ruby imports — verify require_relative paths exist
55
71
  */
@@ -295,12 +295,31 @@ export class HallucinatedImportsGate extends Gate {
295
295
  return stdlibs.has(topLevel);
296
296
  }
297
297
  /**
298
- * Check Go imports — verify relative/project package paths exist
299
- * Go stdlib packages are skipped; only project-relative imports are checked
298
+ * Check Go imports — verify project-relative package paths exist.
299
+ *
300
+ * Strategy:
301
+ * 1. Skip Go standard library (comprehensive list of 150+ packages)
302
+ * 2. Skip external modules (any path containing a dot → domain name)
303
+ * 3. Parse go.mod for the project module path
304
+ * 4. Only flag imports that match the project module prefix but don't resolve
305
+ *
306
+ * @since v3.0.1 — fixed false positives on Go stdlib (encoding/json, net/http, etc.)
300
307
  */
301
308
  checkGoImports(content, file, cwd, projectFiles, hallucinated) {
302
309
  const lines = content.split('\n');
303
310
  let inImportBlock = false;
311
+ // Try to read go.mod for the module path
312
+ const goModPath = path.join(cwd, 'go.mod');
313
+ let modulePath = null;
314
+ try {
315
+ if (fs.pathExistsSync(goModPath)) {
316
+ const goMod = fs.readFileSync(goModPath, 'utf-8');
317
+ const moduleMatch = goMod.match(/^module\s+(\S+)/m);
318
+ if (moduleMatch)
319
+ modulePath = moduleMatch[1];
320
+ }
321
+ }
322
+ catch { /* no go.mod — skip project-relative checks entirely */ }
304
323
  for (let i = 0; i < lines.length; i++) {
305
324
  const line = lines[i].trim();
306
325
  // Detect import block: import ( ... )
@@ -312,30 +331,136 @@ export class HallucinatedImportsGate extends Gate {
312
331
  inImportBlock = false;
313
332
  continue;
314
333
  }
315
- // Single import: import "path"
316
- const singleMatch = line.match(/^import\s+"([^"]+)"/);
317
- const blockMatch = inImportBlock ? line.match(/^\s*"([^"]+)"/) : null;
334
+ // Single import: import "path" or import alias "path"
335
+ const singleMatch = line.match(/^import\s+(?:\w+\s+)?"([^"]+)"/);
336
+ const blockMatch = inImportBlock ? line.match(/^\s*(?:\w+\s+)?"([^"]+)"/) : null;
318
337
  const importPath = singleMatch?.[1] || blockMatch?.[1];
319
338
  if (!importPath)
320
339
  continue;
321
- // Skip Go stdlib (no dots in path = stdlib or well-known)
322
- if (!importPath.includes('.') && !importPath.includes('/'))
340
+ // 1. Skip Go standard library comprehensive list
341
+ if (this.isGoStdlib(importPath))
323
342
  continue;
324
- // Project-relative imports (contain module path from go.mod)
325
- // We only flag imports that look like project paths but don't resolve
326
- if (importPath.includes('/') && !importPath.startsWith('github.com') && !importPath.startsWith('golang.org')) {
327
- // Check if the path maps to a directory in the project
328
- const dirPath = importPath.split('/').slice(-2).join('/');
329
- const hasMatchingFile = [...projectFiles].some(f => f.includes(dirPath));
343
+ // 2. If we have a module path, check project-relative imports FIRST
344
+ // (project imports like github.com/myorg/project/pkg also have dots)
345
+ if (modulePath && importPath.startsWith(modulePath + '/')) {
346
+ const relPath = importPath.slice(modulePath.length + 1);
347
+ const hasMatchingFile = [...projectFiles].some(f => f.endsWith('.go') && f.startsWith(relPath));
330
348
  if (!hasMatchingFile) {
331
349
  hallucinated.push({
332
350
  file, line: i + 1, importPath, type: 'go',
333
- reason: `Go import '${importPath}' — package path not found in project`,
351
+ reason: `Go import '${importPath}' — package directory '${relPath}' not found in project`,
334
352
  });
335
353
  }
354
+ continue;
336
355
  }
356
+ // 3. Skip external modules — any import containing a dot is a domain
357
+ // e.g. github.com/*, google.golang.org/*, go.uber.org/*
358
+ if (importPath.includes('.'))
359
+ continue;
360
+ // 4. No dots, no go.mod match, not stdlib → likely an internal package
361
+ // without go.mod context we can't verify, so skip to avoid false positives
337
362
  }
338
363
  }
364
+ /**
365
+ * Comprehensive Go standard library package list.
366
+ * Includes all packages from Go 1.22+ (latest stable).
367
+ * Go stdlib is identified by having NO dots in the import path.
368
+ * We maintain an explicit list for packages with slashes (e.g. encoding/json).
369
+ *
370
+ * @since v3.0.1
371
+ */
372
+ isGoStdlib(importPath) {
373
+ // Fast check: single-segment packages are always stdlib if no dots
374
+ if (!importPath.includes('/') && !importPath.includes('.'))
375
+ return true;
376
+ // Check the full path against known stdlib packages with sub-paths
377
+ const topLevel = importPath.split('/')[0];
378
+ // All Go stdlib top-level packages (including those with sub-packages)
379
+ const stdlibTopLevel = new Set([
380
+ // Single-word packages
381
+ 'archive', 'bufio', 'builtin', 'bytes', 'cmp', 'compress',
382
+ 'container', 'context', 'crypto', 'database', 'debug',
383
+ 'embed', 'encoding', 'errors', 'expvar', 'flag', 'fmt',
384
+ 'go', 'hash', 'html', 'image', 'index', 'io', 'iter',
385
+ 'log', 'maps', 'math', 'mime', 'net', 'os', 'path',
386
+ 'plugin', 'reflect', 'regexp', 'runtime', 'slices', 'sort',
387
+ 'strconv', 'strings', 'structs', 'sync', 'syscall',
388
+ 'testing', 'text', 'time', 'unicode', 'unique', 'unsafe',
389
+ // Internal packages (used by stdlib, sometimes by tools)
390
+ 'internal', 'vendor',
391
+ ]);
392
+ if (stdlibTopLevel.has(topLevel))
393
+ return true;
394
+ // Explicit full-path list for maximum safety — covers all Go 1.22 stdlib paths
395
+ // This catches any edge case the top-level check might miss
396
+ const knownStdlibPaths = new Set([
397
+ // archive/*
398
+ 'archive/tar', 'archive/zip',
399
+ // compress/*
400
+ 'compress/bzip2', 'compress/flate', 'compress/gzip', 'compress/lzw', 'compress/zlib',
401
+ // container/*
402
+ 'container/heap', 'container/list', 'container/ring',
403
+ // crypto/*
404
+ 'crypto/aes', 'crypto/cipher', 'crypto/des', 'crypto/dsa',
405
+ 'crypto/ecdh', 'crypto/ecdsa', 'crypto/ed25519', 'crypto/elliptic',
406
+ 'crypto/hmac', 'crypto/md5', 'crypto/rand', 'crypto/rc4',
407
+ 'crypto/rsa', 'crypto/sha1', 'crypto/sha256', 'crypto/sha512',
408
+ 'crypto/subtle', 'crypto/tls', 'crypto/x509', 'crypto/x509/pkix',
409
+ // database/*
410
+ 'database/sql', 'database/sql/driver',
411
+ // debug/*
412
+ 'debug/buildinfo', 'debug/dwarf', 'debug/elf', 'debug/gosym',
413
+ 'debug/macho', 'debug/pe', 'debug/plan9obj',
414
+ // encoding/*
415
+ 'encoding/ascii85', 'encoding/asn1', 'encoding/base32', 'encoding/base64',
416
+ 'encoding/binary', 'encoding/csv', 'encoding/gob', 'encoding/hex',
417
+ 'encoding/json', 'encoding/pem', 'encoding/xml',
418
+ // go/*
419
+ 'go/ast', 'go/build', 'go/build/constraint', 'go/constant',
420
+ 'go/doc', 'go/doc/comment', 'go/format', 'go/importer',
421
+ 'go/parser', 'go/printer', 'go/scanner', 'go/token', 'go/types', 'go/version',
422
+ // hash/*
423
+ 'hash/adler32', 'hash/crc32', 'hash/crc64', 'hash/fnv', 'hash/maphash',
424
+ // html/*
425
+ 'html/template',
426
+ // image/*
427
+ 'image/color', 'image/color/palette', 'image/draw',
428
+ 'image/gif', 'image/jpeg', 'image/png',
429
+ // index/*
430
+ 'index/suffixarray',
431
+ // io/*
432
+ 'io/fs', 'io/ioutil',
433
+ // log/*
434
+ 'log/slog', 'log/syslog',
435
+ // math/*
436
+ 'math/big', 'math/bits', 'math/cmplx', 'math/rand', 'math/rand/v2',
437
+ // mime/*
438
+ 'mime/multipart', 'mime/quotedprintable',
439
+ // net/*
440
+ 'net/http', 'net/http/cgi', 'net/http/cookiejar', 'net/http/fcgi',
441
+ 'net/http/httptest', 'net/http/httptrace', 'net/http/httputil',
442
+ 'net/http/pprof', 'net/mail', 'net/netip', 'net/rpc',
443
+ 'net/rpc/jsonrpc', 'net/smtp', 'net/textproto', 'net/url',
444
+ // os/*
445
+ 'os/exec', 'os/signal', 'os/user',
446
+ // path/*
447
+ 'path/filepath',
448
+ // regexp/*
449
+ 'regexp/syntax',
450
+ // runtime/*
451
+ 'runtime/cgo', 'runtime/coverage', 'runtime/debug', 'runtime/metrics',
452
+ 'runtime/pprof', 'runtime/race', 'runtime/trace',
453
+ // sync/*
454
+ 'sync/atomic',
455
+ // testing/*
456
+ 'testing/fstest', 'testing/iotest', 'testing/quick', 'testing/slogtest',
457
+ // text/*
458
+ 'text/scanner', 'text/tabwriter', 'text/template', 'text/template/parse',
459
+ // unicode/*
460
+ 'unicode/utf16', 'unicode/utf8',
461
+ ]);
462
+ return knownStdlibPaths.has(importPath);
463
+ }
339
464
  /**
340
465
  * Check Ruby imports — verify require_relative paths exist
341
466
  */
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Hallucinated Imports Gate — Go Standard Library False Positive Regression Tests
3
+ *
4
+ * Tests the fix for https://github.com/rigour-labs/rigour/issues/XXX
5
+ * Previously, Go stdlib packages with slashes (encoding/json, net/http, etc.)
6
+ * were flagged as hallucinated imports because the gate only recognized
7
+ * single-word stdlib packages (fmt, os, io).
8
+ *
9
+ * @since v3.0.1
10
+ */
11
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
12
+ import { HallucinatedImportsGate } from './hallucinated-imports.js';
13
+ // fs-extra is mocked via module-level mock fns above
14
+ // Mock fs-extra — vi.hoisted ensures these are available when vi.mock runs (hoisted)
15
+ const { mockPathExists, mockPathExistsSync, mockReadFile, mockReadFileSync, mockReadJson } = vi.hoisted(() => ({
16
+ mockPathExists: vi.fn(),
17
+ mockPathExistsSync: vi.fn(),
18
+ mockReadFile: vi.fn(),
19
+ mockReadFileSync: vi.fn(),
20
+ mockReadJson: vi.fn(),
21
+ }));
22
+ vi.mock('fs-extra', () => {
23
+ const mock = {
24
+ pathExists: mockPathExists,
25
+ pathExistsSync: mockPathExistsSync,
26
+ readFile: mockReadFile,
27
+ readFileSync: mockReadFileSync,
28
+ readJson: mockReadJson,
29
+ };
30
+ return {
31
+ ...mock,
32
+ default: mock,
33
+ };
34
+ });
35
+ // Mock FileScanner
36
+ vi.mock('../utils/scanner.js', () => ({
37
+ FileScanner: {
38
+ findFiles: vi.fn().mockResolvedValue([]),
39
+ },
40
+ }));
41
+ import { FileScanner } from '../utils/scanner.js';
42
+ describe('HallucinatedImportsGate — Go stdlib false positives', () => {
43
+ let gate;
44
+ const testCwd = '/tmp/test-go-project';
45
+ const context = {
46
+ cwd: testCwd,
47
+ ignore: [],
48
+ };
49
+ beforeEach(() => {
50
+ vi.clearAllMocks();
51
+ gate = new HallucinatedImportsGate({ enabled: true });
52
+ });
53
+ /**
54
+ * This is the exact scenario from PicoClaw — Go stdlib packages with slashes
55
+ * were being flagged as hallucinated. These are ALL real Go stdlib packages.
56
+ */
57
+ it('should NOT flag Go standard library packages as hallucinated (PicoClaw regression)', async () => {
58
+ const goFileContent = `package main
59
+
60
+ import (
61
+ "encoding/json"
62
+ "path/filepath"
63
+ "net/http"
64
+ "crypto/rand"
65
+ "crypto/sha256"
66
+ "encoding/base64"
67
+ "os/exec"
68
+ "os/signal"
69
+ "net/url"
70
+ "fmt"
71
+ "io"
72
+ "os"
73
+ "strings"
74
+ "context"
75
+ "sync"
76
+ "time"
77
+ "log"
78
+ "errors"
79
+ "io/ioutil"
80
+ "io/fs"
81
+ "math/rand"
82
+ "regexp"
83
+ "strconv"
84
+ "bytes"
85
+ "bufio"
86
+ "sort"
87
+ "testing"
88
+ "net/http/httptest"
89
+ "database/sql"
90
+ "html/template"
91
+ "text/template"
92
+ "archive/zip"
93
+ "compress/gzip"
94
+ "runtime/debug"
95
+ )
96
+
97
+ func main() {}
98
+ `;
99
+ const goFile = 'main.go';
100
+ FileScanner.findFiles.mockResolvedValue([goFile]);
101
+ mockReadFile.mockResolvedValue(goFileContent);
102
+ mockPathExists.mockResolvedValue(false);
103
+ mockPathExistsSync.mockReturnValue(false); // no go.mod
104
+ const failures = await gate.run(context);
105
+ // ZERO failures — every import above is a real Go stdlib package
106
+ expect(failures).toHaveLength(0);
107
+ });
108
+ it('should NOT flag external module imports (github.com, etc.)', async () => {
109
+ const goFileContent = `package main
110
+
111
+ import (
112
+ "github.com/gin-gonic/gin"
113
+ "github.com/stretchr/testify/assert"
114
+ "google.golang.org/grpc"
115
+ "go.uber.org/zap"
116
+ "golang.org/x/crypto/bcrypt"
117
+ )
118
+
119
+ func main() {}
120
+ `;
121
+ FileScanner.findFiles.mockResolvedValue(['main.go']);
122
+ mockReadFile.mockResolvedValue(goFileContent);
123
+ mockPathExists.mockResolvedValue(false);
124
+ mockPathExistsSync.mockReturnValue(false);
125
+ const failures = await gate.run(context);
126
+ expect(failures).toHaveLength(0);
127
+ });
128
+ it('should flag project-relative imports that do not resolve (with go.mod)', async () => {
129
+ const goMod = `module github.com/myorg/myproject
130
+
131
+ go 1.22
132
+ `;
133
+ const goFileContent = `package main
134
+
135
+ import (
136
+ "fmt"
137
+ "github.com/myorg/myproject/pkg/realmodule"
138
+ "github.com/myorg/myproject/pkg/doesnotexist"
139
+ )
140
+
141
+ func main() {}
142
+ `;
143
+ FileScanner.findFiles.mockResolvedValue(['cmd/main.go', 'pkg/realmodule/handler.go']);
144
+ mockReadFile.mockImplementation(async (filePath) => {
145
+ if (filePath.includes('handler.go'))
146
+ return 'package realmodule\n\nimport "fmt"\n\nfunc Handler() {}\n';
147
+ return goFileContent;
148
+ });
149
+ mockPathExists.mockResolvedValue(false);
150
+ mockPathExistsSync.mockReturnValue(true); // go.mod exists
151
+ mockReadFileSync.mockReturnValue(goMod);
152
+ const failures = await gate.run(context);
153
+ // Should flag doesnotexist but NOT realmodule
154
+ expect(failures).toHaveLength(1);
155
+ expect(failures[0].details).toContain('doesnotexist');
156
+ expect(failures[0].details).not.toContain('realmodule');
157
+ });
158
+ it('should NOT flag anything when no go.mod exists and imports have no dots', async () => {
159
+ // Without go.mod, we can't determine the project module path,
160
+ // so we skip project-relative checks to avoid false positives
161
+ const goFileContent = `package main
162
+
163
+ import (
164
+ "fmt"
165
+ "net/http"
166
+ "internal/custom"
167
+ )
168
+
169
+ func main() {}
170
+ `;
171
+ FileScanner.findFiles.mockResolvedValue(['main.go']);
172
+ mockReadFile.mockResolvedValue(goFileContent);
173
+ mockPathExists.mockResolvedValue(false);
174
+ mockPathExistsSync.mockReturnValue(false);
175
+ const failures = await gate.run(context);
176
+ expect(failures).toHaveLength(0);
177
+ });
178
+ it('should handle single-line imports', async () => {
179
+ const goFileContent = `package main
180
+
181
+ import "fmt"
182
+ import "encoding/json"
183
+ import "net/http"
184
+
185
+ func main() {}
186
+ `;
187
+ FileScanner.findFiles.mockResolvedValue(['main.go']);
188
+ mockReadFile.mockResolvedValue(goFileContent);
189
+ mockPathExists.mockResolvedValue(false);
190
+ mockPathExistsSync.mockReturnValue(false);
191
+ const failures = await gate.run(context);
192
+ expect(failures).toHaveLength(0);
193
+ });
194
+ it('should handle aliased imports', async () => {
195
+ const goFileContent = `package main
196
+
197
+ import (
198
+ "fmt"
199
+ mrand "math/rand"
200
+ _ "net/http/pprof"
201
+ . "os"
202
+ )
203
+
204
+ func main() {}
205
+ `;
206
+ FileScanner.findFiles.mockResolvedValue(['main.go']);
207
+ mockReadFile.mockResolvedValue(goFileContent);
208
+ mockPathExists.mockResolvedValue(false);
209
+ mockPathExistsSync.mockReturnValue(false);
210
+ const failures = await gate.run(context);
211
+ expect(failures).toHaveLength(0);
212
+ });
213
+ });
214
+ describe('HallucinatedImportsGate — Python stdlib coverage', () => {
215
+ let gate;
216
+ const testCwd = '/tmp/test-py-project';
217
+ const context = {
218
+ cwd: testCwd,
219
+ ignore: [],
220
+ };
221
+ beforeEach(() => {
222
+ vi.clearAllMocks();
223
+ gate = new HallucinatedImportsGate({ enabled: true });
224
+ });
225
+ it('should NOT flag Python standard library imports', async () => {
226
+ const pyContent = `
227
+ import os
228
+ import sys
229
+ import json
230
+ import hashlib
231
+ import pathlib
232
+ import subprocess
233
+ import argparse
234
+ import typing
235
+ import dataclasses
236
+ import functools
237
+ import itertools
238
+ import collections
239
+ import datetime
240
+ import re
241
+ import math
242
+ import random
243
+ import threading
244
+ import asyncio
245
+ from os.path import join, exists
246
+ from collections import defaultdict
247
+ from typing import List, Optional
248
+ from urllib.parse import urlparse
249
+ `;
250
+ FileScanner.findFiles.mockResolvedValue(['main.py']);
251
+ mockReadFile.mockResolvedValue(pyContent);
252
+ mockPathExists.mockResolvedValue(false);
253
+ const failures = await gate.run(context);
254
+ expect(failures).toHaveLength(0);
255
+ });
256
+ });
257
+ describe('HallucinatedImportsGate — JS/TS Node builtins', () => {
258
+ let gate;
259
+ const testCwd = '/tmp/test-node-project';
260
+ const context = {
261
+ cwd: testCwd,
262
+ ignore: [],
263
+ };
264
+ beforeEach(() => {
265
+ vi.clearAllMocks();
266
+ gate = new HallucinatedImportsGate({ enabled: true });
267
+ });
268
+ it('should NOT flag Node.js built-in modules', async () => {
269
+ const jsContent = `
270
+ import fs from 'fs';
271
+ import path from 'path';
272
+ import crypto from 'crypto';
273
+ import http from 'http';
274
+ import https from 'https';
275
+ import url from 'url';
276
+ import os from 'os';
277
+ import stream from 'stream';
278
+ import util from 'util';
279
+ import { readFile } from 'node:fs';
280
+ import { join } from 'node:path';
281
+ `;
282
+ FileScanner.findFiles.mockResolvedValue(['index.ts']);
283
+ mockReadFile.mockResolvedValue(jsContent);
284
+ mockPathExists.mockResolvedValue(false);
285
+ const failures = await gate.run(context);
286
+ expect(failures).toHaveLength(0);
287
+ });
288
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
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",