@rigour-labs/core 6.3.0 → 6.3.1
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.
|
@@ -9,7 +9,7 @@ const home = vi.hoisted(() => {
|
|
|
9
9
|
return p.join(o.tmpdir(), `rigour-models-${process.pid}-${Date.now()}`);
|
|
10
10
|
});
|
|
11
11
|
vi.mock('../storage/db.js', () => ({ RIGOUR_DIR: home }));
|
|
12
|
-
const { downloadModel, getCachedModel } = await import('./model-manager.js');
|
|
12
|
+
const { checkForUpdates, downloadModel, getCachedModel, normalizeModelVersion } = await import('./model-manager.js');
|
|
13
13
|
const { FALLBACK_MODELS, MODELS } = await import('./types.js');
|
|
14
14
|
const modelsDir = path.join(home, 'models');
|
|
15
15
|
const PAYLOAD = Buffer.from('gguf-model-bytes');
|
|
@@ -22,7 +22,7 @@ describe('downloadModel', () => {
|
|
|
22
22
|
beforeEach(async () => {
|
|
23
23
|
await fs.remove(home);
|
|
24
24
|
// A fresh version check: no network call to Hugging Face.
|
|
25
|
-
await fs.outputJson(path.join(modelsDir, '.latest_version.json'), { version: '
|
|
25
|
+
await fs.outputJson(path.join(modelsDir, '.latest_version.json'), { format: 2, version: '5', checkedAt: new Date().toISOString() });
|
|
26
26
|
});
|
|
27
27
|
afterAll(async () => { await fs.remove(home); });
|
|
28
28
|
it('falls back to the stock model when the fine-tuned model is unavailable, and says so', async () => {
|
|
@@ -39,6 +39,19 @@ describe('downloadModel', () => {
|
|
|
39
39
|
const meta = await fs.readJson(`${modelPath}.meta.json`);
|
|
40
40
|
expect(meta).toMatchObject({ sha256: PAYLOAD_SHA, sizeBytes: PAYLOAD.length, sourceUrl: FALLBACK_MODELS.lite.url });
|
|
41
41
|
});
|
|
42
|
+
it('retries the fine-tuned model once a day when only the stock fallback is cached', async () => {
|
|
43
|
+
const failing = (url) => (url.includes('rigour-labs/') ? unauthorized : ok(PAYLOAD, PAYLOAD_SHA));
|
|
44
|
+
const stockPath = await downloadModel('lite', undefined, failing);
|
|
45
|
+
const requested = [];
|
|
46
|
+
const again = await downloadModel('lite', undefined, (url) => { requested.push(url); return unauthorized; });
|
|
47
|
+
expect(requested).toEqual([]); // failed less than a day ago: keep the fallback, no request
|
|
48
|
+
expect(again).toBe(stockPath);
|
|
49
|
+
await fs.writeJson(path.join(modelsDir, '.fine-tuned-unavailable-lite.json'), { failedAt: '2000-01-01T00:00:00Z', url: MODELS.lite.url });
|
|
50
|
+
const fineTuned = await downloadModel('lite', undefined, (url) => { requested.push(url); return ok(PAYLOAD, PAYLOAD_SHA); });
|
|
51
|
+
expect(requested).toEqual([MODELS.lite.url]);
|
|
52
|
+
expect(path.basename(fineTuned)).toBe(MODELS.lite.filename);
|
|
53
|
+
expect(await fs.pathExists(path.join(modelsDir, '.fine-tuned-unavailable-lite.json'))).toBe(false);
|
|
54
|
+
});
|
|
42
55
|
it('rejects a checksum mismatch and deletes the partial download', async () => {
|
|
43
56
|
const get = () => ok(PAYLOAD, 'f'.repeat(64));
|
|
44
57
|
await expect(downloadModel('lite', undefined, get)).rejects.toThrow('Checksum mismatch');
|
|
@@ -65,9 +78,48 @@ describe('getCachedModel', () => {
|
|
|
65
78
|
const cached = await getCachedModel('deep');
|
|
66
79
|
expect(cached).toMatchObject({ fallback: true, info: { filename: FALLBACK_MODELS.deep.filename } });
|
|
67
80
|
});
|
|
81
|
+
it('accepts a verified fine-tuned file smaller than the size estimate', async () => {
|
|
82
|
+
await cacheFile(MODELS.lite.filename, 397_807_360); // the published v5 lite model
|
|
83
|
+
expect(await getCachedModel('lite')).toMatchObject({ fallback: false, info: { filename: MODELS.lite.filename } });
|
|
84
|
+
});
|
|
85
|
+
it('accepts a file verified within the same millisecond it was written', async () => {
|
|
86
|
+
const file = path.join(modelsDir, FALLBACK_MODELS.deep.filename);
|
|
87
|
+
await cacheFile(FALLBACK_MODELS.deep.filename, 10);
|
|
88
|
+
const written = new Date('2026-01-01T00:00:00.000Z');
|
|
89
|
+
await fs.utimes(file, written, written.getTime() / 1000 + 0.0007); // mtime 0.7 ms into the same millisecond
|
|
90
|
+
await fs.writeJson(`${file}.meta.json`, { sha256: 'a'.repeat(64), sizeBytes: 10, verifiedAt: written.toISOString(), sourceUrl: 'https://x' });
|
|
91
|
+
expect(await getCachedModel('deep')).toMatchObject({ fallback: true });
|
|
92
|
+
});
|
|
93
|
+
it('rejects a file modified after it was verified', async () => {
|
|
94
|
+
const file = path.join(modelsDir, FALLBACK_MODELS.deep.filename);
|
|
95
|
+
await cacheFile(FALLBACK_MODELS.deep.filename, 10);
|
|
96
|
+
await fs.writeJson(`${file}.meta.json`, { sha256: 'a'.repeat(64), sizeBytes: 10, verifiedAt: '2000-01-01T00:00:00.000Z', sourceUrl: 'https://x' });
|
|
97
|
+
expect(await getCachedModel('deep')).toBeNull();
|
|
98
|
+
});
|
|
68
99
|
it('ignores a file without verified metadata', async () => {
|
|
69
100
|
await fs.ensureDir(modelsDir);
|
|
70
101
|
await fs.writeFile(path.join(modelsDir, FALLBACK_MODELS.deep.filename), 'partial');
|
|
71
102
|
expect(await getCachedModel('deep')).toBeNull();
|
|
72
103
|
});
|
|
73
104
|
});
|
|
105
|
+
describe('model versions', () => {
|
|
106
|
+
beforeEach(async () => { await fs.remove(home); vi.unstubAllGlobals(); });
|
|
107
|
+
it('keeps published version strings as they are', () => {
|
|
108
|
+
expect(normalizeModelVersion(5)).toBe('5');
|
|
109
|
+
expect(normalizeModelVersion('5')).toBe('5');
|
|
110
|
+
expect(normalizeModelVersion('v2.1.0')).toBe('2.1.0');
|
|
111
|
+
expect(normalizeModelVersion('5.0')).toBeNull();
|
|
112
|
+
expect(normalizeModelVersion({})).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
it('names the published v5 repositories by default', () => {
|
|
115
|
+
expect(MODELS.lite.url).toBe('https://huggingface.co/rigour-labs/rigour-lite-v5-gguf/resolve/main/rigour-lite-v5-q4_k_m.gguf');
|
|
116
|
+
expect(MODELS.deep.url).toBe('https://huggingface.co/rigour-labs/rigour-deep-v5-gguf/resolve/main/rigour-deep-v5-q4_k_m.gguf');
|
|
117
|
+
});
|
|
118
|
+
it('ignores a version cache from the old SemVer conversion and re-reads the published version', async () => {
|
|
119
|
+
await fs.outputJson(path.join(modelsDir, '.latest_version.json'), { version: '5.0.0', checkedAt: new Date().toISOString() });
|
|
120
|
+
vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, json: async () => ({ version: 5 }) })));
|
|
121
|
+
expect(await checkForUpdates()).toBe('5');
|
|
122
|
+
expect(await fs.readJson(path.join(modelsDir, '.latest_version.json'))).toMatchObject({ format: 2, version: '5' });
|
|
123
|
+
expect(MODELS.deep.filename).toBe('rigour-deep-v5-q4_k_m.gguf');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -26,6 +26,12 @@ export declare function getModelPath(tier: ModelTier): string;
|
|
|
26
26
|
* Get model info for a tier.
|
|
27
27
|
*/
|
|
28
28
|
export declare function getModelInfo(tier: ModelTier): ModelInfo;
|
|
29
|
+
/**
|
|
30
|
+
* The version exactly as published. Integer versions (the RLAIF pipeline
|
|
31
|
+
* before SemVer) are published as `v5`, SemVer versions as `v2.1.0`; either
|
|
32
|
+
* way the string goes into the repository and file names unchanged.
|
|
33
|
+
*/
|
|
34
|
+
export declare function normalizeModelVersion(raw: unknown): string | null;
|
|
29
35
|
/**
|
|
30
36
|
* Check HuggingFace for a newer model version (like antivirus signature updates).
|
|
31
37
|
* Reads latest_version.json from the RLAIF dataset repo. Non-blocking — if the
|
|
@@ -8,6 +8,7 @@ import { createHash } from 'crypto';
|
|
|
8
8
|
import { RIGOUR_DIR } from '../storage/db.js';
|
|
9
9
|
import { downloadToFile } from './http-download.js';
|
|
10
10
|
import { MODELS, FALLBACK_MODELS, VERSION_CHECK_URL, BUNDLED_MODEL_VERSION, updateModelVersion } from './types.js';
|
|
11
|
+
import { Logger } from '../utils/logger.js';
|
|
11
12
|
const MODELS_DIR = path.join(RIGOUR_DIR, 'models');
|
|
12
13
|
const VERSION_CACHE_PATH = path.join(MODELS_DIR, '.latest_version.json');
|
|
13
14
|
const VERSION_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // Check once per day
|
|
@@ -63,13 +64,14 @@ async function isFileCached(model) {
|
|
|
63
64
|
const metadata = await readModelMeta(model.filename);
|
|
64
65
|
if (!metadata)
|
|
65
66
|
return false;
|
|
67
|
+
// The metadata is written only after the checksum passed, so its exact size is
|
|
68
|
+
// the completeness check; ModelInfo sizes are display estimates that differ by version.
|
|
66
69
|
const stat = await fs.stat(modelPath);
|
|
67
|
-
const tolerance = model.sizeBytes * 0.1;
|
|
68
|
-
if (stat.size <= model.sizeBytes - tolerance)
|
|
69
|
-
return false;
|
|
70
70
|
if (metadata.sizeBytes !== stat.size)
|
|
71
71
|
return false;
|
|
72
|
-
|
|
72
|
+
// verifiedAt has millisecond precision and mtimeMs a sub-millisecond fraction: compare
|
|
73
|
+
// whole milliseconds, or a file verified within the same millisecond looks modified.
|
|
74
|
+
if (new Date(metadata.verifiedAt).getTime() < Math.floor(stat.mtimeMs))
|
|
73
75
|
return false;
|
|
74
76
|
return true;
|
|
75
77
|
}
|
|
@@ -161,6 +163,27 @@ async function downloadFromUrl(model, onProgress, get) {
|
|
|
161
163
|
onProgress?.(`Model ${model.name} ready`, 100);
|
|
162
164
|
return destPath;
|
|
163
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Version cache format. Format 1 turned a published integer version into
|
|
168
|
+
* SemVer ("5" -> "5.0.0"), which names repositories that were never published,
|
|
169
|
+
* so those caches are ignored.
|
|
170
|
+
*/
|
|
171
|
+
const VERSION_CACHE_FORMAT = 2;
|
|
172
|
+
/** How long a failed fine-tuned download keeps the stock fallback before retrying. */
|
|
173
|
+
const FINE_TUNED_RETRY_MS = 24 * 60 * 60 * 1000;
|
|
174
|
+
/**
|
|
175
|
+
* The version exactly as published. Integer versions (the RLAIF pipeline
|
|
176
|
+
* before SemVer) are published as `v5`, SemVer versions as `v2.1.0`; either
|
|
177
|
+
* way the string goes into the repository and file names unchanged.
|
|
178
|
+
*/
|
|
179
|
+
export function normalizeModelVersion(raw) {
|
|
180
|
+
if (typeof raw === 'number' && Number.isInteger(raw) && raw > 0)
|
|
181
|
+
return String(raw);
|
|
182
|
+
if (typeof raw !== 'string')
|
|
183
|
+
return null;
|
|
184
|
+
const value = raw.trim().replace(/^v/, '');
|
|
185
|
+
return /^\d+$/.test(value) || /^\d+\.\d+\.\d+$/.test(value) ? value : null;
|
|
186
|
+
}
|
|
164
187
|
/**
|
|
165
188
|
* Check HuggingFace for a newer model version (like antivirus signature updates).
|
|
166
189
|
* Reads latest_version.json from the RLAIF dataset repo. Non-blocking — if the
|
|
@@ -175,8 +198,8 @@ export async function checkForUpdates(onProgress) {
|
|
|
175
198
|
if (await fs.pathExists(VERSION_CACHE_PATH)) {
|
|
176
199
|
const cached = await fs.readJson(VERSION_CACHE_PATH);
|
|
177
200
|
const age = Date.now() - new Date(cached.checkedAt).getTime();
|
|
178
|
-
|
|
179
|
-
|
|
201
|
+
const v = cached.format === VERSION_CACHE_FORMAT ? normalizeModelVersion(cached.version) : null;
|
|
202
|
+
if (age < VERSION_CHECK_INTERVAL_MS && v) {
|
|
180
203
|
updateModelVersion(v);
|
|
181
204
|
return v;
|
|
182
205
|
}
|
|
@@ -193,13 +216,10 @@ export async function checkForUpdates(onProgress) {
|
|
|
193
216
|
clearTimeout(timeout);
|
|
194
217
|
if (response.ok) {
|
|
195
218
|
const data = await response.json();
|
|
196
|
-
|
|
197
|
-
let rawVersion = data.version ?? BUNDLED_MODEL_VERSION;
|
|
198
|
-
const latestVersion = typeof rawVersion === 'number'
|
|
199
|
-
? `${rawVersion}.0.0`
|
|
200
|
-
: String(rawVersion);
|
|
219
|
+
const latestVersion = normalizeModelVersion(data.version) ?? BUNDLED_MODEL_VERSION;
|
|
201
220
|
// Cache the result locally
|
|
202
221
|
await fs.writeJson(VERSION_CACHE_PATH, {
|
|
222
|
+
format: VERSION_CACHE_FORMAT,
|
|
203
223
|
version: latestVersion,
|
|
204
224
|
checkedAt: new Date().toISOString(),
|
|
205
225
|
source: 'huggingface',
|
|
@@ -226,22 +246,42 @@ export async function downloadModel(tier, onProgress, get) {
|
|
|
226
246
|
// Check for newer model version (non-blocking, cached 24h)
|
|
227
247
|
await checkForUpdates(onProgress);
|
|
228
248
|
const cached = await getCachedModel(tier);
|
|
229
|
-
if (cached) {
|
|
249
|
+
if (cached && !cached.fallback) {
|
|
230
250
|
onProgress?.(`Model ${cached.info.name} already cached`, 100);
|
|
231
251
|
return cached.path;
|
|
232
252
|
}
|
|
253
|
+
// Only the stock fallback is cached: try the fine-tuned model again, at most once a day,
|
|
254
|
+
// so one failed download does not pin a user to the stock model forever.
|
|
255
|
+
if (cached && !(await fineTunedRetryDue(tier)))
|
|
256
|
+
return cached.path;
|
|
233
257
|
const model = MODELS[tier];
|
|
234
258
|
onProgress?.(`Downloading ${model.name} (${model.sizeHuman})...`, 0);
|
|
235
259
|
try {
|
|
236
|
-
|
|
260
|
+
const downloaded = await downloadFromUrl(model, onProgress, get);
|
|
261
|
+
await fs.remove(retryMarker(tier)).catch(() => { });
|
|
262
|
+
return downloaded;
|
|
237
263
|
}
|
|
238
264
|
catch (error) {
|
|
239
265
|
const fallback = FALLBACK_MODELS[tier];
|
|
240
|
-
if (fallback
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
266
|
+
if (!fallback || fallback.url === model.url)
|
|
267
|
+
throw error;
|
|
268
|
+
const reason = error?.message ?? 'unknown error';
|
|
269
|
+
Logger.warn(`Fine-tuned model ${model.name} could not be downloaded from ${model.url} (${reason}); using the stock ${fallback.name}. Deep findings will be less accurate.`);
|
|
270
|
+
onProgress?.(`Fine-tuned model unavailable (${reason}); using stock ${fallback.name}`, 0);
|
|
271
|
+
await fs.writeJson(retryMarker(tier), { failedAt: new Date().toISOString(), url: model.url, reason }).catch(() => { });
|
|
272
|
+
return cached?.path ?? downloadFromUrl(fallback, onProgress, get);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function retryMarker(tier) {
|
|
276
|
+
return path.join(MODELS_DIR, `.fine-tuned-unavailable-${tier}.json`);
|
|
277
|
+
}
|
|
278
|
+
async function fineTunedRetryDue(tier) {
|
|
279
|
+
try {
|
|
280
|
+
const marker = await fs.readJson(retryMarker(tier));
|
|
281
|
+
return Date.now() - new Date(marker.failedAt).getTime() >= FINE_TUNED_RETRY_MS || marker.url !== MODELS[tier].url;
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return true;
|
|
245
285
|
}
|
|
246
286
|
}
|
|
247
287
|
/**
|
|
@@ -86,12 +86,16 @@ export interface ModelInfo {
|
|
|
86
86
|
* At startup, model-manager checks HF for the latest version and downloads
|
|
87
87
|
* it automatically (like antivirus signature updates).
|
|
88
88
|
*
|
|
89
|
-
*
|
|
89
|
+
* The fallback is the last version actually published: v5, from the RLAIF
|
|
90
|
+
* pipeline's integer versions. SemVer versions (MAJOR.MINOR.PATCH) are
|
|
91
|
+
* published under their SemVer names.
|
|
92
|
+
*
|
|
93
|
+
* Version format: SemVer (MAJOR.MINOR.PATCH), or a legacy integer
|
|
90
94
|
* - MAJOR: Training data format change, base model change, pipeline architecture
|
|
91
95
|
* - MINOR: New training repos, updated dataset, hyperparameter improvements
|
|
92
96
|
* - PATCH: Bug fixes, retraining with same data/format
|
|
93
97
|
*/
|
|
94
|
-
export declare const BUNDLED_MODEL_VERSION = "
|
|
98
|
+
export declare const BUNDLED_MODEL_VERSION = "5";
|
|
95
99
|
/** HuggingFace dataset repo where latest_version.json lives */
|
|
96
100
|
export declare const VERSION_CHECK_URL = "https://huggingface.co/datasets/rigour-labs/rigour-rlaif-data/resolve/main/latest_version.json";
|
|
97
101
|
/** Build model info for a given tier and version */
|
package/dist/inference/types.js
CHANGED
|
@@ -5,19 +5,23 @@
|
|
|
5
5
|
* At startup, model-manager checks HF for the latest version and downloads
|
|
6
6
|
* it automatically (like antivirus signature updates).
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* The fallback is the last version actually published: v5, from the RLAIF
|
|
9
|
+
* pipeline's integer versions. SemVer versions (MAJOR.MINOR.PATCH) are
|
|
10
|
+
* published under their SemVer names.
|
|
11
|
+
*
|
|
12
|
+
* Version format: SemVer (MAJOR.MINOR.PATCH), or a legacy integer
|
|
9
13
|
* - MAJOR: Training data format change, base model change, pipeline architecture
|
|
10
14
|
* - MINOR: New training repos, updated dataset, hyperparameter improvements
|
|
11
15
|
* - PATCH: Bug fixes, retraining with same data/format
|
|
12
16
|
*/
|
|
13
|
-
export const BUNDLED_MODEL_VERSION = '
|
|
17
|
+
export const BUNDLED_MODEL_VERSION = '5';
|
|
14
18
|
/** HuggingFace dataset repo where latest_version.json lives */
|
|
15
19
|
export const VERSION_CHECK_URL = 'https://huggingface.co/datasets/rigour-labs/rigour-rlaif-data/resolve/main/latest_version.json';
|
|
16
20
|
/** Build model info for a given tier and version */
|
|
17
21
|
export function buildModelInfo(tier, version) {
|
|
18
22
|
const meta = {
|
|
19
23
|
deep: { base: 'Qwen2.5-Coder-1.5B', size: 900_000_000, sizeH: '900MB' },
|
|
20
|
-
lite: { base: 'Qwen2.5-Coder-0.5B', size:
|
|
24
|
+
lite: { base: 'Qwen2.5-Coder-0.5B', size: 400_000_000, sizeH: '400MB' },
|
|
21
25
|
legacy: { base: 'Qwen2.5-Coder-0.5B', size: 350_000_000, sizeH: '350MB' },
|
|
22
26
|
};
|
|
23
27
|
const m = meta[tier];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/core",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.1",
|
|
4
4
|
"description": "AI-native quality gate engine with local Bayesian learning. AST analysis, drift detection, Fix Packet generation, and agent self-healing across TypeScript, JavaScript, Python, Go, Ruby, and C#.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://rigour.run",
|
|
@@ -67,11 +67,11 @@
|
|
|
67
67
|
"pg": "^8.16.3",
|
|
68
68
|
"sqlite3": "^5.1.7",
|
|
69
69
|
"openai": "^4.104.0",
|
|
70
|
-
"@rigour-labs/brain-darwin-
|
|
71
|
-
"@rigour-labs/brain-darwin-
|
|
72
|
-
"@rigour-labs/brain-linux-
|
|
73
|
-
"@rigour-labs/brain-
|
|
74
|
-
"@rigour-labs/brain-
|
|
70
|
+
"@rigour-labs/brain-darwin-x64": "6.3.1",
|
|
71
|
+
"@rigour-labs/brain-darwin-arm64": "6.3.1",
|
|
72
|
+
"@rigour-labs/brain-linux-x64": "6.3.1",
|
|
73
|
+
"@rigour-labs/brain-linux-arm64": "6.3.1",
|
|
74
|
+
"@rigour-labs/brain-win-x64": "6.3.1"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
77
|
"@types/fs-extra": "^11.0.4",
|