@rigour-labs/core 6.2.4 → 6.3.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.
- package/README.md +2 -0
- package/dist/deep/code-context.d.ts +28 -0
- package/dist/deep/code-context.js +93 -0
- package/dist/deep/code-context.test.d.ts +1 -0
- package/dist/deep/code-context.test.js +48 -0
- package/dist/deep/code-pass.d.ts +15 -0
- package/dist/deep/code-pass.js +44 -0
- package/dist/deep/code-review-prompt.d.ts +17 -0
- package/dist/deep/code-review-prompt.js +30 -0
- package/dist/deep/code-review-prompt.test.d.ts +1 -0
- package/dist/deep/code-review-prompt.test.js +20 -0
- package/dist/deep/code-verifier.d.ts +19 -0
- package/dist/deep/code-verifier.js +48 -0
- package/dist/deep/code-verifier.test.d.ts +1 -0
- package/dist/deep/code-verifier.test.js +46 -0
- package/dist/deep/fact-extractor.d.ts +7 -1
- package/dist/deep/fact-extractor.js +16 -8
- package/dist/deep/fact-extractor.test.js +34 -1
- package/dist/deep/facts-pass.d.ts +24 -0
- package/dist/deep/facts-pass.js +69 -0
- package/dist/deep/index.d.ts +6 -1
- package/dist/deep/index.js +5 -1
- package/dist/deep/parse-findings.d.ts +12 -0
- package/dist/deep/parse-findings.js +73 -0
- package/dist/deep/parse-findings.test.d.ts +1 -0
- package/dist/deep/parse-findings.test.js +25 -0
- package/dist/deep/prompts.d.ts +1 -1
- package/dist/deep/prompts.js +7 -8
- package/dist/deep/prompts.test.js +15 -0
- package/dist/discovery-signals.d.ts +27 -0
- package/dist/discovery-signals.js +96 -0
- package/dist/discovery.d.ts +1 -2
- package/dist/discovery.js +36 -49
- package/dist/discovery.test.js +84 -71
- package/dist/gates/context.js +7 -0
- package/dist/gates/deep-analysis.d.ts +33 -8
- package/dist/gates/deep-analysis.js +169 -306
- package/dist/gates/deep-analysis.test.d.ts +1 -0
- package/dist/gates/deep-analysis.test.js +86 -0
- package/dist/gates/deep-intent.d.ts +3 -0
- package/dist/gates/deep-intent.js +57 -0
- package/dist/gates/deep-intent.test.d.ts +1 -0
- package/dist/gates/deep-intent.test.js +51 -0
- package/dist/gates/deep-runner.d.ts +15 -0
- package/dist/gates/deep-runner.js +66 -0
- package/dist/gates/dependency.js +4 -0
- package/dist/gates/deprecated-apis-ast.d.ts +8 -0
- package/dist/gates/deprecated-apis-ast.js +25 -0
- package/dist/gates/deprecated-apis-rules-node.js +2 -8
- package/dist/gates/deprecated-apis.js +5 -1
- package/dist/gates/deprecated-apis.test.js +30 -0
- package/dist/gates/frontend-secret-exposure.js +5 -1
- package/dist/gates/frontend-secret-exposure.test.js +20 -0
- package/dist/gates/hallucinated-imports/framework-modules.d.ts +14 -0
- package/dist/gates/hallucinated-imports/framework-modules.js +75 -0
- package/dist/gates/hallucinated-imports/index.js +14 -6
- package/dist/gates/hallucinated-imports/js-resolver.d.ts +16 -12
- package/dist/gates/hallucinated-imports/js-resolver.js +65 -153
- package/dist/gates/hallucinated-imports/ts-path-target.js +16 -0
- package/dist/gates/hallucinated-imports/tsconfig-paths.d.ts +16 -0
- package/dist/gates/hallucinated-imports/tsconfig-paths.js +120 -0
- package/dist/gates/hallucinated-imports-sveltekit.test.d.ts +1 -0
- package/dist/gates/hallucinated-imports-sveltekit.test.js +108 -0
- package/dist/gates/logic-drift.js +7 -1
- package/dist/gates/runner.js +10 -40
- package/dist/gates/runner.test.js +37 -25
- package/dist/gates/scoped-gates.test.d.ts +1 -0
- package/dist/gates/scoped-gates.test.js +52 -0
- package/dist/gates/semantic-bugs.d.ts +23 -0
- package/dist/gates/semantic-bugs.js +48 -0
- package/dist/gates/semantic-bugs.test.d.ts +1 -0
- package/dist/gates/semantic-bugs.test.js +70 -0
- package/dist/gates/side-effect-analysis/index.js +11 -1
- package/dist/gates/side-effect-analysis.test.d.ts +1 -0
- package/dist/gates/side-effect-analysis.test.js +162 -0
- package/dist/gates/side-effect-helpers/pattern-detection.js +35 -7
- package/dist/gates/side-effect-helpers/resource-tracking.js +17 -10
- package/dist/gates/side-effect-helpers/scope-analysis.d.ts +6 -0
- package/dist/gates/side-effect-helpers/scope-analysis.js +20 -0
- package/dist/gates/style-drift.js +3 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.js +10 -2
- package/dist/inference/http-download.d.ts +22 -0
- package/dist/inference/http-download.js +132 -0
- package/dist/inference/http-download.test.d.ts +1 -0
- package/dist/inference/http-download.test.js +109 -0
- package/dist/inference/index.d.ts +5 -1
- package/dist/inference/index.js +2 -1
- package/dist/inference/llama-engine.d.ts +27 -0
- package/dist/inference/llama-engine.js +152 -0
- package/dist/inference/llama-engine.test.d.ts +1 -0
- package/dist/inference/llama-engine.test.js +51 -0
- package/dist/inference/llama-process.d.ts +72 -0
- package/dist/inference/llama-process.js +126 -0
- package/dist/inference/llama-process.test.d.ts +1 -0
- package/dist/inference/llama-process.test.js +61 -0
- package/dist/inference/model-download.test.d.ts +1 -0
- package/dist/inference/model-download.test.js +73 -0
- package/dist/inference/model-manager.d.ts +14 -1
- package/dist/inference/model-manager.js +65 -80
- package/dist/inference/sidecar-provider.d.ts +19 -10
- package/dist/inference/sidecar-provider.js +125 -342
- package/dist/inference/types.d.ts +2 -0
- package/dist/semantic/ast.d.ts +29 -0
- package/dist/semantic/ast.js +122 -0
- package/dist/semantic/benchmark.d.ts +16 -0
- package/dist/semantic/benchmark.js +31 -0
- package/dist/semantic/benchmark.test.d.ts +1 -0
- package/dist/semantic/benchmark.test.js +18 -0
- package/dist/semantic/engine.d.ts +8 -0
- package/dist/semantic/engine.js +45 -0
- package/dist/semantic/intent/benchmark.d.ts +31 -0
- package/dist/semantic/intent/benchmark.js +74 -0
- package/dist/semantic/intent/intent-check.d.ts +43 -0
- package/dist/semantic/intent/intent-check.js +72 -0
- package/dist/semantic/intent/intent.test.d.ts +1 -0
- package/dist/semantic/intent/intent.test.js +46 -0
- package/dist/semantic/intent/parallel-reads.d.ts +23 -0
- package/dist/semantic/intent/parallel-reads.js +59 -0
- package/dist/semantic/learn/benchmark.d.ts +20 -0
- package/dist/semantic/learn/benchmark.js +71 -0
- package/dist/semantic/learn/compile.d.ts +4 -0
- package/dist/semantic/learn/compile.js +56 -0
- package/dist/semantic/learn/conditions.d.ts +22 -0
- package/dist/semantic/learn/conditions.js +93 -0
- package/dist/semantic/learn/extract.d.ts +30 -0
- package/dist/semantic/learn/extract.js +122 -0
- package/dist/semantic/learn/git-trees.d.ts +8 -0
- package/dist/semantic/learn/git-trees.js +32 -0
- package/dist/semantic/learn/identity.d.ts +18 -0
- package/dist/semantic/learn/identity.js +97 -0
- package/dist/semantic/learn/learn.d.ts +25 -0
- package/dist/semantic/learn/learn.js +109 -0
- package/dist/semantic/learn/learn.test.d.ts +1 -0
- package/dist/semantic/learn/learn.test.js +67 -0
- package/dist/semantic/learn/store.d.ts +4 -0
- package/dist/semantic/learn/store.js +39 -0
- package/dist/semantic/learn/types.d.ts +468 -0
- package/dist/semantic/learn/types.js +55 -0
- package/dist/semantic/origins.d.ts +42 -0
- package/dist/semantic/origins.js +149 -0
- package/dist/semantic/origins.test.d.ts +1 -0
- package/dist/semantic/origins.test.js +77 -0
- package/dist/semantic/program.d.ts +13 -0
- package/dist/semantic/program.js +62 -0
- package/dist/semantic/rules/credential-redirect.d.ts +2 -0
- package/dist/semantic/rules/credential-redirect.js +209 -0
- package/dist/semantic/rules/degraded-response-cached.d.ts +2 -0
- package/dist/semantic/rules/degraded-response-cached.js +271 -0
- package/dist/semantic/rules/in-memory-aggregation.d.ts +2 -0
- package/dist/semantic/rules/in-memory-aggregation.js +179 -0
- package/dist/semantic/summaries.d.ts +18 -0
- package/dist/semantic/summaries.js +33 -0
- package/dist/semantic/types.d.ts +32 -0
- package/dist/semantic/types.js +1 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/team-diagnostics.d.ts +22 -0
- package/dist/storage/team-diagnostics.js +58 -0
- package/dist/storage/team-diagnostics.test.d.ts +1 -0
- package/dist/storage/team-diagnostics.test.js +51 -0
- package/dist/storage/team-schema.d.ts +6 -0
- package/dist/storage/team-schema.js +82 -0
- package/dist/storage/team-store.js +25 -72
- package/dist/templates/universal-config.js +4 -2
- package/dist/types/index.d.ts +114 -14
- package/dist/types/index.js +16 -2
- package/dist/utils/deep-status.d.ts +7 -0
- package/dist/utils/deep-status.js +11 -0
- package/dist/utils/diff.d.ts +3 -0
- package/dist/utils/diff.js +52 -0
- package/dist/utils/diff.test.d.ts +1 -0
- package/dist/utils/diff.test.js +38 -0
- package/dist/utils/scanner.js +13 -2
- package/dist/utils/scanner.test.js +22 -8
- package/dist/utils/scope.d.ts +15 -0
- package/dist/utils/scope.js +51 -0
- package/dist/utils/scope.test.d.ts +1 -0
- package/dist/utils/scope.test.js +38 -0
- package/package.json +6 -6
|
@@ -71,7 +71,7 @@ export function pathsOverlap(watchPath, writePath) {
|
|
|
71
71
|
// ═══════════════════════════════════════════════════════════════════
|
|
72
72
|
// LOOP & RECURSION ANALYSIS — Context-aware body extraction
|
|
73
73
|
// ═══════════════════════════════════════════════════════════════════
|
|
74
|
-
import { findBlockEndBrace, findBlockEndIndent } from './scope-analysis.js';
|
|
74
|
+
import { findBlockEndBrace, findBlockEndIndent, findExpressionEnd } from './scope-analysis.js';
|
|
75
75
|
/**
|
|
76
76
|
* Extract loop body with correct scope tracking.
|
|
77
77
|
* Uses brace/indent matching (not just "next N lines").
|
|
@@ -122,13 +122,24 @@ export function extractFunctionDefs(lines, lang) {
|
|
|
122
122
|
? findBlockEndIndent(lines, i)
|
|
123
123
|
: lang === 'rb'
|
|
124
124
|
? findBlockEndRuby(lines, i)
|
|
125
|
-
: findBlockEndBrace(lines, i);
|
|
125
|
+
: isExpressionArrow(lines, i, stripped, m.index) ? findExpressionEnd(lines, i) : findBlockEndBrace(lines, i);
|
|
126
126
|
defs.push({ name, start: i, end, params: lines[i] });
|
|
127
127
|
break;
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
return defs;
|
|
131
131
|
}
|
|
132
|
+
/** An arrow whose body is an expression, not a `{ ... }` block. */
|
|
133
|
+
function isExpressionArrow(lines, line, stripped, matchAt) {
|
|
134
|
+
const arrow = stripped.indexOf('=>', matchAt);
|
|
135
|
+
if (arrow < 0)
|
|
136
|
+
return false;
|
|
137
|
+
const rest = stripped.slice(arrow + 2).trim();
|
|
138
|
+
if (rest !== '')
|
|
139
|
+
return !rest.startsWith('{');
|
|
140
|
+
const next = lines.slice(line + 1).find(l => l.trim() !== '');
|
|
141
|
+
return next !== undefined && !next.trim().startsWith('{');
|
|
142
|
+
}
|
|
132
143
|
function getFuncDefPatterns(lang) {
|
|
133
144
|
switch (lang) {
|
|
134
145
|
case 'py': return [/^\s*(?:async\s+)?def\s+(\w+)\s*\(/];
|
|
@@ -297,11 +308,24 @@ export function hasCatchWithContinue(body, lang) {
|
|
|
297
308
|
// ═══════════════════════════════════════════════════════════════════
|
|
298
309
|
// PROCESS & TIMER OPERATIONS
|
|
299
310
|
// ═══════════════════════════════════════════════════════════════════
|
|
311
|
+
/**
|
|
312
|
+
* Drop a trailing line comment (and whole comment lines) so prose such as
|
|
313
|
+
* "ported from the fork (verified …)" is not read as code. Strings are
|
|
314
|
+
* already blanked, so `//` inside a URL string is gone before this runs.
|
|
315
|
+
*/
|
|
316
|
+
function stripLineComment(line, lang) {
|
|
317
|
+
const trimmed = line.trim();
|
|
318
|
+
if (trimmed.startsWith('*') || trimmed.startsWith('/*'))
|
|
319
|
+
return '';
|
|
320
|
+
const marker = lang === 'py' || lang === 'rb' ? '#' : '//';
|
|
321
|
+
const at = line.indexOf(marker);
|
|
322
|
+
return at === -1 ? line : line.slice(0, at);
|
|
323
|
+
}
|
|
300
324
|
/**
|
|
301
325
|
* Check if a line contains a process spawn call.
|
|
302
326
|
*/
|
|
303
327
|
export function isProcessSpawn(line, lang) {
|
|
304
|
-
const stripped = stripStrings(line);
|
|
328
|
+
const stripped = stripLineComment(stripStrings(line), lang);
|
|
305
329
|
const patterns = getSpawnPatterns(lang);
|
|
306
330
|
for (const pat of patterns) {
|
|
307
331
|
const m = pat.exec(stripped);
|
|
@@ -314,9 +338,13 @@ function getSpawnPatterns(lang) {
|
|
|
314
338
|
switch (lang) {
|
|
315
339
|
case 'js':
|
|
316
340
|
case 'ts':
|
|
341
|
+
// A bare call (from a child_process import) or a call on a
|
|
342
|
+
// child_process namespace; `re.exec(src)` or `db.exec(sql)` is not a
|
|
343
|
+
// spawn. *Sync variants finish before returning, so they cannot orphan.
|
|
317
344
|
return [
|
|
318
|
-
|
|
319
|
-
|
|
345
|
+
/(?<![.\w$])(?:spawn|exec|execFile|fork)\s*\(/,
|
|
346
|
+
/(?<![.\w$])(?:execa|execaCommand)\s*\(/,
|
|
347
|
+
/\b(?:child_process|childProcess|cp)\.(?:spawn|exec|execFile|fork)\s*\(/,
|
|
320
348
|
];
|
|
321
349
|
case 'py':
|
|
322
350
|
return [
|
|
@@ -354,9 +382,9 @@ function getTimerCreatePatterns(lang) {
|
|
|
354
382
|
switch (lang) {
|
|
355
383
|
case 'js':
|
|
356
384
|
case 'ts':
|
|
385
|
+
// setTimeout fires once and cannot run unbounded; only setInterval repeats.
|
|
357
386
|
return [
|
|
358
387
|
[/\bsetInterval\s*\(/, 'setInterval'],
|
|
359
|
-
[/\bsetTimeout\s*\(/, 'setTimeout'],
|
|
360
388
|
];
|
|
361
389
|
case 'py':
|
|
362
390
|
return [
|
|
@@ -410,7 +438,7 @@ export function getProcessCleanupPatterns(lang) {
|
|
|
410
438
|
case 'js':
|
|
411
439
|
case 'ts':
|
|
412
440
|
return [
|
|
413
|
-
/\.on\s*\(\s*['"](?:exit|close)['"]/,
|
|
441
|
+
/\.(?:on|once|addListener)\s*\(\s*['"](?:exit|close)['"]/,
|
|
414
442
|
/\.kill\s*\(/,
|
|
415
443
|
/\.disconnect\s*\(/,
|
|
416
444
|
];
|
|
@@ -39,8 +39,8 @@ export function extractVariableBinding(line, lang) {
|
|
|
39
39
|
const jsMatch = stripped.match(/^(?:const|let|var|final|auto|val)\s+(\w+)\s*=\s*/);
|
|
40
40
|
if (jsMatch)
|
|
41
41
|
return jsMatch[1];
|
|
42
|
-
// Member assignment: `this.timer = ...`, `self.timer = ...`
|
|
43
|
-
const memberMatch = stripped.match(/^(?:this|self)\.([\w]+)\s*=\s*/);
|
|
42
|
+
// Member assignment: `this.timer = ...`, `self.timer = ...`, `this.#timer = ...`
|
|
43
|
+
const memberMatch = stripped.match(/^(?:this|self)\.(#?[\w]+)\s*=\s*/);
|
|
44
44
|
if (memberMatch)
|
|
45
45
|
return memberMatch[1];
|
|
46
46
|
// Simple assignment: `timer = ...`
|
|
@@ -68,26 +68,33 @@ export function hasCleanupForVariable(lines, varName, scopeStart, scopeEnd, clea
|
|
|
68
68
|
const scope = lines.slice(scopeStart, scopeEnd);
|
|
69
69
|
for (let i = 0; i < scope.length; i++) {
|
|
70
70
|
const stripped = stripStrings(scope[i]);
|
|
71
|
-
// Check cleanup patterns that reference the specific variable
|
|
71
|
+
// Check cleanup patterns that reference the specific variable. Some
|
|
72
|
+
// patterns need the quoted event name (`.once('exit', …)`), which
|
|
73
|
+
// stripStrings blanks, so they are also tried on the raw line.
|
|
72
74
|
for (const pat of cleanupPatterns) {
|
|
73
|
-
if (!pat.test(stripped))
|
|
75
|
+
if (!pat.test(stripped) && !pat.test(scope[i]))
|
|
74
76
|
continue;
|
|
75
|
-
// The cleanup call should reference our variable
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (varRef.test(stripped))
|
|
77
|
+
// The cleanup call should reference our variable. Lookarounds, not
|
|
78
|
+
// \b, so a private field such as `#timer` matches too.
|
|
79
|
+
if (variableReference(varName).test(stripped))
|
|
79
80
|
return true;
|
|
80
81
|
// Also check method calls on the variable: timer.close(), timer.stop()
|
|
81
82
|
// The pattern might match a generic .close() — check if it's on our var
|
|
82
83
|
}
|
|
83
84
|
// Direct method cleanup on the variable: varName.close(), varName.Stop()
|
|
84
|
-
const escapedVar = varName
|
|
85
|
-
const methodCleanup = new RegExp(
|
|
85
|
+
const escapedVar = escapeRegExp(varName);
|
|
86
|
+
const methodCleanup = new RegExp(`(?<![\\w$#])${escapedVar}\\.(?:close|stop|destroy|kill|terminate|dispose|cancel|shutdown|unsubscribe|disconnect|end|release|Clear|Stop|Dispose|Close|Cancel)\\s*\\(`);
|
|
86
87
|
if (methodCleanup.test(stripped))
|
|
87
88
|
return true;
|
|
88
89
|
}
|
|
89
90
|
return false;
|
|
90
91
|
}
|
|
92
|
+
function escapeRegExp(value) {
|
|
93
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
94
|
+
}
|
|
95
|
+
function variableReference(varName) {
|
|
96
|
+
return new RegExp(`(?<![\\w$#])${escapeRegExp(varName)}(?![\\w$])`);
|
|
97
|
+
}
|
|
91
98
|
/**
|
|
92
99
|
* Check if a line is inside a cleanup/teardown context.
|
|
93
100
|
*
|
|
@@ -14,6 +14,12 @@ export declare function findEnclosingFunction(lines: string[], lineIdx: number,
|
|
|
14
14
|
* Find the end of a brace-delimited block starting at `start`.
|
|
15
15
|
*/
|
|
16
16
|
export declare function findBlockEndBrace(lines: string[], start: number): number;
|
|
17
|
+
/**
|
|
18
|
+
* Find the end of an expression-bodied arrow (`const f = (x) => x + 1;`):
|
|
19
|
+
* the first line where brackets balance and the expression does not
|
|
20
|
+
* continue. Scanning for a `{` instead would run into the next block.
|
|
21
|
+
*/
|
|
22
|
+
export declare function findExpressionEnd(lines: string[], start: number): number;
|
|
17
23
|
/**
|
|
18
24
|
* Find the end of an indentation-delimited block (Python).
|
|
19
25
|
*/
|
|
@@ -95,6 +95,26 @@ export function findBlockEndBrace(lines, start) {
|
|
|
95
95
|
}
|
|
96
96
|
return maxScan;
|
|
97
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Find the end of an expression-bodied arrow (`const f = (x) => x + 1;`):
|
|
100
|
+
* the first line where brackets balance and the expression does not
|
|
101
|
+
* continue. Scanning for a `{` instead would run into the next block.
|
|
102
|
+
*/
|
|
103
|
+
export function findExpressionEnd(lines, start) {
|
|
104
|
+
let depth = 0;
|
|
105
|
+
const maxScan = Math.min(lines.length, start + 300);
|
|
106
|
+
for (let j = start; j < maxScan; j++) {
|
|
107
|
+
const stripped = stripStrings(lines[j]);
|
|
108
|
+
depth += bracketDelta(stripped);
|
|
109
|
+
const continues = /(?:=>|[=+\-*/%&|?:,.(])\s*$/.test(stripped.trimEnd());
|
|
110
|
+
if (depth <= 0 && !continues)
|
|
111
|
+
return j + 1;
|
|
112
|
+
}
|
|
113
|
+
return maxScan;
|
|
114
|
+
}
|
|
115
|
+
function bracketDelta(text) {
|
|
116
|
+
return (text.match(/[([{]/g)?.length ?? 0) - (text.match(/[)\]}]/g)?.length ?? 0);
|
|
117
|
+
}
|
|
98
118
|
/**
|
|
99
119
|
* Find the end of an indentation-delimited block (Python).
|
|
100
120
|
*/
|
|
@@ -24,6 +24,7 @@ import { FileScanner } from '../utils/scanner.js';
|
|
|
24
24
|
import { Logger } from '../utils/logger.js';
|
|
25
25
|
import { languageAdapters } from './language-adapters/index.js';
|
|
26
26
|
import { extractComparableJsNames } from './js-style-context.js';
|
|
27
|
+
import { isScoped } from '../utils/scope.js';
|
|
27
28
|
import { TRY_CATCH_PATTERN, CATCH_PATTERN, RESULT_TYPE_PATTERN, NAMED_IMPORT_PATTERN, WILDCARD_IMPORT_PATTERN, SIDE_EFFECT_IMPORT_PATTERN, DEFAULT_IMPORT_PATTERN, countQuotes, } from './style-drift-rules.js';
|
|
28
29
|
import fs from 'fs-extra';
|
|
29
30
|
import path from 'path';
|
|
@@ -79,6 +80,8 @@ export class StyleDriftGate extends Gate {
|
|
|
79
80
|
Logger.debug('Failed to load style baseline, will create new one');
|
|
80
81
|
}
|
|
81
82
|
}
|
|
83
|
+
if (!baseline && isScoped(context.patterns))
|
|
84
|
+
return []; // never baseline from a subset of files
|
|
82
85
|
if (!baseline) {
|
|
83
86
|
// First scan: create per-language baseline
|
|
84
87
|
baseline = await this.computePerLanguageBaseline(context, filesByLang);
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,9 @@ export { RetryLoopBreakerGate } from './gates/retry-loop-breaker.js';
|
|
|
9
9
|
export { SideEffectAnalysisGate } from './gates/side-effect-analysis/index.js';
|
|
10
10
|
export { FrontendSecretExposureGate } from './gates/frontend-secret-exposure.js';
|
|
11
11
|
export * from './utils/logger.js';
|
|
12
|
+
export { normalizeScopePatterns, isScoped } from './utils/scope.js';
|
|
13
|
+
export { deepAnalysisError } from './utils/deep-status.js';
|
|
14
|
+
export { parseDiff, changedLinesByFile } from './utils/diff.js';
|
|
12
15
|
export { FileScanner } from './utils/scanner.js';
|
|
13
16
|
export * from './services/score-history.js';
|
|
14
17
|
export * from './hooks/index.js';
|
|
@@ -18,7 +21,9 @@ export { DeepAnalysisGate } from './gates/deep-analysis.js';
|
|
|
18
21
|
export { createProvider } from './inference/index.js';
|
|
19
22
|
export type { InferenceProvider, DeepFinding, DeepAnalysisResult, ModelTier } from './inference/types.js';
|
|
20
23
|
export { MODELS } from './inference/types.js';
|
|
21
|
-
export { isModelCached, getModelsDir, getModelInfo } from './inference/model-manager.js';
|
|
24
|
+
export { isModelCached, getModelsDir, getModelInfo, getCachedModel, ensureModel } from './inference/model-manager.js';
|
|
25
|
+
export { installLlamaEngine, probeBinary, managedEnginePath, LLAMA_RELEASE_TAG } from './inference/llama-engine.js';
|
|
26
|
+
export { SidecarProvider } from './inference/sidecar-provider.js';
|
|
22
27
|
export { extractFacts, factsToPromptString } from './deep/fact-extractor.js';
|
|
23
28
|
export { openDatabase, isSQLiteAvailable, compactDatabase, getDatabaseSize, resetDatabase, insertScan, insertFindings, getRecentScans, getScoreTrendFromDB, getTopIssues, reinforcePattern, getStrongPatterns } from './storage/index.js';
|
|
24
29
|
export type { RigourDB, CompactResult } from './storage/index.js';
|
|
@@ -26,7 +31,7 @@ export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './stor
|
|
|
26
31
|
export type { ProjectStats } from './storage/index.js';
|
|
27
32
|
export { recordInteractionEvidence, countInteractionEvidence, recordInteractionLesson, listLessons, listKnowledgeLessons, getApplicableLessons, transitionLesson, getRepositoryId, queueLocalLessonsForTeam } from './storage/index.js';
|
|
28
33
|
export type { ApplicableLessons, InteractionEvidence, LessonRecord, LessonState, LessonVisibility, LocalLessonImportOptions, LocalLessonImportResult } from './storage/index.js';
|
|
29
|
-
export { loadTeamConfiguration, saveTeamConfiguration, initializeTeamSchema, getTeamModeStatus, doctorTeamConnection, syncTeamOutbox, searchTeamKnowledge, backfillTeamEmbeddings, TEAM_CONFIG_PATH, validateTeamDatabaseUrl, } from './storage/index.js';
|
|
34
|
+
export { loadTeamConfiguration, saveTeamConfiguration, initializeTeamSchema, getTeamModeStatus, doctorTeamConnection, syncTeamOutbox, searchTeamKnowledge, backfillTeamEmbeddings, TEAM_CONFIG_PATH, validateTeamDatabaseUrl, explainTeamConnectionError, } from './storage/index.js';
|
|
30
35
|
export type { TeamConfiguration, TeamSemanticConfiguration, TeamModeStatus, TeamDoctorResult, SemanticKnowledgeCandidate, SemanticKnowledgeResult, } from './storage/index.js';
|
|
31
36
|
export { generateTemporalDriftReport, formatDriftSummary } from './services/temporal-drift.js';
|
|
32
37
|
export type { TemporalDriftReport, ProvenanceStream, MonthlyBucket, WeeklyBucket, DriftDirection } from './services/temporal-drift.js';
|
|
@@ -46,4 +51,8 @@ export * from './services/system-health.js';
|
|
|
46
51
|
export * from './services/agent-history.js';
|
|
47
52
|
export * from './services/engineering-knowledge-graph.js';
|
|
48
53
|
export * from './firewall/index.js';
|
|
54
|
+
export { learnFromFix, type LearnReport, type Rejection } from './semantic/learn/learn.js';
|
|
55
|
+
export { extractFixTrees, type FixTrees } from './semantic/learn/git-trees.js';
|
|
56
|
+
export { saveLearnedRule, loadLearnedRules, LEARNED_RULES_DIR } from './semantic/learn/store.js';
|
|
57
|
+
export type { LearnedRule, LearnedPattern } from './semantic/learn/types.js';
|
|
49
58
|
export { recordContextEvent, recordModelUsage, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics, } from './storage/index.js';
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,9 @@ export { RetryLoopBreakerGate } from './gates/retry-loop-breaker.js';
|
|
|
9
9
|
export { SideEffectAnalysisGate } from './gates/side-effect-analysis/index.js';
|
|
10
10
|
export { FrontendSecretExposureGate } from './gates/frontend-secret-exposure.js';
|
|
11
11
|
export * from './utils/logger.js';
|
|
12
|
+
export { normalizeScopePatterns, isScoped } from './utils/scope.js';
|
|
13
|
+
export { deepAnalysisError } from './utils/deep-status.js';
|
|
14
|
+
export { parseDiff, changedLinesByFile } from './utils/diff.js';
|
|
12
15
|
export { FileScanner } from './utils/scanner.js';
|
|
13
16
|
export * from './services/score-history.js';
|
|
14
17
|
export * from './hooks/index.js';
|
|
@@ -18,14 +21,16 @@ export { loadSettings, saveSettings, getSettingsPath, resolveDeepOptions, getPro
|
|
|
18
21
|
export { DeepAnalysisGate } from './gates/deep-analysis.js';
|
|
19
22
|
export { createProvider } from './inference/index.js';
|
|
20
23
|
export { MODELS } from './inference/types.js';
|
|
21
|
-
export { isModelCached, getModelsDir, getModelInfo } from './inference/model-manager.js';
|
|
24
|
+
export { isModelCached, getModelsDir, getModelInfo, getCachedModel, ensureModel } from './inference/model-manager.js';
|
|
25
|
+
export { installLlamaEngine, probeBinary, managedEnginePath, LLAMA_RELEASE_TAG } from './inference/llama-engine.js';
|
|
26
|
+
export { SidecarProvider } from './inference/sidecar-provider.js';
|
|
22
27
|
export { extractFacts, factsToPromptString } from './deep/fact-extractor.js';
|
|
23
28
|
// Storage (SQLite Brain)
|
|
24
29
|
export { openDatabase, isSQLiteAvailable, compactDatabase, getDatabaseSize, resetDatabase, insertScan, insertFindings, getRecentScans, getScoreTrendFromDB, getTopIssues, reinforcePattern, getStrongPatterns } from './storage/index.js';
|
|
25
30
|
// Local Project Memory (hybrid intelligence — SQLite-backed per-project learning)
|
|
26
31
|
export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './storage/index.js';
|
|
27
32
|
export { recordInteractionEvidence, countInteractionEvidence, recordInteractionLesson, listLessons, listKnowledgeLessons, getApplicableLessons, transitionLesson, getRepositoryId, queueLocalLessonsForTeam } from './storage/index.js';
|
|
28
|
-
export { loadTeamConfiguration, saveTeamConfiguration, initializeTeamSchema, getTeamModeStatus, doctorTeamConnection, syncTeamOutbox, searchTeamKnowledge, backfillTeamEmbeddings, TEAM_CONFIG_PATH, validateTeamDatabaseUrl, } from './storage/index.js';
|
|
33
|
+
export { loadTeamConfiguration, saveTeamConfiguration, initializeTeamSchema, getTeamModeStatus, doctorTeamConnection, syncTeamOutbox, searchTeamKnowledge, backfillTeamEmbeddings, TEAM_CONFIG_PATH, validateTeamDatabaseUrl, explainTeamConnectionError, } from './storage/index.js';
|
|
29
34
|
// Temporal Drift Engine (v5 — cross-session trend analysis + per-provenance EWMA)
|
|
30
35
|
export { generateTemporalDriftReport, formatDriftSummary } from './services/temporal-drift.js';
|
|
31
36
|
// Adaptive Thresholds (v5 — Z-score + per-provenance trends)
|
|
@@ -45,6 +50,9 @@ export * from './services/system-health.js';
|
|
|
45
50
|
export * from './services/agent-history.js';
|
|
46
51
|
export * from './services/engineering-knowledge-graph.js';
|
|
47
52
|
export * from './firewall/index.js';
|
|
53
|
+
export { learnFromFix } from './semantic/learn/learn.js';
|
|
54
|
+
export { extractFixTrees } from './semantic/learn/git-trees.js';
|
|
55
|
+
export { saveLearnedRule, loadLearnedRules, LEARNED_RULES_DIR } from './semantic/learn/store.js';
|
|
48
56
|
export { recordContextEvent, recordModelUsage, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics, } from './storage/index.js';
|
|
49
57
|
// Pattern Index is intentionally NOT exported here to prevent
|
|
50
58
|
// native dependency issues (sharp/transformers) from leaking into
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Readable } from 'stream';
|
|
2
|
+
export interface HttpResponse {
|
|
3
|
+
statusCode: number;
|
|
4
|
+
headers: Record<string, string | string[] | undefined>;
|
|
5
|
+
body: Readable;
|
|
6
|
+
}
|
|
7
|
+
export type HttpGet = (url: string, headers: Record<string, string>, timeoutMs: number) => Promise<HttpResponse>;
|
|
8
|
+
export interface DownloadOptions {
|
|
9
|
+
onProgress?: (downloadedBytes: number, totalBytes: number | null) => void;
|
|
10
|
+
stallTimeoutMs?: number;
|
|
11
|
+
/** Resume from an existing partial file at `destPath` (default true). */
|
|
12
|
+
resume?: boolean;
|
|
13
|
+
/** Transport override for tests. */
|
|
14
|
+
get?: HttpGet;
|
|
15
|
+
}
|
|
16
|
+
export interface DownloadResult {
|
|
17
|
+
sha256: string;
|
|
18
|
+
bytes: number;
|
|
19
|
+
/** ETag seen on any hop; Hugging Face exposes the LFS SHA-256 as x-linked-etag. */
|
|
20
|
+
etag: string | null;
|
|
21
|
+
}
|
|
22
|
+
export declare function downloadToFile(url: string, destPath: string, options?: DownloadOptions): Promise<DownloadResult>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming HTTPS download for large binaries (models, engine archives).
|
|
3
|
+
*
|
|
4
|
+
* Uses node:https rather than global fetch: on large Hugging Face downloads
|
|
5
|
+
* undici's fetch measured ~1.7 MB/s where node:https reached ~17 MB/s on the
|
|
6
|
+
* same host and byte range, which made the first-run model download exceed
|
|
7
|
+
* any reasonable setup window.
|
|
8
|
+
*
|
|
9
|
+
* - follows at most MAX_REDIRECTS redirects, https only
|
|
10
|
+
* - resumes a kept partial file with a Range request (the prefix is re-hashed
|
|
11
|
+
* so the SHA-256 still covers the whole file)
|
|
12
|
+
* - aborts when no bytes arrive for `stallTimeoutMs` instead of imposing a
|
|
13
|
+
* wall-clock limit that a slow but healthy connection cannot meet
|
|
14
|
+
*/
|
|
15
|
+
import https from 'https';
|
|
16
|
+
import fs from 'fs-extra';
|
|
17
|
+
import { createHash } from 'crypto';
|
|
18
|
+
import { Transform } from 'stream';
|
|
19
|
+
import { pipeline } from 'stream/promises';
|
|
20
|
+
const MAX_REDIRECTS = 5;
|
|
21
|
+
const DEFAULT_STALL_TIMEOUT_MS = 30_000;
|
|
22
|
+
export async function downloadToFile(url, destPath, options = {}) {
|
|
23
|
+
const stallMs = options.stallTimeoutMs ?? DEFAULT_STALL_TIMEOUT_MS;
|
|
24
|
+
const get = options.get ?? httpsGet;
|
|
25
|
+
let existing = options.resume === false ? 0 : await partialSize(destPath);
|
|
26
|
+
let { response, etag } = await followRedirects(url, rangeFrom(existing), get, stallMs);
|
|
27
|
+
if (response.statusCode === 416 && existing > 0) {
|
|
28
|
+
// The partial is already at or past the end: start over once, from zero.
|
|
29
|
+
response.body.resume();
|
|
30
|
+
await fs.remove(destPath);
|
|
31
|
+
existing = 0;
|
|
32
|
+
({ response, etag } = await followRedirects(url, rangeFrom(0), get, stallMs));
|
|
33
|
+
}
|
|
34
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
35
|
+
response.body.resume();
|
|
36
|
+
throw new Error(`HTTP ${response.statusCode} for ${url}`);
|
|
37
|
+
}
|
|
38
|
+
const resumed = response.statusCode === 206 && existing > 0;
|
|
39
|
+
const hash = createHash('sha256');
|
|
40
|
+
if (resumed)
|
|
41
|
+
await hashFilePrefix(destPath, hash);
|
|
42
|
+
const startBytes = resumed ? existing : 0;
|
|
43
|
+
const total = contentLength(response.headers, startBytes);
|
|
44
|
+
const bytes = await writeBody(response.body, destPath, hash, {
|
|
45
|
+
append: resumed, startBytes, total, stallMs, onProgress: options.onProgress,
|
|
46
|
+
});
|
|
47
|
+
return { sha256: hash.digest('hex'), bytes, etag };
|
|
48
|
+
}
|
|
49
|
+
function rangeFrom(offset) {
|
|
50
|
+
return offset > 0 ? { Range: `bytes=${offset}-` } : {};
|
|
51
|
+
}
|
|
52
|
+
async function partialSize(filePath) {
|
|
53
|
+
try {
|
|
54
|
+
return (await fs.stat(filePath)).size;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function followRedirects(url, headers, get, timeoutMs) {
|
|
61
|
+
let current = url;
|
|
62
|
+
let etag = null;
|
|
63
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
64
|
+
if (new URL(current).protocol !== 'https:') {
|
|
65
|
+
throw new Error(`Refusing non-https download URL: ${current}`);
|
|
66
|
+
}
|
|
67
|
+
const response = await get(current, headers, timeoutMs);
|
|
68
|
+
etag = etag ?? headerValue(response.headers, 'x-linked-etag') ?? headerValue(response.headers, 'etag');
|
|
69
|
+
const location = headerValue(response.headers, 'location');
|
|
70
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && location) {
|
|
71
|
+
response.body.resume();
|
|
72
|
+
current = new URL(location, current).toString();
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
return { response, etag };
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`Too many redirects downloading ${url}`);
|
|
78
|
+
}
|
|
79
|
+
async function writeBody(body, destPath, hash, opts) {
|
|
80
|
+
let downloaded = opts.startBytes;
|
|
81
|
+
let stallTimer;
|
|
82
|
+
const armStall = () => {
|
|
83
|
+
clearTimeout(stallTimer);
|
|
84
|
+
stallTimer = setTimeout(() => {
|
|
85
|
+
body.destroy(new Error(`Download stalled: no data for ${opts.stallMs / 1000}s`));
|
|
86
|
+
}, opts.stallMs);
|
|
87
|
+
};
|
|
88
|
+
const tap = new Transform({
|
|
89
|
+
transform(chunk, _encoding, callback) {
|
|
90
|
+
armStall();
|
|
91
|
+
hash.update(chunk);
|
|
92
|
+
downloaded += chunk.length;
|
|
93
|
+
opts.onProgress?.(downloaded, opts.total);
|
|
94
|
+
callback(null, chunk);
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
armStall();
|
|
98
|
+
try {
|
|
99
|
+
await pipeline(body, tap, fs.createWriteStream(destPath, { flags: opts.append ? 'a' : 'w' }));
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
clearTimeout(stallTimer);
|
|
103
|
+
}
|
|
104
|
+
return downloaded;
|
|
105
|
+
}
|
|
106
|
+
async function hashFilePrefix(filePath, hash) {
|
|
107
|
+
const stream = fs.createReadStream(filePath);
|
|
108
|
+
try {
|
|
109
|
+
for await (const chunk of stream)
|
|
110
|
+
hash.update(chunk);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
stream.destroy();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function contentLength(headers, offset) {
|
|
117
|
+
const value = Number(headerValue(headers, 'content-length'));
|
|
118
|
+
return Number.isFinite(value) && value > 0 ? value + offset : null;
|
|
119
|
+
}
|
|
120
|
+
function headerValue(headers, name) {
|
|
121
|
+
const value = headers[name];
|
|
122
|
+
if (Array.isArray(value))
|
|
123
|
+
return value[0] ?? null;
|
|
124
|
+
return value ?? null;
|
|
125
|
+
}
|
|
126
|
+
const httpsGet = (url, headers, timeoutMs) => new Promise((resolve, reject) => {
|
|
127
|
+
const request = https.get(url, { headers: { 'user-agent': 'rigour-cli', ...headers } }, (res) => {
|
|
128
|
+
resolve({ statusCode: res.statusCode ?? 0, headers: res.headers, body: res });
|
|
129
|
+
});
|
|
130
|
+
request.setTimeout(timeoutMs, () => request.destroy(new Error(`No response from ${new URL(url).host} after ${timeoutMs / 1000}s`)));
|
|
131
|
+
request.on('error', reject);
|
|
132
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
5
|
+
import { Readable, PassThrough } from 'stream';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
7
|
+
import { downloadToFile } from './http-download.js';
|
|
8
|
+
const PAYLOAD = Buffer.from('0123456789abcdefghijklmnopqrstuvwxyz');
|
|
9
|
+
const sha = (b) => createHash('sha256').update(b).digest('hex');
|
|
10
|
+
function response(statusCode, body = Buffer.alloc(0), headers = {}) {
|
|
11
|
+
return { statusCode, headers, body: Buffer.isBuffer(body) ? Readable.from([body]) : body };
|
|
12
|
+
}
|
|
13
|
+
/** Serves PAYLOAD, honouring Range unless told not to. */
|
|
14
|
+
function server(opts = {}) {
|
|
15
|
+
const calls = [];
|
|
16
|
+
const get = async (url, headers) => {
|
|
17
|
+
calls.push({ url, headers });
|
|
18
|
+
const range = headers.Range?.match(/bytes=(\d+)-/);
|
|
19
|
+
if (range && opts.honourRange !== false) {
|
|
20
|
+
const start = Number(range[1]);
|
|
21
|
+
if (start >= PAYLOAD.length)
|
|
22
|
+
return response(416);
|
|
23
|
+
return response(206, PAYLOAD.subarray(start), { 'content-length': String(PAYLOAD.length - start) });
|
|
24
|
+
}
|
|
25
|
+
return response(200, PAYLOAD, { 'content-length': String(PAYLOAD.length), etag: opts.etag });
|
|
26
|
+
};
|
|
27
|
+
return { get, calls };
|
|
28
|
+
}
|
|
29
|
+
describe('downloadToFile', () => {
|
|
30
|
+
let dir;
|
|
31
|
+
let dest;
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
dir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-dl-'));
|
|
34
|
+
dest = path.join(dir, 'model.gguf.download');
|
|
35
|
+
});
|
|
36
|
+
afterEach(async () => { await fs.remove(dir); });
|
|
37
|
+
it('downloads the full body and hashes it', async () => {
|
|
38
|
+
const { get } = server({ etag: '"abc"' });
|
|
39
|
+
const result = await downloadToFile('https://host/model', dest, { get });
|
|
40
|
+
expect(await fs.readFile(dest)).toEqual(PAYLOAD);
|
|
41
|
+
expect(result).toEqual({ sha256: sha(PAYLOAD), bytes: PAYLOAD.length, etag: '"abc"' });
|
|
42
|
+
});
|
|
43
|
+
it('follows redirects and keeps the x-linked-etag from the first hop', async () => {
|
|
44
|
+
const { get: origin, calls } = server();
|
|
45
|
+
const get = async (url, headers, timeout) => {
|
|
46
|
+
if (url === 'https://hub/model') {
|
|
47
|
+
return response(302, Buffer.alloc(0), { location: 'https://cdn/model', 'x-linked-etag': '"lfs-sha"' });
|
|
48
|
+
}
|
|
49
|
+
return origin(url, headers, timeout);
|
|
50
|
+
};
|
|
51
|
+
const result = await downloadToFile('https://hub/model', dest, { get });
|
|
52
|
+
expect(calls.map(c => c.url)).toEqual(['https://cdn/model']);
|
|
53
|
+
expect(result.etag).toBe('"lfs-sha"');
|
|
54
|
+
});
|
|
55
|
+
it('resumes a partial file with a Range request and hashes the whole file', async () => {
|
|
56
|
+
await fs.writeFile(dest, PAYLOAD.subarray(0, 10));
|
|
57
|
+
const { get, calls } = server();
|
|
58
|
+
const result = await downloadToFile('https://host/model', dest, { get });
|
|
59
|
+
expect(calls[0].headers.Range).toBe('bytes=10-');
|
|
60
|
+
expect(await fs.readFile(dest)).toEqual(PAYLOAD);
|
|
61
|
+
expect(result.sha256).toBe(sha(PAYLOAD));
|
|
62
|
+
expect(result.bytes).toBe(PAYLOAD.length);
|
|
63
|
+
});
|
|
64
|
+
it('restarts from zero when the server ignores Range', async () => {
|
|
65
|
+
await fs.writeFile(dest, Buffer.from('stale-bytes'));
|
|
66
|
+
const { get } = server({ honourRange: false });
|
|
67
|
+
const result = await downloadToFile('https://host/model', dest, { get });
|
|
68
|
+
expect(await fs.readFile(dest)).toEqual(PAYLOAD);
|
|
69
|
+
expect(result.sha256).toBe(sha(PAYLOAD));
|
|
70
|
+
});
|
|
71
|
+
it('restarts when the partial is already past the end (416)', async () => {
|
|
72
|
+
await fs.writeFile(dest, Buffer.concat([PAYLOAD, Buffer.from('extra')]));
|
|
73
|
+
const { get, calls } = server();
|
|
74
|
+
await downloadToFile('https://host/model', dest, { get });
|
|
75
|
+
expect(calls).toHaveLength(2);
|
|
76
|
+
expect(await fs.readFile(dest)).toEqual(PAYLOAD);
|
|
77
|
+
});
|
|
78
|
+
it('rejects non-success responses without creating the file', async () => {
|
|
79
|
+
const get = async () => response(401);
|
|
80
|
+
await expect(downloadToFile('https://host/model', dest, { get })).rejects.toThrow('HTTP 401');
|
|
81
|
+
expect(await fs.pathExists(dest)).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
it('refuses non-https URLs, including redirects to them', async () => {
|
|
84
|
+
const { get } = server();
|
|
85
|
+
await expect(downloadToFile('http://host/model', dest, { get })).rejects.toThrow('non-https');
|
|
86
|
+
const redirecting = async () => response(302, Buffer.alloc(0), { location: 'http://evil/model' });
|
|
87
|
+
await expect(downloadToFile('https://host/model', dest, { get: redirecting })).rejects.toThrow('non-https');
|
|
88
|
+
});
|
|
89
|
+
it('stops after too many redirects', async () => {
|
|
90
|
+
const loop = async (url) => response(302, Buffer.alloc(0), { location: `${url}x` });
|
|
91
|
+
await expect(downloadToFile('https://host/m', dest, { get: loop })).rejects.toThrow('Too many redirects');
|
|
92
|
+
});
|
|
93
|
+
it('aborts a stalled body and keeps the partial for the next resume', async () => {
|
|
94
|
+
const body = new PassThrough();
|
|
95
|
+
body.write(PAYLOAD.subarray(0, 5));
|
|
96
|
+
const get = async () => response(200, body, { 'content-length': String(PAYLOAD.length) });
|
|
97
|
+
await expect(downloadToFile('https://host/model', dest, { get, stallTimeoutMs: 50 })).rejects.toThrow('stalled');
|
|
98
|
+
// The partial file is kept for the next resume (how many bytes reached disk is up to the OS).
|
|
99
|
+
expect(await fs.pathExists(dest)).toBe(true);
|
|
100
|
+
expect((await fs.stat(dest)).size).toBeLessThanOrEqual(5);
|
|
101
|
+
});
|
|
102
|
+
it('reports progress against the full size when resuming', async () => {
|
|
103
|
+
await fs.writeFile(dest, PAYLOAD.subarray(0, 10));
|
|
104
|
+
const { get } = server();
|
|
105
|
+
const seen = [];
|
|
106
|
+
await downloadToFile('https://host/model', dest, { get, onProgress: (d, t) => seen.push([d, t]) });
|
|
107
|
+
expect(seen.at(-1)).toEqual([PAYLOAD.length, PAYLOAD.length]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
export type { InferenceProvider, InferenceOptions, DeepFinding, DeepAnalysisResult, ModelTier, ModelInfo } from './types.js';
|
|
5
5
|
export { MODELS } from './types.js';
|
|
6
6
|
export { SidecarProvider } from './sidecar-provider.js';
|
|
7
|
+
export type { ActiveModel } from './sidecar-provider.js';
|
|
8
|
+
export { installLlamaEngine, probeBinary, managedEnginePath, LLAMA_RELEASE_TAG } from './llama-engine.js';
|
|
9
|
+
export type { ProbeResult } from './llama-engine.js';
|
|
7
10
|
export { CloudProvider } from './cloud-provider.js';
|
|
8
|
-
export { ensureModel, isModelCached, getModelPath, getModelInfo, downloadModel, getModelsDir } from './model-manager.js';
|
|
11
|
+
export { ensureModel, isModelCached, getModelPath, getModelInfo, downloadModel, getModelsDir, getCachedModel } from './model-manager.js';
|
|
12
|
+
export type { CachedModel } from './model-manager.js';
|
|
9
13
|
import type { InferenceProvider } from './types.js';
|
|
10
14
|
import type { DeepOptions } from '../types/index.js';
|
|
11
15
|
/**
|
package/dist/inference/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { MODELS } from './types.js';
|
|
2
2
|
export { SidecarProvider } from './sidecar-provider.js';
|
|
3
|
+
export { installLlamaEngine, probeBinary, managedEnginePath, LLAMA_RELEASE_TAG } from './llama-engine.js';
|
|
3
4
|
export { CloudProvider } from './cloud-provider.js';
|
|
4
|
-
export { ensureModel, isModelCached, getModelPath, getModelInfo, downloadModel, getModelsDir } from './model-manager.js';
|
|
5
|
+
export { ensureModel, isModelCached, getModelPath, getModelInfo, downloadModel, getModelsDir, getCachedModel } from './model-manager.js';
|
|
5
6
|
import { SidecarProvider } from './sidecar-provider.js';
|
|
6
7
|
import { CloudProvider } from './cloud-provider.js';
|
|
7
8
|
/**
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type HttpGet } from './http-download.js';
|
|
2
|
+
export declare const LLAMA_RELEASE_TAG = "b5604";
|
|
3
|
+
export declare function llamaBinaryName(): string;
|
|
4
|
+
/** Install directory for the pinned release: ~/.rigour/bin/llama-<tag>. */
|
|
5
|
+
export declare function managedEngineDir(): string;
|
|
6
|
+
export declare function managedEnginePath(): string;
|
|
7
|
+
export declare function releaseAssetFor(platformKey: string): string | undefined;
|
|
8
|
+
export interface ProbeResult {
|
|
9
|
+
ok: boolean;
|
|
10
|
+
version?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Run `<binary> --version` and accept it only if it exits 0. A placeholder
|
|
15
|
+
* script (such as an unbundled brain package) or a binary missing its shared
|
|
16
|
+
* libraries fails here instead of on every inference call.
|
|
17
|
+
*/
|
|
18
|
+
export declare function probeBinary(binaryPath: string): Promise<ProbeResult>;
|
|
19
|
+
/** Forget cached probe results, e.g. after installing a new engine. */
|
|
20
|
+
export declare function resetProbeCache(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Download the pinned release for this platform and install its whole bin
|
|
23
|
+
* folder into managedEngineDir(). Returns the verified binary path.
|
|
24
|
+
*/
|
|
25
|
+
export declare function installLlamaEngine(onProgress?: (message: string) => void, get?: HttpGet): Promise<string>;
|
|
26
|
+
/** Copy the folder that contains llama-cli (binary + shared libraries) into place. */
|
|
27
|
+
export declare function installBinFolder(extractedRoot: string, installDir: string): Promise<void>;
|