@rigour-labs/core 4.0.5 → 4.1.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.
Files changed (49) hide show
  1. package/dist/gates/ast-handlers/typescript.js +39 -12
  2. package/dist/gates/ast-handlers/universal.js +9 -3
  3. package/dist/gates/ast.js +15 -1
  4. package/dist/gates/ast.test.d.ts +1 -0
  5. package/dist/gates/ast.test.js +112 -0
  6. package/dist/gates/content.d.ts +5 -0
  7. package/dist/gates/content.js +66 -7
  8. package/dist/gates/content.test.d.ts +1 -0
  9. package/dist/gates/content.test.js +73 -0
  10. package/dist/gates/context-window-artifacts.d.ts +1 -0
  11. package/dist/gates/context-window-artifacts.js +10 -3
  12. package/dist/gates/context.d.ts +1 -0
  13. package/dist/gates/context.js +29 -8
  14. package/dist/gates/deep-analysis.js +2 -2
  15. package/dist/gates/deprecated-apis.d.ts +1 -0
  16. package/dist/gates/deprecated-apis.js +15 -2
  17. package/dist/gates/hallucinated-imports.d.ts +14 -0
  18. package/dist/gates/hallucinated-imports.js +267 -60
  19. package/dist/gates/hallucinated-imports.test.js +164 -1
  20. package/dist/gates/inconsistent-error-handling.d.ts +1 -0
  21. package/dist/gates/inconsistent-error-handling.js +12 -1
  22. package/dist/gates/phantom-apis.d.ts +2 -0
  23. package/dist/gates/phantom-apis.js +28 -3
  24. package/dist/gates/phantom-apis.test.js +14 -0
  25. package/dist/gates/promise-safety.d.ts +2 -0
  26. package/dist/gates/promise-safety.js +31 -9
  27. package/dist/gates/runner.js +8 -2
  28. package/dist/gates/runner.test.d.ts +1 -0
  29. package/dist/gates/runner.test.js +65 -0
  30. package/dist/gates/security-patterns.d.ts +1 -0
  31. package/dist/gates/security-patterns.js +22 -6
  32. package/dist/gates/security-patterns.test.js +18 -0
  33. package/dist/hooks/templates.d.ts +1 -1
  34. package/dist/hooks/templates.js +12 -12
  35. package/dist/inference/executable.d.ts +6 -0
  36. package/dist/inference/executable.js +29 -0
  37. package/dist/inference/executable.test.d.ts +1 -0
  38. package/dist/inference/executable.test.js +41 -0
  39. package/dist/inference/model-manager.d.ts +3 -1
  40. package/dist/inference/model-manager.js +76 -8
  41. package/dist/inference/model-manager.test.d.ts +1 -0
  42. package/dist/inference/model-manager.test.js +24 -0
  43. package/dist/inference/sidecar-provider.d.ts +1 -0
  44. package/dist/inference/sidecar-provider.js +91 -15
  45. package/dist/services/context-engine.js +1 -1
  46. package/dist/templates/universal-config.js +3 -3
  47. package/dist/types/index.js +3 -3
  48. package/dist/utils/scanner.js +6 -0
  49. package/package.json +6 -6
@@ -4,21 +4,74 @@
4
4
  */
5
5
  import path from 'path';
6
6
  import fs from 'fs-extra';
7
+ import { createHash } from 'crypto';
7
8
  import { RIGOUR_DIR } from '../storage/db.js';
8
9
  import { MODELS } from './types.js';
9
10
  const MODELS_DIR = path.join(RIGOUR_DIR, 'models');
11
+ const SHA256_RE = /^[a-f0-9]{64}$/i;
12
+ function getModelMetadataPath(tier) {
13
+ return path.join(MODELS_DIR, MODELS[tier].filename + '.meta.json');
14
+ }
15
+ function isValidMetadata(raw) {
16
+ return !!raw &&
17
+ typeof raw.sha256 === 'string' &&
18
+ SHA256_RE.test(raw.sha256) &&
19
+ typeof raw.sizeBytes === 'number' &&
20
+ typeof raw.verifiedAt === 'string' &&
21
+ typeof raw.sourceUrl === 'string';
22
+ }
23
+ export function extractSha256FromEtag(etag) {
24
+ if (!etag)
25
+ return null;
26
+ const normalized = etag.replace(/^W\//i, '').replace(/^"+|"+$/g, '').trim();
27
+ return SHA256_RE.test(normalized) ? normalized.toLowerCase() : null;
28
+ }
29
+ export async function hashFileSha256(filePath) {
30
+ const hash = createHash('sha256');
31
+ const stream = fs.createReadStream(filePath);
32
+ for await (const chunk of stream) {
33
+ hash.update(chunk);
34
+ }
35
+ return hash.digest('hex');
36
+ }
37
+ async function writeModelMetadata(tier, metadata) {
38
+ const metadataPath = getModelMetadataPath(tier);
39
+ await fs.writeJson(metadataPath, metadata, { spaces: 2 });
40
+ }
41
+ async function readModelMetadata(tier) {
42
+ const metadataPath = getModelMetadataPath(tier);
43
+ if (!(await fs.pathExists(metadataPath))) {
44
+ return null;
45
+ }
46
+ try {
47
+ const raw = await fs.readJson(metadataPath);
48
+ return isValidMetadata(raw) ? raw : null;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
10
54
  /**
11
55
  * Check if a model is already downloaded and valid.
12
56
  */
13
- export function isModelCached(tier) {
57
+ export async function isModelCached(tier) {
14
58
  const model = MODELS[tier];
15
59
  const modelPath = path.join(MODELS_DIR, model.filename);
16
- if (!fs.existsSync(modelPath))
60
+ if (!(await fs.pathExists(modelPath)))
17
61
  return false;
18
- // Basic size check (within 10% tolerance)
19
- const stat = fs.statSync(modelPath);
62
+ const metadata = await readModelMetadata(tier);
63
+ if (!metadata)
64
+ return false;
65
+ // Size check + "changed since verification" check.
66
+ const stat = await fs.stat(modelPath);
20
67
  const tolerance = model.sizeBytes * 0.1;
21
- return stat.size > model.sizeBytes - tolerance;
68
+ if (stat.size <= model.sizeBytes - tolerance)
69
+ return false;
70
+ if (metadata.sizeBytes !== stat.size)
71
+ return false;
72
+ if (new Date(metadata.verifiedAt).getTime() < stat.mtimeMs)
73
+ return false;
74
+ return true;
22
75
  }
23
76
  /**
24
77
  * Get the path to a cached model.
@@ -42,7 +95,7 @@ export async function downloadModel(tier, onProgress) {
42
95
  const tempPath = destPath + '.download';
43
96
  fs.ensureDirSync(MODELS_DIR);
44
97
  // Already cached
45
- if (isModelCached(tier)) {
98
+ if (await isModelCached(tier)) {
46
99
  onProgress?.(`Model ${model.name} already cached`, 100);
47
100
  return destPath;
48
101
  }
@@ -52,18 +105,22 @@ export async function downloadModel(tier, onProgress) {
52
105
  if (!response.ok) {
53
106
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
54
107
  }
108
+ const expectedSha256 = extractSha256FromEtag(response.headers.get('etag'));
55
109
  const contentLength = parseInt(response.headers.get('content-length') || '0', 10);
56
110
  const reader = response.body?.getReader();
57
111
  if (!reader)
58
112
  throw new Error('No response body');
59
113
  const writeStream = fs.createWriteStream(tempPath);
114
+ const hash = createHash('sha256');
60
115
  let downloaded = 0;
61
116
  let lastProgressPercent = 0;
62
117
  while (true) {
63
118
  const { done, value } = await reader.read();
64
119
  if (done)
65
120
  break;
66
- writeStream.write(Buffer.from(value));
121
+ const chunk = Buffer.from(value);
122
+ writeStream.write(chunk);
123
+ hash.update(chunk);
67
124
  downloaded += value.length;
68
125
  if (contentLength > 0) {
69
126
  const percent = Math.round((downloaded / contentLength) * 100);
@@ -78,8 +135,19 @@ export async function downloadModel(tier, onProgress) {
78
135
  writeStream.on('finish', resolve);
79
136
  writeStream.on('error', reject);
80
137
  });
138
+ const actualSha256 = hash.digest('hex');
139
+ if (expectedSha256 && actualSha256 !== expectedSha256) {
140
+ throw new Error(`Model checksum mismatch for ${model.name}: expected ${expectedSha256}, got ${actualSha256}`);
141
+ }
81
142
  // Atomic rename
82
143
  fs.renameSync(tempPath, destPath);
144
+ await writeModelMetadata(tier, {
145
+ sha256: actualSha256,
146
+ sizeBytes: downloaded,
147
+ verifiedAt: new Date().toISOString(),
148
+ sourceUrl: model.url,
149
+ sourceEtag: response.headers.get('etag') || undefined,
150
+ });
83
151
  onProgress?.(`Model ${model.name} ready`, 100);
84
152
  return destPath;
85
153
  }
@@ -93,7 +161,7 @@ export async function downloadModel(tier, onProgress) {
93
161
  * Ensure a model is available, downloading if needed.
94
162
  */
95
163
  export async function ensureModel(tier, onProgress) {
96
- if (isModelCached(tier)) {
164
+ if (await isModelCached(tier)) {
97
165
  return getModelPath(tier);
98
166
  }
99
167
  return downloadModel(tier, onProgress);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import fs from 'fs-extra';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { extractSha256FromEtag, hashFileSha256 } from './model-manager.js';
6
+ describe('model manager integrity helpers', () => {
7
+ it('extracts sha256 digest from a strong ETag', () => {
8
+ const digest = 'a'.repeat(64);
9
+ expect(extractSha256FromEtag(`"${digest}"`)).toBe(digest);
10
+ expect(extractSha256FromEtag(`W/"${digest}"`)).toBe(digest);
11
+ });
12
+ it('returns null for non-sha ETags', () => {
13
+ expect(extractSha256FromEtag('"not-a-digest"')).toBeNull();
14
+ expect(extractSha256FromEtag(null)).toBeNull();
15
+ });
16
+ it('hashes file contents with sha256', async () => {
17
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-model-hash-'));
18
+ const filePath = path.join(dir, 'sample.gguf');
19
+ await fs.writeFile(filePath, 'rigour-model-check');
20
+ const digest = await hashFileSha256(filePath);
21
+ expect(digest).toBe('e123266ea4b37a81948a0a844dd58eddfc81737aa6fdf9dafc818fd23bae75f0');
22
+ await fs.remove(dir);
23
+ });
24
+ });
@@ -11,6 +11,7 @@ export declare class SidecarProvider implements InferenceProvider {
11
11
  analyze(prompt: string, options?: InferenceOptions): Promise<string>;
12
12
  dispose(): void;
13
13
  private getPlatformKey;
14
+ private getPlatformPackageName;
14
15
  private resolveBinaryPath;
15
16
  private installSidecarBinary;
16
17
  }
@@ -10,7 +10,9 @@ import os from 'os';
10
10
  import fs from 'fs-extra';
11
11
  import { createRequire } from 'module';
12
12
  import { ensureModel, isModelCached, getModelInfo } from './model-manager.js';
13
+ import { ensureExecutableBinary } from './executable.js';
13
14
  const execFileAsync = promisify(execFile);
15
+ const SIDECAR_INSTALL_DIR = path.join(os.homedir(), '.rigour', 'sidecar');
14
16
  /** Platform → npm package mapping */
15
17
  const PLATFORM_PACKAGES = {
16
18
  'darwin-arm64': '@rigour-labs/brain-darwin-arm64',
@@ -34,11 +36,10 @@ export class SidecarProvider {
34
36
  return binary !== null;
35
37
  }
36
38
  async setup(onProgress) {
37
- const platformKey = this.getPlatformKey();
38
- const packageName = PLATFORM_PACKAGES[platformKey];
39
+ const packageName = this.getPlatformPackageName();
39
40
  // 1. Check/resolve binary
40
41
  this.binaryPath = await this.resolveBinaryPath();
41
- // Auto-bootstrap local sidecar once before failing.
42
+ // Auto-bootstrap local sidecar when missing.
42
43
  if (!this.binaryPath && packageName) {
43
44
  const installed = await this.installSidecarBinary(packageName, onProgress);
44
45
  if (installed) {
@@ -47,12 +48,31 @@ export class SidecarProvider {
47
48
  }
48
49
  if (!this.binaryPath) {
49
50
  onProgress?.('⚠ Inference engine not found. Install @rigour-labs/brain-* or add llama-cli to PATH');
50
- const installHint = packageName || `@rigour-labs/brain-${platformKey}`;
51
+ const installHint = packageName || `@rigour-labs/brain-${this.getPlatformKey()}`;
51
52
  throw new Error(`Sidecar binary not found. Run: npm install ${installHint}`);
52
53
  }
54
+ let executableCheck = ensureExecutableBinary(this.binaryPath);
55
+ // If the discovered binary is not executable, try a managed reinstall once.
56
+ if (!executableCheck.ok && packageName) {
57
+ onProgress?.('⚠ Inference engine is present but not executable. Reinstalling managed sidecar...');
58
+ const installed = await this.installSidecarBinary(packageName, onProgress);
59
+ if (installed) {
60
+ const refreshedPath = await this.resolveBinaryPath();
61
+ if (refreshedPath) {
62
+ this.binaryPath = refreshedPath;
63
+ executableCheck = ensureExecutableBinary(this.binaryPath);
64
+ }
65
+ }
66
+ }
67
+ if (!executableCheck.ok) {
68
+ throw new Error(`Sidecar binary is not executable: ${this.binaryPath}. Run: chmod +x "${this.binaryPath}"`);
69
+ }
70
+ if (executableCheck.fixed) {
71
+ onProgress?.('✓ Fixed execute permission for inference engine');
72
+ }
53
73
  onProgress?.('✓ Inference engine ready');
54
74
  // 2. Ensure model is downloaded
55
- if (!isModelCached(this.tier)) {
75
+ if (!(await isModelCached(this.tier))) {
56
76
  const modelInfo = getModelInfo(this.tier);
57
77
  onProgress?.(`⬇ Downloading analysis model (${modelInfo.sizeHuman})...`);
58
78
  }
@@ -86,9 +106,45 @@ export class SidecarProvider {
86
106
  maxBuffer: 10 * 1024 * 1024, // 10MB
87
107
  env: { ...process.env, LLAMA_LOG_DISABLE: '1' },
88
108
  };
89
- const { stdout } = process.platform === 'win32' && this.binaryPath.endsWith('.cmd')
90
- ? await execFileAsync('cmd.exe', ['/d', '/s', '/c', [this.binaryPath, ...args].map(quoteCmdArg).join(' ')], execOptions)
91
- : await execFileAsync(this.binaryPath, args, execOptions);
109
+ const runInference = async () => {
110
+ return process.platform === 'win32' && this.binaryPath.endsWith('.cmd')
111
+ ? await execFileAsync('cmd.exe', ['/d', '/s', '/c', [this.binaryPath, ...args].map(quoteCmdArg).join(' ')], execOptions)
112
+ : await execFileAsync(this.binaryPath, args, execOptions);
113
+ };
114
+ let stdout;
115
+ try {
116
+ ({ stdout } = await runInference());
117
+ }
118
+ catch (error) {
119
+ // One retry path for stale/bad file mode in packaged installs.
120
+ if (error?.code === 'EACCES') {
121
+ const check = ensureExecutableBinary(this.binaryPath);
122
+ if (check.ok) {
123
+ ({ stdout } = await runInference());
124
+ }
125
+ else {
126
+ const packageName = this.getPlatformPackageName();
127
+ if (packageName) {
128
+ const installed = await this.installSidecarBinary(packageName);
129
+ if (installed) {
130
+ const refreshedPath = await this.resolveBinaryPath();
131
+ if (refreshedPath) {
132
+ this.binaryPath = refreshedPath;
133
+ const refreshedCheck = ensureExecutableBinary(this.binaryPath);
134
+ if (refreshedCheck.ok) {
135
+ ({ stdout } = await runInference());
136
+ return stdout.trim();
137
+ }
138
+ }
139
+ }
140
+ }
141
+ throw error;
142
+ }
143
+ }
144
+ else {
145
+ throw error;
146
+ }
147
+ }
92
148
  // llama.cpp sometimes outputs to stderr for diagnostics — ignore
93
149
  return stdout.trim();
94
150
  }
@@ -96,6 +152,9 @@ export class SidecarProvider {
96
152
  if (error.killed) {
97
153
  throw new Error(`Inference timed out after ${(options?.timeout || 60000) / 1000}s`);
98
154
  }
155
+ if (error?.code === 'EACCES') {
156
+ throw new Error(`Inference binary is not executable: ${this.binaryPath}. Run: chmod +x "${this.binaryPath}"`);
157
+ }
99
158
  throw new Error(`Inference failed: ${error.message}`);
100
159
  }
101
160
  }
@@ -107,11 +166,25 @@ export class SidecarProvider {
107
166
  getPlatformKey() {
108
167
  return `${os.platform()}-${os.arch()}`;
109
168
  }
169
+ getPlatformPackageName() {
170
+ const platformKey = this.getPlatformKey();
171
+ return PLATFORM_PACKAGES[platformKey];
172
+ }
110
173
  async resolveBinaryPath() {
111
174
  const platformKey = this.getPlatformKey();
112
175
  // Strategy 1: Check @rigour-labs/brain-{platform} optional dependency
113
176
  const packageName = PLATFORM_PACKAGES[platformKey];
114
177
  if (packageName) {
178
+ // Prefer Rigour-managed sidecar install root first to avoid brittle global/homebrew layouts.
179
+ const managedPath = path.join(SIDECAR_INSTALL_DIR, 'node_modules', ...packageName.split('/'), 'bin', 'rigour-brain');
180
+ const managedCandidates = os.platform() === 'win32'
181
+ ? [managedPath + '.exe', managedPath + '.cmd', managedPath]
182
+ : [managedPath];
183
+ for (const managedBinPath of managedCandidates) {
184
+ if (await fs.pathExists(managedBinPath)) {
185
+ return managedBinPath;
186
+ }
187
+ }
115
188
  try {
116
189
  const require = createRequire(import.meta.url);
117
190
  const pkgJsonPath = require.resolve(path.posix.join(packageName, 'package.json'));
@@ -165,9 +238,10 @@ export class SidecarProvider {
165
238
  }
166
239
  }
167
240
  // Strategy 3: Check PATH for llama-cli (llama.cpp CLI)
241
+ const locator = os.platform() === 'win32' ? 'where' : 'which';
168
242
  try {
169
- const { stdout } = await execFileAsync('which', ['llama-cli']);
170
- const llamaPath = stdout.trim();
243
+ const { stdout } = await execFileAsync(locator, ['llama-cli']);
244
+ const llamaPath = stdout.split(/\r?\n/).map(s => s.trim()).find(Boolean) || '';
171
245
  if (llamaPath && await fs.pathExists(llamaPath)) {
172
246
  return llamaPath;
173
247
  }
@@ -179,9 +253,10 @@ export class SidecarProvider {
179
253
  const altNames = ['llama-cli', 'llama', 'main'];
180
254
  for (const name of altNames) {
181
255
  try {
182
- const { stdout } = await execFileAsync('which', [name]);
183
- if (stdout.trim())
184
- return stdout.trim();
256
+ const { stdout } = await execFileAsync(locator, [name]);
257
+ const resolved = stdout.split(/\r?\n/).map(s => s.trim()).find(Boolean);
258
+ if (resolved && await fs.pathExists(resolved))
259
+ return resolved;
185
260
  }
186
261
  catch {
187
262
  // Continue
@@ -192,8 +267,9 @@ export class SidecarProvider {
192
267
  async installSidecarBinary(packageName, onProgress) {
193
268
  onProgress?.(`⬇ Inference engine missing. Attempting automatic install: ${packageName}`);
194
269
  try {
195
- await execFileAsync('npm', ['install', '--no-save', '--no-package-lock', packageName], {
196
- cwd: process.cwd(),
270
+ await fs.ensureDir(SIDECAR_INSTALL_DIR);
271
+ await execFileAsync(os.platform() === 'win32' ? 'npm.cmd' : 'npm', ['install', '--no-save', '--no-package-lock', '--prefix', SIDECAR_INSTALL_DIR, packageName], {
272
+ cwd: SIDECAR_INSTALL_DIR,
197
273
  timeout: 120000,
198
274
  maxBuffer: 10 * 1024 * 1024,
199
275
  });
@@ -47,7 +47,7 @@ export class ContextEngine {
47
47
  anchors,
48
48
  metadata: {
49
49
  scannedFiles,
50
- detectedCasing: 'unknown', // TODO: Implement casing discovery
50
+ detectedCasing: 'unknown', // Placeholder until casing discovery is implemented
51
51
  }
52
52
  };
53
53
  }
@@ -113,9 +113,9 @@ export const UNIVERSAL_CONFIG = {
113
113
  },
114
114
  context_window_artifacts: {
115
115
  enabled: true,
116
- min_file_lines: 100,
117
- degradation_threshold: 0.4,
118
- signals_required: 2,
116
+ min_file_lines: 180,
117
+ degradation_threshold: 0.55,
118
+ signals_required: 4,
119
119
  },
120
120
  promise_safety: {
121
121
  enabled: true,
@@ -125,9 +125,9 @@ export const GatesSchema = z.object({
125
125
  }).optional().default({}),
126
126
  context_window_artifacts: z.object({
127
127
  enabled: z.boolean().optional().default(true),
128
- min_file_lines: z.number().optional().default(100),
129
- degradation_threshold: z.number().min(0).max(1).optional().default(0.4),
130
- signals_required: z.number().optional().default(2),
128
+ min_file_lines: z.number().optional().default(180),
129
+ degradation_threshold: z.number().min(0).max(1).optional().default(0.55),
130
+ signals_required: z.number().optional().default(4),
131
131
  }).optional().default({}),
132
132
  promise_safety: z.object({
133
133
  enabled: z.boolean().optional().default(true),
@@ -6,6 +6,12 @@ export class FileScanner {
6
6
  static DEFAULT_IGNORE = [
7
7
  '**/node_modules/**',
8
8
  '**/dist/**',
9
+ '**/studio-dist/**',
10
+ '**/.next/**',
11
+ '**/coverage/**',
12
+ '**/out/**',
13
+ '**/target/**',
14
+ '**/examples/**',
9
15
  '**/package-lock.json',
10
16
  '**/pnpm-lock.yaml',
11
17
  '**/.git/**',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "4.0.5",
3
+ "version": "4.1.0",
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.0.5",
63
- "@rigour-labs/brain-darwin-x64": "4.0.5",
64
- "@rigour-labs/brain-linux-x64": "4.0.5",
65
- "@rigour-labs/brain-linux-arm64": "4.0.5",
66
- "@rigour-labs/brain-win-x64": "4.0.5"
62
+ "@rigour-labs/brain-darwin-arm64": "4.1.0",
63
+ "@rigour-labs/brain-darwin-x64": "4.1.0",
64
+ "@rigour-labs/brain-win-x64": "4.1.0",
65
+ "@rigour-labs/brain-linux-x64": "4.1.0",
66
+ "@rigour-labs/brain-linux-arm64": "4.1.0"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@types/better-sqlite3": "^7.6.12",