@rigour-labs/core 6.7.1 → 6.7.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.
package/README.md CHANGED
@@ -23,6 +23,6 @@ console.log(result.status, result.findings);
23
23
  ```
24
24
 
25
25
  What each check finds and whether it blocks: [What Rigour checks](https://github.com/rigour-labs/rigour/blob/main/docs/CHECKS.md). Every setting:
26
- [Configuration reference](https://github.com/rigour-labs/rigour/blob/main/docs/CONFIG_REFERENCE.md). Node 22 or later.
26
+ [Configuration reference](https://github.com/rigour-labs/rigour/blob/main/docs/CONFIG_REFERENCE.md). Node 22.13 or later.
27
27
 
28
28
  MIT © [Rigour Labs](https://github.com/rigour-labs)
@@ -43,13 +43,28 @@ export async function reviewChange(input) {
43
43
  const preexisting = await dropPreexisting(input, report, targets);
44
44
  if (input.diffTests && deep)
45
45
  report.failures.push(...await diffTestFailures(input.cwd, input.source, deep));
46
- report.failures.push(...migrationOrderFailures(input.cwd, diff, input.source, input.config));
47
- report.failures.push(...unusedExportFailures(input.cwd, diff, input.config), ...orphanFileFailures(input.cwd, diff, input.config));
48
- report.failures.push(...queryPatternFailures(input.cwd, changedLines, input.config), ...optionalParamFailures(input.cwd, changedLines, input.config), ...duplicateFunctionFailures(input.cwd, changedLines, input.config), ...loopCopyFailures(input.cwd, changedLines, input.config), ...partialFixFailures(input.cwd, changedLines, input.config), ...partialWiringFailures(input.cwd, changedLines, input.config));
46
+ // The review's own checks, each recorded in the summary beside the gates, so a report says everything that ran.
47
+ const reviewCheck = (id, key, failures) => {
48
+ const enabled = input.config.gates[key]?.enabled;
49
+ report.summary[id] = !enabled ? 'SKIP' : failures.length ? 'FAIL' : 'PASS';
50
+ report.failures.push(...failures);
51
+ };
52
+ reviewCheck('migration-order', 'migration_order', migrationOrderFailures(input.cwd, diff, input.source, input.config));
53
+ reviewCheck('unused-exports', 'unused_exports', unusedExportFailures(input.cwd, diff, input.config));
54
+ reviewCheck('orphan-files', 'orphan_files', orphanFileFailures(input.cwd, diff, input.config));
55
+ reviewCheck('query-patterns', 'query_patterns', queryPatternFailures(input.cwd, changedLines, input.config));
56
+ reviewCheck('optional-params', 'optional_params', optionalParamFailures(input.cwd, changedLines, input.config));
57
+ reviewCheck('duplicate-functions', 'duplicate_functions', duplicateFunctionFailures(input.cwd, changedLines, input.config));
58
+ reviewCheck('change-sweep', 'change_sweep', [
59
+ ...loopCopyFailures(input.cwd, changedLines, input.config),
60
+ ...partialFixFailures(input.cwd, changedLines, input.config),
61
+ ...partialWiringFailures(input.cwd, changedLines, input.config),
62
+ ]);
49
63
  const typed = input.typed ? typedChecks(input.cwd, changedLines, input.config) : { failures: [], hints: [] };
50
- report.failures.push(...typed.failures);
51
64
  if (typed.error)
52
65
  report.summary[TYPED_CHECKS] = 'ERROR'; // a check that could not run is a crashed gate, never a pass
66
+ else if (input.typed)
67
+ reviewCheck('redundancy', 'redundancy', typed.failures);
53
68
  const split = splitByChangedLines(report.failures, changedLines, deep ? changedFunctionSpans(input.cwd, changedLines) : {}, removedByFile(diff));
54
69
  const deepError = deepAnalysisError(report);
55
70
  const quiet = quietSplit(input.cwd, split.findings, input.config.review?.include_heuristics, input.trustedRef);
@@ -117,6 +117,17 @@ describe('git-backed review', () => {
117
117
  expect(semantic.map(f => [f.files?.[0], f.line])).toEqual([['src/notify.ts', 2]]);
118
118
  expect(toReviewFinding(semantic[0])).toMatchObject({ id: 'semantic-bugs', file: 'src/notify.ts', line: 2, severity: 'high' });
119
119
  });
120
+ it("names every check it ran in the summary, the review's own beside the gates", async () => {
121
+ write('src/a.ts', 'export const a = 1;\n');
122
+ git('add', '-A');
123
+ git('commit', '-qm', 'init');
124
+ write('src/b.ts', 'export const unused = 2;\n');
125
+ git('add', '-A');
126
+ const config = ConfigSchema.parse({ gates: { migration_order: { enabled: false } } });
127
+ const { report } = await reviewChange({ cwd: repo, config });
128
+ expect(report?.summary).toMatchObject({ 'unused-exports': 'FAIL', 'migration-order': 'SKIP', 'query-patterns': 'PASS', 'change-sweep': 'PASS' });
129
+ expect(report?.summary).not.toHaveProperty('redundancy'); // typed checks were not asked for
130
+ });
120
131
  it('reports what the change introduced, not what the touched code already had', async () => {
121
132
  const branchy = (name, extra = '') => [
122
133
  `export function ${name}(x: number) {`,
@@ -94,7 +94,7 @@ export async function generateTemporalDriftReport(cwd, maxScans = 200) {
94
94
  if (!db) {
95
95
  if (!_sqliteWarningShown) {
96
96
  _sqliteWarningShown = true;
97
- Logger.warn('Temporal drift: SQLite not available — install sqlite3 to enable scan history');
97
+ Logger.warn('Temporal drift: SQLite not available (it is built into Node 22.13 and later), so there is no scan history');
98
98
  }
99
99
  return null;
100
100
  }
@@ -1,8 +1,8 @@
1
1
  declare const RIGOUR_DIR: string;
2
2
  declare const DB_PATH: string;
3
3
  /**
4
- * Promisified wrapper around a node-sqlite3 Database instance.
5
- * Provides run/get/all/exec methods that return Promises.
4
+ * The database behind a promise API: run/get/all/exec return Promises, so callers do not change
5
+ * with the driver, and a later move to a worker or an async driver stays inside this file.
6
6
  */
7
7
  export interface RigourDB {
8
8
  /** Execute a write statement (INSERT/UPDATE/DELETE). Returns { changes }. */
@@ -23,7 +23,7 @@ export interface RigourDB {
23
23
  }
24
24
  /**
25
25
  * Open (or create) the Rigour SQLite database.
26
- * Returns null if sqlite3 is not available.
26
+ * Returns null where this Node has no node:sqlite.
27
27
  */
28
28
  export declare function openDatabase(dbPath?: string): Promise<RigourDB | null>;
29
29
  /**
@@ -44,8 +44,6 @@ export declare function getDatabaseSize(): number;
44
44
  * Reset the database — delete and recreate from scratch.
45
45
  */
46
46
  export declare function resetDatabase(): void;
47
- /**
48
- * Check if SQLite is available (sqlite3 installed)
49
- */
47
+ /** Whether this Node has SQLite (node:sqlite, Node 22.13 or later). */
50
48
  export declare function isSQLiteAvailable(): boolean;
51
49
  export { RIGOUR_DIR, DB_PATH };
@@ -3,44 +3,38 @@
3
3
  * Single file at ~/.rigour/rigour.db stores all scan history, findings,
4
4
  * learned patterns, and feedback. ACID-safe, portable, queryable.
5
5
  *
6
- * Uses node-sqlite3 (async, widely supported, no native build issues).
7
- * All public APIs are async. Graceful degradation if sqlite3 not installed.
6
+ * Uses Node's built-in SQLite (node:sqlite): nothing to install or compile, and no native addon to
7
+ * crash a worker thread. All public APIs are async. Where node:sqlite is missing (Node before
8
+ * 22.13), storage is off and every caller degrades as before.
8
9
  */
9
10
  import path from 'path';
10
11
  import fs from 'fs-extra';
11
12
  import { createRequire } from 'module';
12
13
  import { rigourUserDir } from '../utils/user-state.js';
13
- // ---------------------------------------------------------------------------
14
- // Optional dynamic import of sqlite3
15
- // ---------------------------------------------------------------------------
16
- let sqlite3Module = null;
17
- let _resolved = false;
18
- function loadSqlite3() {
19
- if (_resolved)
20
- return sqlite3Module;
21
- _resolved = true;
22
- // Try multiple resolution paths:
23
- // 1. Relative to this package (works when sqlite3 is installed in monorepo)
24
- // 2. Relative to cwd (works when user installs sqlite3 in their project)
25
- // 3. Relative to global node_modules (works for global installs)
26
- const searchPaths = [
27
- import.meta.url,
28
- `file://${process.cwd()}/package.json`,
29
- `file://${path.join(rigourUserDir(), 'package.json')}`,
30
- ];
31
- for (const base of searchPaths) {
32
- try {
33
- const req = createRequire(base);
34
- sqlite3Module = req('sqlite3');
35
- if (sqlite3Module)
36
- return sqlite3Module;
37
- }
38
- catch {
39
- // Try next path
40
- }
14
+ let sqliteModule;
15
+ /**
16
+ * node:sqlite, or null where this Node has none. Node 22 prints an "experimental feature" warning on
17
+ * the first load; it says nothing a Rigour user can act on, so that one warning is not shown.
18
+ */
19
+ function loadSqlite() {
20
+ if (sqliteModule !== undefined)
21
+ return sqliteModule;
22
+ const emitWarning = process.emitWarning;
23
+ process.emitWarning = ((warning, ...rest) => {
24
+ if (String(warning?.message ?? warning).startsWith('SQLite is an experimental feature'))
25
+ return;
26
+ return emitWarning.call(process, warning, ...rest);
27
+ });
28
+ try {
29
+ sqliteModule = createRequire(import.meta.url)('node:sqlite');
30
+ }
31
+ catch {
32
+ sqliteModule = null;
33
+ }
34
+ finally {
35
+ process.emitWarning = emitWarning;
41
36
  }
42
- sqlite3Module = null;
43
- return sqlite3Module;
37
+ return sqliteModule;
44
38
  }
45
39
  // RIGOUR_HOME when set (tests, sandboxes) so nothing but a real run touches ~/.rigour.
46
40
  const RIGOUR_DIR = rigourUserDir();
@@ -254,52 +248,41 @@ CREATE INDEX IF NOT EXISTS idx_repositories_last_seen ON repositories(last_seen)
254
248
  CREATE INDEX IF NOT EXISTS idx_interaction_events_repo_created ON interaction_events(repository_id, created_at);
255
249
  CREATE INDEX IF NOT EXISTS idx_interaction_events_request ON interaction_events(request_id);
256
250
  `;
251
+ /**
252
+ * A value as SQLite stores it. node:sqlite refuses what sqlite3 converted silently, so the same
253
+ * conversions happen here: undefined is NULL, a boolean is 1 or 0, a Date is its epoch milliseconds.
254
+ */
255
+ function bindable(value) {
256
+ if (value === undefined)
257
+ return null;
258
+ if (typeof value === 'boolean')
259
+ return value ? 1 : 0;
260
+ if (value instanceof Date)
261
+ return value.getTime();
262
+ return value;
263
+ }
264
+ /** Rows as plain objects (node:sqlite returns them without a prototype). */
265
+ function plain(row) {
266
+ return row && typeof row === 'object' ? { ...row } : row;
267
+ }
257
268
  function wrapDatabase(raw) {
269
+ const prepare = (sql) => raw.prepare(sql);
258
270
  const db = {
259
- run(sql, ...params) {
260
- return new Promise((resolve, reject) => {
261
- raw.run(sql, ...params, function (err) {
262
- if (err)
263
- return reject(err);
264
- resolve({ changes: this.changes, lastID: this.lastID });
265
- });
266
- });
271
+ async run(sql, ...params) {
272
+ const result = prepare(sql).run(...params.map(bindable));
273
+ return { changes: Number(result.changes), lastID: Number(result.lastInsertRowid) };
267
274
  },
268
- get(sql, ...params) {
269
- return new Promise((resolve, reject) => {
270
- raw.get(sql, ...params, (err, row) => {
271
- if (err)
272
- return reject(err);
273
- resolve(row);
274
- });
275
- });
275
+ async get(sql, ...params) {
276
+ return plain(prepare(sql).get(...params.map(bindable)));
276
277
  },
277
- all(sql, ...params) {
278
- return new Promise((resolve, reject) => {
279
- raw.all(sql, ...params, (err, rows) => {
280
- if (err)
281
- return reject(err);
282
- resolve(rows || []);
283
- });
284
- });
278
+ async all(sql, ...params) {
279
+ return prepare(sql).all(...params.map(bindable)).map(plain);
285
280
  },
286
- exec(sql) {
287
- return new Promise((resolve, reject) => {
288
- raw.exec(sql, (err) => {
289
- if (err)
290
- return reject(err);
291
- resolve();
292
- });
293
- });
281
+ async exec(sql) {
282
+ raw.exec(sql);
294
283
  },
295
- close() {
296
- return new Promise((resolve, reject) => {
297
- raw.close((err) => {
298
- if (err)
299
- return reject(err);
300
- resolve();
301
- });
302
- });
284
+ async close() {
285
+ raw.close();
303
286
  },
304
287
  async transaction(fn) {
305
288
  await db.exec('BEGIN TRANSACTION');
@@ -318,22 +301,15 @@ function wrapDatabase(raw) {
318
301
  }
319
302
  /**
320
303
  * Open (or create) the Rigour SQLite database.
321
- * Returns null if sqlite3 is not available.
304
+ * Returns null where this Node has no node:sqlite.
322
305
  */
323
306
  export async function openDatabase(dbPath) {
324
- const sqlite3 = loadSqlite3();
325
- if (!sqlite3)
307
+ const sqlite = loadSqlite();
308
+ if (!sqlite)
326
309
  return null;
327
310
  const resolvedPath = dbPath || DB_PATH;
328
311
  fs.ensureDirSync(path.dirname(resolvedPath));
329
- const raw = await new Promise((resolve, reject) => {
330
- const instance = new sqlite3.Database(resolvedPath, (err) => {
331
- if (err)
332
- return reject(err);
333
- resolve(instance);
334
- });
335
- });
336
- const db = wrapDatabase(raw);
312
+ const db = wrapDatabase(new sqlite.DatabaseSync(resolvedPath));
337
313
  // Wait for a lock instead of failing at once: a hook and an MCP server often open the database
338
314
  // at the same moment, and without this the second open fails with SQLITE_BUSY.
339
315
  await db.exec('PRAGMA busy_timeout = 5000');
@@ -528,7 +504,7 @@ async function enableWal(db, attempts = 10) {
528
504
  return;
529
505
  }
530
506
  catch (error) {
531
- if (!/SQLITE_BUSY/.test(String(error)) || attempt >= attempts)
507
+ if (!/SQLITE_BUSY|database is locked/i.test(String(error)) || attempt >= attempts)
532
508
  throw error;
533
509
  await new Promise(resolve => setTimeout(resolve, 25 * attempt));
534
510
  }
@@ -555,8 +531,7 @@ async function addColumnIfMissing(db, table, column, alterSql) {
555
531
  * Compact the database — prune old data, reclaim disk space.
556
532
  */
557
533
  export async function compactDatabase(retainDays = 90) {
558
- const sqlite3 = loadSqlite3();
559
- if (!sqlite3)
534
+ if (!loadSqlite())
560
535
  return { pruned: 0, patternsDecayed: 0, sizeBefore: 0, sizeAfter: 0 };
561
536
  const resolvedPath = DB_PATH;
562
537
  const sizeBefore = fs.existsSync(resolvedPath) ? fs.statSync(resolvedPath).size : 0;
@@ -604,10 +579,8 @@ export function resetDatabase() {
604
579
  if (fs.existsSync(DB_PATH + '-shm'))
605
580
  fs.removeSync(DB_PATH + '-shm');
606
581
  }
607
- /**
608
- * Check if SQLite is available (sqlite3 installed)
609
- */
582
+ /** Whether this Node has SQLite (node:sqlite, Node 22.13 or later). */
610
583
  export function isSQLiteAvailable() {
611
- return loadSqlite3() !== null;
584
+ return loadSqlite() !== null;
612
585
  }
613
586
  export { RIGOUR_DIR, DB_PATH };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { isSQLiteAvailable, openDatabase } from './db.js';
6
+ let dir;
7
+ let db;
8
+ beforeEach(async () => {
9
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rigour-db-'));
10
+ db = (await openDatabase(path.join(dir, 'rigour.db')));
11
+ });
12
+ afterEach(async () => {
13
+ await db?.close();
14
+ fs.rmSync(dir, { recursive: true, force: true });
15
+ });
16
+ describe('the database (node:sqlite)', () => {
17
+ it('is built in: no package to install', () => {
18
+ expect(isSQLiteAvailable()).toBe(true);
19
+ expect(db).not.toBeNull();
20
+ });
21
+ it('stores what callers pass the way sqlite3 did: undefined as NULL, booleans as 1 or 0, a Date as epoch ms', async () => {
22
+ await db.exec('CREATE TABLE t (a, b, c, d)');
23
+ const when = new Date('2026-01-02T03:04:05Z');
24
+ const written = await db.run('INSERT INTO t VALUES (?, ?, ?, ?)', undefined, true, false, when);
25
+ expect(written).toEqual({ changes: 1, lastID: 1 });
26
+ const row = await db.get('SELECT * FROM t');
27
+ expect(row).toEqual({ a: null, b: 1, c: 0, d: when.getTime() });
28
+ expect(Object.getPrototypeOf(row)).toBe(Object.prototype); // a plain object, as callers spread and compare
29
+ expect(await db.get('SELECT * FROM t WHERE a = 1')).toBeUndefined();
30
+ });
31
+ it('rolls a failed transaction back whole', async () => {
32
+ await db.exec('CREATE TABLE t (a UNIQUE)');
33
+ await expect(db.transaction(async (tx) => {
34
+ await tx.run('INSERT INTO t VALUES (?)', 1);
35
+ await tx.run('INSERT INTO t VALUES (?)', 1); // violates UNIQUE
36
+ })).rejects.toThrow(/UNIQUE/);
37
+ expect(await db.all('SELECT * FROM t')).toEqual([]);
38
+ });
39
+ it('opens an existing database again with its schema and data, in WAL mode', async () => {
40
+ await db.run("INSERT INTO meta (key, value) VALUES ('probe', 'kept')");
41
+ await db.close();
42
+ db = (await openDatabase(path.join(dir, 'rigour.db')));
43
+ expect(await db.get("SELECT value FROM meta WHERE key = 'probe'")).toEqual({ value: 'kept' });
44
+ expect(await db.get('PRAGMA journal_mode')).toEqual({ journal_mode: 'wal' });
45
+ });
46
+ });
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "6.7.1",
3
+ "version": "6.7.2",
4
4
  "description": "Rigour's review engine: deterministic gates on changed lines, rules and lessons learned from your team's fixes, and per-check precision from what you fix versus dismiss, across TypeScript, JavaScript, Python, Go, Ruby and C#.",
5
5
  "engines": {
6
- "node": ">=22"
6
+ "node": ">=22.13"
7
7
  },
8
8
  "license": "MIT",
9
9
  "homepage": "https://rigour.run",
@@ -71,13 +71,12 @@
71
71
  "optionalDependencies": {
72
72
  "@anthropic-ai/sdk": "^0.30.1",
73
73
  "pg": "^8.16.3",
74
- "sqlite3": "^5.1.7",
75
- "openai": "^4.104.0",
76
- "@rigour-labs/brain-darwin-arm64": "6.7.1",
77
- "@rigour-labs/brain-darwin-x64": "6.7.1",
78
- "@rigour-labs/brain-linux-arm64": "6.7.1",
79
- "@rigour-labs/brain-linux-x64": "6.7.1",
80
- "@rigour-labs/brain-win-x64": "6.7.1"
74
+ "openai": "^5.23.2",
75
+ "@rigour-labs/brain-darwin-x64": "6.7.2",
76
+ "@rigour-labs/brain-linux-x64": "6.7.2",
77
+ "@rigour-labs/brain-win-x64": "6.7.2",
78
+ "@rigour-labs/brain-darwin-arm64": "6.7.2",
79
+ "@rigour-labs/brain-linux-arm64": "6.7.2"
81
80
  },
82
81
  "devDependencies": {
83
82
  "@types/fs-extra": "^11.0.4",