@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.
Files changed (180) hide show
  1. package/README.md +2 -0
  2. package/dist/deep/code-context.d.ts +28 -0
  3. package/dist/deep/code-context.js +93 -0
  4. package/dist/deep/code-context.test.d.ts +1 -0
  5. package/dist/deep/code-context.test.js +48 -0
  6. package/dist/deep/code-pass.d.ts +15 -0
  7. package/dist/deep/code-pass.js +44 -0
  8. package/dist/deep/code-review-prompt.d.ts +17 -0
  9. package/dist/deep/code-review-prompt.js +30 -0
  10. package/dist/deep/code-review-prompt.test.d.ts +1 -0
  11. package/dist/deep/code-review-prompt.test.js +20 -0
  12. package/dist/deep/code-verifier.d.ts +19 -0
  13. package/dist/deep/code-verifier.js +48 -0
  14. package/dist/deep/code-verifier.test.d.ts +1 -0
  15. package/dist/deep/code-verifier.test.js +46 -0
  16. package/dist/deep/fact-extractor.d.ts +7 -1
  17. package/dist/deep/fact-extractor.js +16 -8
  18. package/dist/deep/fact-extractor.test.js +34 -1
  19. package/dist/deep/facts-pass.d.ts +24 -0
  20. package/dist/deep/facts-pass.js +69 -0
  21. package/dist/deep/index.d.ts +6 -1
  22. package/dist/deep/index.js +5 -1
  23. package/dist/deep/parse-findings.d.ts +12 -0
  24. package/dist/deep/parse-findings.js +73 -0
  25. package/dist/deep/parse-findings.test.d.ts +1 -0
  26. package/dist/deep/parse-findings.test.js +25 -0
  27. package/dist/deep/prompts.d.ts +1 -1
  28. package/dist/deep/prompts.js +7 -8
  29. package/dist/deep/prompts.test.js +15 -0
  30. package/dist/discovery-signals.d.ts +27 -0
  31. package/dist/discovery-signals.js +96 -0
  32. package/dist/discovery.d.ts +1 -2
  33. package/dist/discovery.js +36 -49
  34. package/dist/discovery.test.js +84 -71
  35. package/dist/gates/context.js +7 -0
  36. package/dist/gates/deep-analysis.d.ts +33 -8
  37. package/dist/gates/deep-analysis.js +169 -306
  38. package/dist/gates/deep-analysis.test.d.ts +1 -0
  39. package/dist/gates/deep-analysis.test.js +86 -0
  40. package/dist/gates/deep-intent.d.ts +3 -0
  41. package/dist/gates/deep-intent.js +57 -0
  42. package/dist/gates/deep-intent.test.d.ts +1 -0
  43. package/dist/gates/deep-intent.test.js +51 -0
  44. package/dist/gates/deep-runner.d.ts +15 -0
  45. package/dist/gates/deep-runner.js +66 -0
  46. package/dist/gates/dependency.js +4 -0
  47. package/dist/gates/deprecated-apis-ast.d.ts +8 -0
  48. package/dist/gates/deprecated-apis-ast.js +25 -0
  49. package/dist/gates/deprecated-apis-rules-node.js +2 -8
  50. package/dist/gates/deprecated-apis.js +5 -1
  51. package/dist/gates/deprecated-apis.test.js +30 -0
  52. package/dist/gates/frontend-secret-exposure.js +5 -1
  53. package/dist/gates/frontend-secret-exposure.test.js +20 -0
  54. package/dist/gates/hallucinated-imports/framework-modules.d.ts +14 -0
  55. package/dist/gates/hallucinated-imports/framework-modules.js +75 -0
  56. package/dist/gates/hallucinated-imports/index.js +14 -6
  57. package/dist/gates/hallucinated-imports/js-resolver.d.ts +16 -12
  58. package/dist/gates/hallucinated-imports/js-resolver.js +65 -153
  59. package/dist/gates/hallucinated-imports/ts-path-target.js +16 -0
  60. package/dist/gates/hallucinated-imports/tsconfig-paths.d.ts +16 -0
  61. package/dist/gates/hallucinated-imports/tsconfig-paths.js +120 -0
  62. package/dist/gates/hallucinated-imports-sveltekit.test.d.ts +1 -0
  63. package/dist/gates/hallucinated-imports-sveltekit.test.js +108 -0
  64. package/dist/gates/logic-drift.js +7 -1
  65. package/dist/gates/runner.js +10 -40
  66. package/dist/gates/runner.test.js +37 -25
  67. package/dist/gates/scoped-gates.test.d.ts +1 -0
  68. package/dist/gates/scoped-gates.test.js +52 -0
  69. package/dist/gates/semantic-bugs.d.ts +23 -0
  70. package/dist/gates/semantic-bugs.js +48 -0
  71. package/dist/gates/semantic-bugs.test.d.ts +1 -0
  72. package/dist/gates/semantic-bugs.test.js +70 -0
  73. package/dist/gates/side-effect-analysis/index.js +11 -1
  74. package/dist/gates/side-effect-analysis.test.d.ts +1 -0
  75. package/dist/gates/side-effect-analysis.test.js +162 -0
  76. package/dist/gates/side-effect-helpers/pattern-detection.js +35 -7
  77. package/dist/gates/side-effect-helpers/resource-tracking.js +17 -10
  78. package/dist/gates/side-effect-helpers/scope-analysis.d.ts +6 -0
  79. package/dist/gates/side-effect-helpers/scope-analysis.js +20 -0
  80. package/dist/gates/style-drift.js +3 -0
  81. package/dist/index.d.ts +11 -2
  82. package/dist/index.js +10 -2
  83. package/dist/inference/http-download.d.ts +22 -0
  84. package/dist/inference/http-download.js +132 -0
  85. package/dist/inference/http-download.test.d.ts +1 -0
  86. package/dist/inference/http-download.test.js +109 -0
  87. package/dist/inference/index.d.ts +5 -1
  88. package/dist/inference/index.js +2 -1
  89. package/dist/inference/llama-engine.d.ts +27 -0
  90. package/dist/inference/llama-engine.js +152 -0
  91. package/dist/inference/llama-engine.test.d.ts +1 -0
  92. package/dist/inference/llama-engine.test.js +51 -0
  93. package/dist/inference/llama-process.d.ts +72 -0
  94. package/dist/inference/llama-process.js +126 -0
  95. package/dist/inference/llama-process.test.d.ts +1 -0
  96. package/dist/inference/llama-process.test.js +61 -0
  97. package/dist/inference/model-download.test.d.ts +1 -0
  98. package/dist/inference/model-download.test.js +73 -0
  99. package/dist/inference/model-manager.d.ts +14 -1
  100. package/dist/inference/model-manager.js +65 -80
  101. package/dist/inference/sidecar-provider.d.ts +19 -10
  102. package/dist/inference/sidecar-provider.js +125 -342
  103. package/dist/inference/types.d.ts +2 -0
  104. package/dist/semantic/ast.d.ts +29 -0
  105. package/dist/semantic/ast.js +122 -0
  106. package/dist/semantic/benchmark.d.ts +16 -0
  107. package/dist/semantic/benchmark.js +31 -0
  108. package/dist/semantic/benchmark.test.d.ts +1 -0
  109. package/dist/semantic/benchmark.test.js +18 -0
  110. package/dist/semantic/engine.d.ts +8 -0
  111. package/dist/semantic/engine.js +45 -0
  112. package/dist/semantic/intent/benchmark.d.ts +31 -0
  113. package/dist/semantic/intent/benchmark.js +74 -0
  114. package/dist/semantic/intent/intent-check.d.ts +43 -0
  115. package/dist/semantic/intent/intent-check.js +72 -0
  116. package/dist/semantic/intent/intent.test.d.ts +1 -0
  117. package/dist/semantic/intent/intent.test.js +46 -0
  118. package/dist/semantic/intent/parallel-reads.d.ts +23 -0
  119. package/dist/semantic/intent/parallel-reads.js +59 -0
  120. package/dist/semantic/learn/benchmark.d.ts +20 -0
  121. package/dist/semantic/learn/benchmark.js +71 -0
  122. package/dist/semantic/learn/compile.d.ts +4 -0
  123. package/dist/semantic/learn/compile.js +56 -0
  124. package/dist/semantic/learn/conditions.d.ts +22 -0
  125. package/dist/semantic/learn/conditions.js +93 -0
  126. package/dist/semantic/learn/extract.d.ts +30 -0
  127. package/dist/semantic/learn/extract.js +122 -0
  128. package/dist/semantic/learn/git-trees.d.ts +8 -0
  129. package/dist/semantic/learn/git-trees.js +32 -0
  130. package/dist/semantic/learn/identity.d.ts +18 -0
  131. package/dist/semantic/learn/identity.js +97 -0
  132. package/dist/semantic/learn/learn.d.ts +25 -0
  133. package/dist/semantic/learn/learn.js +109 -0
  134. package/dist/semantic/learn/learn.test.d.ts +1 -0
  135. package/dist/semantic/learn/learn.test.js +67 -0
  136. package/dist/semantic/learn/store.d.ts +4 -0
  137. package/dist/semantic/learn/store.js +39 -0
  138. package/dist/semantic/learn/types.d.ts +468 -0
  139. package/dist/semantic/learn/types.js +55 -0
  140. package/dist/semantic/origins.d.ts +42 -0
  141. package/dist/semantic/origins.js +149 -0
  142. package/dist/semantic/origins.test.d.ts +1 -0
  143. package/dist/semantic/origins.test.js +77 -0
  144. package/dist/semantic/program.d.ts +13 -0
  145. package/dist/semantic/program.js +62 -0
  146. package/dist/semantic/rules/credential-redirect.d.ts +2 -0
  147. package/dist/semantic/rules/credential-redirect.js +209 -0
  148. package/dist/semantic/rules/degraded-response-cached.d.ts +2 -0
  149. package/dist/semantic/rules/degraded-response-cached.js +271 -0
  150. package/dist/semantic/rules/in-memory-aggregation.d.ts +2 -0
  151. package/dist/semantic/rules/in-memory-aggregation.js +179 -0
  152. package/dist/semantic/summaries.d.ts +18 -0
  153. package/dist/semantic/summaries.js +33 -0
  154. package/dist/semantic/types.d.ts +32 -0
  155. package/dist/semantic/types.js +1 -0
  156. package/dist/storage/index.d.ts +1 -0
  157. package/dist/storage/index.js +1 -0
  158. package/dist/storage/team-diagnostics.d.ts +22 -0
  159. package/dist/storage/team-diagnostics.js +58 -0
  160. package/dist/storage/team-diagnostics.test.d.ts +1 -0
  161. package/dist/storage/team-diagnostics.test.js +51 -0
  162. package/dist/storage/team-schema.d.ts +6 -0
  163. package/dist/storage/team-schema.js +82 -0
  164. package/dist/storage/team-store.js +25 -72
  165. package/dist/templates/universal-config.js +4 -2
  166. package/dist/types/index.d.ts +114 -14
  167. package/dist/types/index.js +16 -2
  168. package/dist/utils/deep-status.d.ts +7 -0
  169. package/dist/utils/deep-status.js +11 -0
  170. package/dist/utils/diff.d.ts +3 -0
  171. package/dist/utils/diff.js +52 -0
  172. package/dist/utils/diff.test.d.ts +1 -0
  173. package/dist/utils/diff.test.js +38 -0
  174. package/dist/utils/scanner.js +13 -2
  175. package/dist/utils/scanner.test.js +22 -8
  176. package/dist/utils/scope.d.ts +15 -0
  177. package/dist/utils/scope.js +51 -0
  178. package/dist/utils/scope.test.d.ts +1 -0
  179. package/dist/utils/scope.test.js +38 -0
  180. 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
- /\b(?:spawn|exec|execFile|fork|execa)\s*\(/,
319
- /\bchild_process\.\w+\s*\(/,
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
- const escapedVar = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
77
- const varRef = new RegExp(`\\b${escapedVar}\\b`);
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.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
85
- const methodCleanup = new RegExp(`\\b${escapedVar}\\.(?:close|stop|destroy|kill|terminate|dispose|cancel|shutdown|unsubscribe|disconnect|end|release|Clear|Stop|Dispose|Close|Cancel)\\s*\\(`);
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
  /**
@@ -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>;