@rigour-labs/core 6.6.5 → 6.7.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 (154) hide show
  1. package/README.md +16 -92
  2. package/dist/environment.test.js +0 -4
  3. package/dist/gates/agent-team.d.ts +0 -2
  4. package/dist/gates/agent-team.js +0 -8
  5. package/dist/gates/checkpoint.d.ts +0 -1
  6. package/dist/gates/checkpoint.js +0 -1
  7. package/dist/gates/context.d.ts +0 -4
  8. package/dist/gates/context.js +7 -17
  9. package/dist/gates/deep-analysis.d.ts +0 -1
  10. package/dist/gates/deep-runner.js +0 -1
  11. package/dist/gates/frontend-secret-exposure.d.ts +1 -1
  12. package/dist/gates/frontend-secret-exposure.js +9 -3
  13. package/dist/gates/frontend-secret-exposure.test.js +6 -3
  14. package/dist/gates/hallucinated-imports/index.d.ts +0 -2
  15. package/dist/gates/hallucinated-imports/index.js +2 -2
  16. package/dist/gates/hallucinated-imports/js-resolver.js +1 -0
  17. package/dist/gates/hallucinated-imports.test.js +10 -0
  18. package/dist/gates/runner.js +3 -4
  19. package/dist/gates/test-quality-matchers.d.ts +0 -4
  20. package/dist/gates/test-quality-matchers.js +0 -7
  21. package/dist/gates/test-quality.d.ts +0 -2
  22. package/dist/gates/test-quality.js +1 -10
  23. package/dist/hooks/checker.js +1 -1
  24. package/dist/hooks/index.d.ts +2 -3
  25. package/dist/hooks/index.js +1 -2
  26. package/dist/hooks/stop-review.d.ts +6 -9
  27. package/dist/hooks/stop-review.js +5 -10
  28. package/dist/hooks/types.d.ts +0 -14
  29. package/dist/hooks/types.js +1 -15
  30. package/dist/index.d.ts +11 -3
  31. package/dist/index.js +11 -3
  32. package/dist/inference/cloud-provider.d.ts +2 -0
  33. package/dist/inference/cloud-provider.js +16 -6
  34. package/dist/inference/cloud-provider.test.js +21 -0
  35. package/dist/pattern-index/embeddings.d.ts +0 -5
  36. package/dist/pattern-index/embeddings.js +11 -4
  37. package/dist/pattern-index/semantic-runtime.d.ts +10 -0
  38. package/dist/pattern-index/semantic-runtime.js +46 -0
  39. package/dist/pattern-index/semantic-runtime.test.d.ts +1 -0
  40. package/dist/pattern-index/semantic-runtime.test.js +31 -0
  41. package/dist/review/backtest-init.d.ts +8 -0
  42. package/dist/review/backtest-init.js +87 -0
  43. package/dist/review/backtest-init.test.d.ts +1 -0
  44. package/dist/review/backtest-init.test.js +74 -0
  45. package/dist/review/backtest-judges.d.ts +38 -0
  46. package/dist/review/backtest-judges.js +63 -0
  47. package/dist/review/backtest-judges.test.d.ts +1 -0
  48. package/dist/review/backtest-judges.test.js +32 -0
  49. package/dist/review/backtest.d.ts +320 -0
  50. package/dist/review/backtest.js +207 -0
  51. package/dist/review/backtest.test.d.ts +1 -0
  52. package/dist/review/backtest.test.js +129 -0
  53. package/dist/review/baseline.js +2 -1
  54. package/dist/review/dead-code.test.js +15 -0
  55. package/dist/review/fix-scope.d.ts +21 -0
  56. package/dist/review/fix-scope.js +94 -0
  57. package/dist/review/fix-scope.test.d.ts +1 -0
  58. package/dist/review/fix-scope.test.js +104 -0
  59. package/dist/review/lint-overlay.d.ts +21 -0
  60. package/dist/review/lint-overlay.js +89 -0
  61. package/dist/review/orphan-files.js +3 -2
  62. package/dist/review/partial-wiring.js +5 -3
  63. package/dist/review/query-patterns.js +3 -2
  64. package/dist/review/quiet.d.ts +6 -0
  65. package/dist/review/quiet.js +12 -2
  66. package/dist/review/quiet.test.js +11 -0
  67. package/dist/review/review.d.ts +6 -0
  68. package/dist/review/review.js +10 -2
  69. package/dist/review/reviewer/adapters.d.ts +51 -0
  70. package/dist/review/reviewer/adapters.js +181 -0
  71. package/dist/review/reviewer/adapters.test.d.ts +1 -0
  72. package/dist/review/reviewer/adapters.test.js +23 -0
  73. package/dist/review/reviewer/background.d.ts +41 -0
  74. package/dist/review/reviewer/background.js +153 -0
  75. package/dist/review/reviewer/background.test.d.ts +1 -0
  76. package/dist/review/reviewer/background.test.js +118 -0
  77. package/dist/review/reviewer/consensus.d.ts +36 -0
  78. package/dist/review/reviewer/consensus.js +144 -0
  79. package/dist/review/reviewer/context.d.ts +75 -0
  80. package/dist/review/reviewer/context.js +140 -0
  81. package/dist/review/reviewer/context.test.d.ts +1 -0
  82. package/dist/review/reviewer/context.test.js +18 -0
  83. package/dist/review/reviewer/exec.d.ts +17 -0
  84. package/dist/review/reviewer/exec.js +27 -0
  85. package/dist/review/reviewer/exec.test.d.ts +1 -0
  86. package/dist/review/reviewer/exec.test.js +23 -0
  87. package/dist/review/reviewer/inputs.d.ts +39 -0
  88. package/dist/review/reviewer/inputs.js +107 -0
  89. package/dist/review/reviewer/merge-impact.d.ts +2 -0
  90. package/dist/review/reviewer/merge-impact.js +64 -0
  91. package/dist/review/reviewer/panel.d.ts +67 -0
  92. package/dist/review/reviewer/panel.js +101 -0
  93. package/dist/review/reviewer/panel.test.d.ts +1 -0
  94. package/dist/review/reviewer/panel.test.js +110 -0
  95. package/dist/review/reviewer/prompt.d.ts +32 -0
  96. package/dist/review/reviewer/prompt.js +145 -0
  97. package/dist/review/reviewer/settings.d.ts +58 -0
  98. package/dist/review/reviewer/settings.js +152 -0
  99. package/dist/review/reviewer/settings.test.d.ts +1 -0
  100. package/dist/review/reviewer/settings.test.js +57 -0
  101. package/dist/review/reviewer/store.d.ts +49 -0
  102. package/dist/review/reviewer/store.js +99 -0
  103. package/dist/review/reviewer/usage.d.ts +2 -0
  104. package/dist/review/reviewer/usage.js +28 -0
  105. package/dist/review/reviewer/usage.test.d.ts +1 -0
  106. package/dist/review/reviewer/usage.test.js +14 -0
  107. package/dist/review/reviewer/verdict.d.ts +141 -0
  108. package/dist/review/reviewer/verdict.js +166 -0
  109. package/dist/review/reviewer.d.ts +84 -49
  110. package/dist/review/reviewer.js +364 -195
  111. package/dist/review/reviewer.test.js +316 -50
  112. package/dist/review/test-files.d.ts +5 -1
  113. package/dist/review/test-files.js +6 -2
  114. package/dist/review/toolchain.d.ts +5 -2
  115. package/dist/review/toolchain.js +89 -7
  116. package/dist/review/toolchain.test.js +45 -4
  117. package/dist/review/typed/program.d.ts +26 -0
  118. package/dist/review/typed/program.js +90 -0
  119. package/dist/review/typed/redundancy.d.ts +10 -0
  120. package/dist/review/typed/redundancy.js +378 -0
  121. package/dist/review/typed/redundancy.test.d.ts +1 -0
  122. package/dist/review/typed/redundancy.test.js +179 -0
  123. package/dist/review/typed/schema-nullability.d.ts +6 -0
  124. package/dist/review/typed/schema-nullability.js +218 -0
  125. package/dist/review/typed/schema-nullability.test.d.ts +1 -0
  126. package/dist/review/typed/schema-nullability.test.js +63 -0
  127. package/dist/review/unused-exports.js +10 -7
  128. package/dist/semantic/learn/types.d.ts +2 -2
  129. package/dist/services/adaptive-thresholds.d.ts +2 -52
  130. package/dist/services/adaptive-thresholds.js +2 -147
  131. package/dist/services/adaptive-thresholds.test.js +1 -81
  132. package/dist/settings.d.ts +14 -6
  133. package/dist/settings.js +0 -12
  134. package/dist/storage/team-diagnostics.js +1 -1
  135. package/dist/storage/team-schema.d.ts +1 -1
  136. package/dist/storage/team-schema.js +2 -0
  137. package/dist/storage/team-store.js +26 -3
  138. package/dist/storage/team-sync-scope.test.js +35 -2
  139. package/dist/telemetry/telemetry.d.ts +5 -0
  140. package/dist/telemetry/telemetry.js +11 -1
  141. package/dist/templates/paradigms.js +0 -8
  142. package/dist/templates/presets.d.ts +0 -1
  143. package/dist/templates/presets.js +0 -23
  144. package/dist/templates/universal-config.js +16 -46
  145. package/dist/types/index.d.ts +202 -574
  146. package/dist/types/index.js +64 -67
  147. package/dist/types/index.test.d.ts +1 -0
  148. package/dist/types/index.test.js +33 -0
  149. package/dist/utils/command-line.d.ts +8 -0
  150. package/dist/utils/command-line.js +8 -0
  151. package/dist/utils/command-line.test.d.ts +1 -0
  152. package/dist/utils/command-line.test.js +8 -0
  153. package/dist/utils/profile.js +1 -1
  154. package/package.json +6 -7
@@ -5,18 +5,4 @@
5
5
  * has its own hook format. These types unify the config generation.
6
6
  *
7
7
  */
8
- /** The fast gates that can run per-file in <200ms */
9
- export const FAST_GATE_IDS = [
10
- 'file-guard',
11
- 'governance',
12
- 'hallucinated-imports',
13
- 'promise-safety',
14
- 'security-patterns',
15
- 'file-size',
16
- ];
17
- export const DEFAULT_HOOK_CONFIG = {
18
- tools: ['claude'],
19
- fast_gates: [...FAST_GATE_IDS],
20
- timeout_ms: 5000,
21
- block_on_failure: false,
22
- };
8
+ export {};
package/dist/index.d.ts CHANGED
@@ -12,17 +12,25 @@ export * from './utils/logger.js';
12
12
  export { normalizeScopePatterns, isScoped } from './utils/scope.js';
13
13
  export { deepAnalysisError } from './utils/deep-status.js';
14
14
  export { rigourUserDir, repoStateDir } from './utils/user-state.js';
15
+ export { locateTransformers, semanticRuntimeDir, semanticRuntimeInstalled, TRANSFORMERS_SPEC } from './pattern-index/semantic-runtime.js';
15
16
  export { reviewChange, toReviewFinding, type ReviewInput, type ReviewResult, type ReviewFinding } from './review/review.js';
16
17
  export { splitByChangedLines, type ChangedLineSplit } from './review/changed-lines.js';
17
18
  export { isControlFile, mergeBaseOf, readStateFile } from './review/trusted-state.js';
18
- export { dismissFinding, dismissedKeys, findingKey, isProven, quietSplit, DISMISSED_FILE } from './review/quiet.js';
19
+ export { dismissFinding, dismissedKeys, findingKey, isProven, mustFix, quietSplit, DISMISSED_FILE } from './review/quiet.js';
20
+ export { loadLedger, ledgerProblems, runBacktest, backtestPassed, formatBacktest, LEDGER_PATH, type Ledger, type RoundResult } from './review/backtest.js';
21
+ export { scaffoldLedger } from './review/backtest-init.js';
22
+ export { fixScope, type FixScope } from './review/fix-scope.js';
19
23
  export { isGeneratedFile } from './review/generated-files.js';
20
- export { countUsage, durationBucket, flushDailyUsage, isTelemetryEnabled, readTelemetryState, setTelemetryEnabled, shouldAskTelemetry, telemetryToken, trackUsage } from './telemetry/telemetry.js';
24
+ export { costBucket, countUsage, doNotTrack, durationBucket, flushDailyUsage, isTelemetryEnabled, readTelemetryState, setTelemetryEnabled, shouldAskTelemetry, telemetryToken, trackUsage } from './telemetry/telemetry.js';
21
25
  export { buildReviewTask, type ReviewTask, type ReviewTaskItem } from './review/review-task.js';
22
26
  export { buildQualityReceipt, receiptReport, type QualityReceipt, type ReceiptGap, type ReceiptOptions } from './review/receipt.js';
23
27
  export { runToolchain, type ToolResult, type ToolStatus } from './review/toolchain.js';
24
28
  export { branchFailures, mergeConflicts, staleReferences } from './review/branch-checks.js';
25
- export { runReviewer, reviewerBlocks, type ReviewerResult, type ReviewerVerdict } from './review/reviewer.js';
29
+ export { runReviewer, reviewerBlocks, itemLine, type ReviewerOptions, type ReviewerResult, type ReviewerOutcome, type OpenItem } from './review/reviewer.js';
30
+ export { startBackgroundReview, backgroundReview, reviewStatus, type ReviewStatus } from './review/reviewer/background.js';
31
+ export { dismissReviewerFinding, REVIEW_DISMISSALS, reviewerInputs } from './review/reviewer/context.js';
32
+ export { resolveReviewer, saveUserReviewer, type ResolvedReviewer, type RunChoice, type UserReviewerPatch } from './review/reviewer/settings.js';
33
+ export { reviewerAvailability } from './review/reviewer/adapters.js';
26
34
  export { branchBase, type BranchBase } from './gates/logic-drift-git-base.js';
27
35
  export { acknowledgeReview, type ReviewAck, type AckResult } from './review/review-ack.js';
28
36
  export { exportReviewed, readLedger, reviewedKeys, isReviewed, recordReview, REVIEWED_FILE, type LedgerEntry, type ReviewedKey, type ReviewVerdict } from './review/ledger.js';
package/dist/index.js CHANGED
@@ -12,17 +12,25 @@ export * from './utils/logger.js';
12
12
  export { normalizeScopePatterns, isScoped } from './utils/scope.js';
13
13
  export { deepAnalysisError } from './utils/deep-status.js';
14
14
  export { rigourUserDir, repoStateDir } from './utils/user-state.js';
15
+ export { locateTransformers, semanticRuntimeDir, semanticRuntimeInstalled, TRANSFORMERS_SPEC } from './pattern-index/semantic-runtime.js';
15
16
  export { reviewChange, toReviewFinding } from './review/review.js';
16
17
  export { splitByChangedLines } from './review/changed-lines.js';
17
18
  export { isControlFile, mergeBaseOf, readStateFile } from './review/trusted-state.js';
18
- export { dismissFinding, dismissedKeys, findingKey, isProven, quietSplit, DISMISSED_FILE } from './review/quiet.js';
19
+ export { dismissFinding, dismissedKeys, findingKey, isProven, mustFix, quietSplit, DISMISSED_FILE } from './review/quiet.js';
20
+ export { loadLedger, ledgerProblems, runBacktest, backtestPassed, formatBacktest, LEDGER_PATH } from './review/backtest.js';
21
+ export { scaffoldLedger } from './review/backtest-init.js';
22
+ export { fixScope } from './review/fix-scope.js';
19
23
  export { isGeneratedFile } from './review/generated-files.js';
20
- export { countUsage, durationBucket, flushDailyUsage, isTelemetryEnabled, readTelemetryState, setTelemetryEnabled, shouldAskTelemetry, telemetryToken, trackUsage } from './telemetry/telemetry.js';
24
+ export { costBucket, countUsage, doNotTrack, durationBucket, flushDailyUsage, isTelemetryEnabled, readTelemetryState, setTelemetryEnabled, shouldAskTelemetry, telemetryToken, trackUsage } from './telemetry/telemetry.js';
21
25
  export { buildReviewTask } from './review/review-task.js';
22
26
  export { buildQualityReceipt, receiptReport } from './review/receipt.js';
23
27
  export { runToolchain } from './review/toolchain.js';
24
28
  export { branchFailures, mergeConflicts, staleReferences } from './review/branch-checks.js';
25
- export { runReviewer, reviewerBlocks } from './review/reviewer.js';
29
+ export { runReviewer, reviewerBlocks, itemLine } from './review/reviewer.js';
30
+ export { startBackgroundReview, backgroundReview, reviewStatus } from './review/reviewer/background.js';
31
+ export { dismissReviewerFinding, REVIEW_DISMISSALS, reviewerInputs } from './review/reviewer/context.js';
32
+ export { resolveReviewer, saveUserReviewer } from './review/reviewer/settings.js';
33
+ export { reviewerAvailability } from './review/reviewer/adapters.js';
26
34
  export { branchBase } from './gates/logic-drift-git-base.js';
27
35
  export { acknowledgeReview } from './review/review-ack.js';
28
36
  export { exportReviewed, readLedger, reviewedKeys, isReviewed, recordReview, REVIEWED_FILE } from './review/ledger.js';
@@ -41,5 +41,7 @@ export declare class CloudProvider implements InferenceProvider {
41
41
  /** OpenRouter forwards Anthropic's cache_control to Claude models. */
42
42
  private cachesPrompts;
43
43
  private isOpenRouter;
44
+ /** The API base URL: the one given, else the provider's own. */
45
+ private endpoint;
44
46
  dispose(): void;
45
47
  }
@@ -1,8 +1,8 @@
1
1
  import { priceTokens } from './pricing.js';
2
2
  /** Default models per provider (user can override via model_name) */
3
3
  const DEFAULT_MODELS = {
4
- claude: 'claude-opus-4-6',
5
- anthropic: 'claude-sonnet-4-6',
4
+ claude: 'claude-sonnet-5-5',
5
+ anthropic: 'claude-sonnet-5-5',
6
6
  openai: 'gpt-5-mini',
7
7
  gemini: 'gemini-2.5-flash',
8
8
  groq: 'llama-3.3-70b-versatile',
@@ -24,6 +24,7 @@ const DEFAULT_BASE_URLS = {
24
24
  fireworks: 'https://api.fireworks.ai/inference/v1',
25
25
  deepseek: 'https://api.deepseek.com/v1',
26
26
  perplexity: 'https://api.perplexity.ai',
27
+ openrouter: 'https://openrouter.ai/api/v1',
27
28
  ollama: 'http://localhost:11434/v1',
28
29
  lmstudio: 'http://localhost:1234/v1',
29
30
  };
@@ -43,7 +44,11 @@ export class CloudProvider {
43
44
  this.providerName = providerName.toLowerCase();
44
45
  this.apiKey = apiKey.trim();
45
46
  this.baseUrl = options?.baseUrl;
46
- this.modelName = options?.modelName || DEFAULT_MODELS[this.providerName] || 'gpt-4o-mini';
47
+ const model = options?.modelName || DEFAULT_MODELS[this.providerName];
48
+ // No guessed model: a default that does not exist on this provider fails every call, or bills the wrong one.
49
+ if (!model)
50
+ throw new Error(`No default model for provider "${this.providerName}": pass --model-name (for example rigour settings set deep.defaultModel <name>).`);
51
+ this.modelName = model;
47
52
  this.isClaude = this.providerName === 'claude' || this.providerName === 'anthropic';
48
53
  this.name = `cloud-${this.providerName}`;
49
54
  }
@@ -54,7 +59,8 @@ export class CloudProvider {
54
59
  if (this.isClaude) {
55
60
  try {
56
61
  const { default: Anthropic } = await import('@anthropic-ai/sdk');
57
- this.client = new Anthropic({ apiKey: this.apiKey });
62
+ // A base URL given for Claude (a proxy, a gateway) is honoured like any other provider's.
63
+ this.client = new Anthropic({ apiKey: this.apiKey, ...(this.baseUrl ? { baseURL: this.baseUrl } : {}) });
58
64
  onProgress?.(`✓ ${this.providerName} API connected (model: ${this.modelName})`);
59
65
  }
60
66
  catch {
@@ -68,7 +74,7 @@ export class CloudProvider {
68
74
  // No limitations. User's key, user's choice.
69
75
  try {
70
76
  const { default: OpenAI } = await import('openai');
71
- const baseURL = this.baseUrl || DEFAULT_BASE_URLS[this.providerName] || undefined;
77
+ const baseURL = this.endpoint() || undefined;
72
78
  this.client = new OpenAI({
73
79
  apiKey: this.apiKey,
74
80
  ...(baseURL ? { baseURL } : {}),
@@ -187,7 +193,11 @@ export class CloudProvider {
187
193
  return this.isOpenRouter() && /(^|\/)(anthropic\/|claude)/i.test(this.modelName);
188
194
  }
189
195
  isOpenRouter() {
190
- return /(^|\.)openrouter\.ai(\/|$)/.test((this.baseUrl || '').replace(/^https?:\/\//, ''));
196
+ return /(^|\.)openrouter\.ai(\/|$)/.test(this.endpoint().replace(/^https?:\/\//, ''));
197
+ }
198
+ /** The API base URL: the one given, else the provider's own. */
199
+ endpoint() {
200
+ return this.baseUrl || DEFAULT_BASE_URLS[this.providerName] || '';
191
201
  }
192
202
  dispose() {
193
203
  this.client = null;
@@ -8,6 +8,10 @@ vi.mock('openai', () => ({
8
8
  }));
9
9
  vi.mock('@anthropic-ai/sdk', () => ({
10
10
  default: class {
11
+ options;
12
+ constructor(options) {
13
+ this.options = options;
14
+ }
11
15
  messages = { create: anthropicCreate };
12
16
  },
13
17
  }));
@@ -18,6 +22,23 @@ beforeEach(() => {
18
22
  anthropicCreate.mockReset();
19
23
  });
20
24
  describe('CloudProvider', () => {
25
+ it('names no model it cannot know: a provider without a default needs one', () => {
26
+ expect(() => new CloudProvider('somecloud', 'k')).toThrow(/pass --model-name/);
27
+ expect(() => new CloudProvider('somecloud', 'k', { modelName: 'x' })).not.toThrow();
28
+ });
29
+ it('sends Claude calls to a base URL when one is given', async () => {
30
+ anthropicCreate.mockResolvedValue({ content: [{ type: 'text', text: 'ok' }], usage: { input_tokens: 1, output_tokens: 1 } });
31
+ const provider = new CloudProvider('claude', 'k', { modelName: 'claude-sonnet-5-5', baseUrl: 'https://gateway.example/anthropic' });
32
+ await provider.setup();
33
+ expect(provider.client.options).toMatchObject({ baseURL: 'https://gateway.example/anthropic' });
34
+ });
35
+ it('sends OpenRouter calls to OpenRouter, asking it to report their cost', async () => {
36
+ openaiCreate.mockResolvedValue({ choices: [{ message: { content: 'ok' } }], usage: { prompt_tokens: 1, completion_tokens: 1 } });
37
+ const provider = new CloudProvider('openrouter', 'k', { modelName: 'anthropic/claude-sonnet-5.5' });
38
+ await provider.setup();
39
+ await provider.analyze('prompt');
40
+ expect(openaiCreate.mock.calls[0][0]).toMatchObject({ usage: { include: true } });
41
+ });
21
42
  it('passes the per-call timeout to the SDK request', async () => {
22
43
  anthropicCreate.mockResolvedValue({ content: [{ type: 'text', text: 'ok' }], usage: { input_tokens: 10, output_tokens: 2 } });
23
44
  const provider = new CloudProvider('claude', 'k', { modelName: 'claude-sonnet-5-5' });
@@ -1,8 +1,3 @@
1
- /**
2
- * Semantic Embedding Service
3
- *
4
- * Uses Transformers.js for local vector embeddings.
5
- */
6
1
  /**
7
2
  * Generate an embedding for a piece of text.
8
3
  */
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Semantic Embedding Service
3
3
  *
4
- * Uses Transformers.js for local vector embeddings.
4
+ * Uses Transformers.js for local vector embeddings, loaded from wherever it is installed
5
+ * (semantic-runtime.ts). Without it, embeddings are empty and callers fall back to keywords.
5
6
  */
7
+ import { loadTransformers } from './semantic-runtime.js';
6
8
  /**
7
9
  * Singleton for the embedding pipeline to avoid re-loading the model.
8
10
  */
@@ -26,10 +28,15 @@ async function getPipeline() {
26
28
  return null;
27
29
  if (!embeddingPipeline) {
28
30
  try {
29
- // Dynamic import to isolate native dependency issues (like sharp)
30
- const { pipeline } = await import('@xenova/transformers');
31
+ // Loaded where it is installed; never a dependency of the npm package (semantic-runtime.ts).
32
+ const transformers = await loadTransformers();
33
+ if (!transformers) {
34
+ // Not installed: keyword matching, said once by `rigour doctor`, not on every run.
35
+ embeddingUnavailable = true;
36
+ return null;
37
+ }
31
38
  // Using a compact but high-quality model for local embeddings
32
- embeddingPipeline = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
39
+ embeddingPipeline = await transformers.pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
33
40
  }
34
41
  catch (error) {
35
42
  embeddingUnavailable = true;
@@ -0,0 +1,10 @@
1
+ /** The version installed by `rigour setup`; the model it loads is fetched once on first use. */
2
+ export declare const TRANSFORMERS_SPEC = "@xenova/transformers@2.17.2";
3
+ /** Rigour's own copy, shared by every Rigour version on the machine (per profile home). */
4
+ export declare function semanticRuntimeDir(): string;
5
+ /** Where the library resolves from, or undefined when it is not installed anywhere Rigour looks. */
6
+ export declare function locateTransformers(cwd?: string): string | undefined;
7
+ /** The library's module, loaded from wherever it is installed. */
8
+ export declare function loadTransformers(cwd?: string): Promise<any | undefined>;
9
+ /** Whether Rigour's home has its own copy (what `rigour setup` installs). */
10
+ export declare function semanticRuntimeInstalled(): boolean;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Where the local embedding library (`@xenova/transformers`, with its ONNX runtime about 230 MB)
3
+ * comes from. It is not a dependency of the npm package: npm installed it again for every
4
+ * version a hook pinned, so a machine kept one copy per Rigour version. `rigour setup` installs it
5
+ * once into Rigour's home, where every version finds it.
6
+ *
7
+ * Search order: next to Rigour itself (a checkout, or a project that installs Rigour and the
8
+ * library as devDependencies), the project being worked on, then Rigour's home. Without it,
9
+ * recall and pattern matching fall back to keywords, which find far less (on the committed eval
10
+ * sets, 6% of memories and 20% of existing helpers, against 81% and 93% by meaning).
11
+ */
12
+ import { createRequire } from 'module';
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import { pathToFileURL } from 'url';
16
+ import { rigourUserDir } from '../utils/user-state.js';
17
+ /** The version installed by `rigour setup`; the model it loads is fetched once on first use. */
18
+ export const TRANSFORMERS_SPEC = '@xenova/transformers@2.17.2';
19
+ const PACKAGE = '@xenova/transformers';
20
+ /** Rigour's own copy, shared by every Rigour version on the machine (per profile home). */
21
+ export function semanticRuntimeDir() {
22
+ return path.join(rigourUserDir(), 'runtime', 'semantic');
23
+ }
24
+ /** Where the library resolves from, or undefined when it is not installed anywhere Rigour looks. */
25
+ export function locateTransformers(cwd = process.cwd()) {
26
+ for (const base of [import.meta.url, path.join(cwd, 'package.json'), path.join(semanticRuntimeDir(), 'package.json')]) {
27
+ try {
28
+ return createRequire(base).resolve(PACKAGE);
29
+ }
30
+ catch {
31
+ // not there
32
+ }
33
+ }
34
+ return undefined;
35
+ }
36
+ /** The library's module, loaded from wherever it is installed. */
37
+ export async function loadTransformers(cwd = process.cwd()) {
38
+ const entry = locateTransformers(cwd);
39
+ if (!entry)
40
+ return undefined;
41
+ return import(pathToFileURL(entry).href);
42
+ }
43
+ /** Whether Rigour's home has its own copy (what `rigour setup` installs). */
44
+ export function semanticRuntimeInstalled() {
45
+ return fs.existsSync(path.join(semanticRuntimeDir(), 'node_modules', ...PACKAGE.split('/'), 'package.json'));
46
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { locateTransformers, semanticRuntimeDir, semanticRuntimeInstalled } from './semantic-runtime.js';
6
+ let home;
7
+ const original = process.env.RIGOUR_HOME;
8
+ beforeEach(() => {
9
+ home = fs.mkdtempSync(path.join(os.tmpdir(), 'semantic-home-'));
10
+ process.env.RIGOUR_HOME = home;
11
+ });
12
+ afterEach(() => {
13
+ process.env.RIGOUR_HOME = original;
14
+ fs.rmSync(home, { recursive: true, force: true });
15
+ });
16
+ describe('the semantic runtime', () => {
17
+ it('lives in Rigour\'s home, shared by every Rigour version, and is seen there once installed', () => {
18
+ expect(semanticRuntimeDir()).toBe(path.join(home, '.rigour', 'runtime', 'semantic'));
19
+ expect(semanticRuntimeInstalled()).toBe(false);
20
+ const pkg = path.join(semanticRuntimeDir(), 'node_modules', '@xenova', 'transformers');
21
+ fs.mkdirSync(pkg, { recursive: true });
22
+ fs.writeFileSync(path.join(pkg, 'package.json'), JSON.stringify({ name: '@xenova/transformers', main: 'index.js' }));
23
+ fs.writeFileSync(path.join(pkg, 'index.js'), 'module.exports = {};');
24
+ expect(semanticRuntimeInstalled()).toBe(true);
25
+ expect(locateTransformers(home)).toBeDefined();
26
+ });
27
+ it('is not a dependency of the published package: the 240 MB is installed once, never per version', () => {
28
+ const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf8'));
29
+ expect({ ...manifest.dependencies, ...manifest.optionalDependencies }).not.toHaveProperty('@xenova/transformers');
30
+ });
31
+ });
@@ -0,0 +1,8 @@
1
+ import type { Config } from '../types/index.js';
2
+ import { type LedgerRound } from './backtest.js';
3
+ import { type Exec } from './reviewer.js';
4
+ export declare function scaffoldLedger(cwd: string, pr: number, config: Config, exec?: Exec): Promise<{
5
+ file: string;
6
+ rounds: LedgerRound[];
7
+ incomplete: number;
8
+ }>;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `rigour backtest init --pr <n>`: a ledger scaffolded from a pull request's human reviews, one
3
+ * round per review. An inline comment gives a point its file and a line window for free; a point
4
+ * made in the review's body has neither, so it is written with `needs` set and a person adds the
5
+ * text pattern once. The base of each round is the main branch as it was when the review was
6
+ * posted, so a later merge does not change what the round measures.
7
+ */
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { branchBase } from '../gates/logic-drift-git-base.js';
11
+ import { LEDGER_PATH, LedgerSchema } from './backtest.js';
12
+ import { defaultExec, githubEnv, parseJsonArrays } from './reviewer.js';
13
+ const GH_TIMEOUT_MS = 60_000;
14
+ /** Lines either side of an inline comment that a finding for the same point may land on. */
15
+ const LINE_SLACK = 10;
16
+ const BULLET = /^\s*(?:[-*]|\d+[.)])\s+(.{8,})$/;
17
+ export async function scaffoldLedger(cwd, pr, config, exec = defaultExec) {
18
+ const env = await githubEnv(cwd, config.review?.github_account ?? process.env.RIGOUR_GITHUB_ACCOUNT, exec);
19
+ const gh = (args) => exec('gh', args, { cwd, timeoutMs: GH_TIMEOUT_MS, env });
20
+ const view = await gh(['pr', 'view', String(pr), '--json', 'author', '-q', '.author.login']);
21
+ if (view.exitCode !== 0)
22
+ throw new Error(`could not read pull request ${pr}: ${view.stderr.trim() || 'is gh signed in?'}`);
23
+ const author = view.stdout.trim();
24
+ const reviews = await gh(['api', `repos/{owner}/{repo}/pulls/${pr}/reviews`, '--paginate']);
25
+ if (reviews.exitCode !== 0)
26
+ throw new Error(`could not read the reviews of pull request ${pr}: ${reviews.stderr.trim()}`);
27
+ const humans = parseJsonArrays(reviews.stdout)
28
+ .filter((r) => r?.user && r.user.type !== 'Bot' && !/bot/i.test(r.user.login) && r.user.login !== author && (r.body?.trim() || r.state === 'CHANGES_REQUESTED'));
29
+ if (humans.length === 0)
30
+ throw new Error(`pull request ${pr} has no review by a person yet`);
31
+ const mainRef = branchBase(cwd)?.mainRef ?? 'origin/main';
32
+ const rounds = [];
33
+ for (const [index, review] of humans.entries()) {
34
+ const comments = await gh(['api', `repos/{owner}/{repo}/pulls/${pr}/reviews/${review.id}/comments`, '--paginate']);
35
+ const inline = comments.exitCode === 0 ? parseJsonArrays(comments.stdout) : [];
36
+ const round = index + 1;
37
+ const points = inline.map((c, i) => inlinePoint(`R${round}-${i + 1}`, c));
38
+ for (const [i, line] of bodyPoints(String(review.body ?? '')).entries()) {
39
+ points.push({ id: `R${round}-B${i + 1}`, point: line, file: '', needs: 'a file pattern and a text pattern (this point was made in the review body, not on a line)' });
40
+ }
41
+ const base = await exec('git', ['rev-list', '-1', `--before=${review.submitted_at}`, mainRef], { cwd, timeoutMs: GH_TIMEOUT_MS });
42
+ rounds.push({
43
+ id: `pr${pr}-r${round}`,
44
+ commit: String(review.commit_id),
45
+ base: base.stdout.trim() || mainRef,
46
+ reviewed_at: String(review.submitted_at),
47
+ pr,
48
+ points,
49
+ must_not_flag: [],
50
+ });
51
+ }
52
+ const file = path.join(cwd, LEDGER_PATH);
53
+ const existing = existingLedger(file);
54
+ const merged = { rounds: [...existing.rounds.filter(r => !rounds.some(n => n.id === r.id)), ...rounds] };
55
+ fs.mkdirSync(path.dirname(file), { recursive: true });
56
+ fs.writeFileSync(file, JSON.stringify(merged, null, 2) + '\n');
57
+ return { file, rounds, incomplete: rounds.flatMap(r => r.points).filter(p => p.needs).length };
58
+ }
59
+ function inlinePoint(id, comment) {
60
+ const line = Number(comment.line ?? comment.original_line ?? 0);
61
+ const body = String(comment.body ?? '').trim();
62
+ return {
63
+ id,
64
+ point: firstLine(body),
65
+ file: escape(String(comment.path ?? '')),
66
+ ...(line > 0 ? { lines: [Math.max(1, line - LINE_SLACK), line + LINE_SLACK] } : { needs: 'a line window or a text pattern (the comment has no line)' }),
67
+ };
68
+ }
69
+ /** The bullet and numbered lines of a review body: the points a reviewer makes outside any one line. */
70
+ function bodyPoints(body) {
71
+ return body.split('\n').map(l => BULLET.exec(l)?.[1]).filter((l) => !!l).map(firstLine);
72
+ }
73
+ function firstLine(text) {
74
+ const line = text.split('\n').find(l => l.trim())?.replace(/[*_`#]/g, '').trim() ?? '';
75
+ return line.length > 140 ? `${line.slice(0, 137)}...` : line;
76
+ }
77
+ function escape(text) {
78
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
79
+ }
80
+ function existingLedger(file) {
81
+ if (!fs.existsSync(file))
82
+ return { rounds: [] };
83
+ const parsed = LedgerSchema.safeParse(JSON.parse(fs.readFileSync(file, 'utf8')));
84
+ if (!parsed.success)
85
+ throw new Error(`${LEDGER_PATH} exists but is not a ledger; fix or remove it first`);
86
+ return parsed.data;
87
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,74 @@
1
+ import { execFileSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
+ import { ConfigSchema } from '../types/index.js';
7
+ import { ledgerProblems, loadLedger, LEDGER_PATH, LedgerSchema } from './backtest.js';
8
+ import { scaffoldLedger } from './backtest-init.js';
9
+ let repo;
10
+ const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' }).trim();
11
+ const config = ConfigSchema.parse({ version: 1 });
12
+ const REVIEWS = [
13
+ { id: 1, user: { login: 'ci-bot', type: 'Bot' }, body: 'automated', state: 'COMMENTED', submitted_at: '2026-04-12T10:00:00Z', commit_id: 'c1c1c1c1c1' },
14
+ { id: 2, user: { login: 'senior', type: 'User' }, body: '## Review\n\n- A retry posts the invoice twice.\n- **Quadratic** scan in `latestPayment`.\nlooks good otherwise', state: 'CHANGES_REQUESTED', submitted_at: '2026-04-12T12:00:00Z', commit_id: 'c2c2c2c2c2' },
15
+ ];
16
+ const INLINE = [
17
+ { path: 'src/lib/server/invoices.ts', line: 40, body: 'The export reads every line item for ids.\n\nNarrow it.', created_at: '2026-04-12T12:00:00Z' },
18
+ { path: 'src/checkout/total.ts', line: null, original_line: null, body: 'Drops the currency', created_at: '2026-04-12T12:00:00Z' },
19
+ ];
20
+ function fakeExec(calls) {
21
+ return async (command, args, options) => {
22
+ calls.push([command, ...args]);
23
+ if (command === 'git')
24
+ return { exitCode: 0, stdout: execFileSync('git', args, { cwd: options.cwd, encoding: 'utf8' }), stderr: '' };
25
+ if (args[0] === 'auth')
26
+ return { exitCode: 0, stdout: 'token\n', stderr: '' };
27
+ if (args[0] === 'pr')
28
+ return { exitCode: 0, stdout: 'author\n', stderr: '' };
29
+ if (args[1].endsWith('/reviews'))
30
+ return { exitCode: 0, stdout: JSON.stringify(REVIEWS), stderr: '' };
31
+ return { exitCode: 0, stdout: JSON.stringify(INLINE), stderr: '' };
32
+ };
33
+ }
34
+ beforeEach(() => {
35
+ repo = fs.mkdtempSync(path.join(os.tmpdir(), 'backtest-init-'));
36
+ git('init', '-q', '-b', 'main');
37
+ git('config', 'user.email', 't@example.com');
38
+ git('config', 'user.name', 't');
39
+ git('config', 'commit.gpgsign', 'false');
40
+ fs.writeFileSync(path.join(repo, 'a.ts'), 'export const a = 1;\n');
41
+ git('add', '-A');
42
+ execFileSync('git', ['-C', repo, 'commit', '-qm', 'main as of the review'], { env: { ...process.env, GIT_COMMITTER_DATE: '2026-04-12T09:00:00Z', GIT_AUTHOR_DATE: '2026-04-12T09:00:00Z' } });
43
+ });
44
+ afterEach(() => { fs.rmSync(repo, { recursive: true, force: true }); });
45
+ describe('rigour backtest init', () => {
46
+ it('writes one round per human review: inline comments become file-and-window points, body bullets need a pattern', async () => {
47
+ const calls = [];
48
+ const mainAtReview = git('rev-parse', 'HEAD');
49
+ const { rounds, incomplete } = await scaffoldLedger(repo, 212, config, fakeExec(calls));
50
+ expect(rounds).toHaveLength(1);
51
+ expect(rounds[0]).toMatchObject({ id: 'pr212-r1', commit: 'c2c2c2c2c2', base: mainAtReview, reviewed_at: '2026-04-12T12:00:00Z', pr: 212 });
52
+ expect(rounds[0].points).toEqual([
53
+ { id: 'R1-1', point: 'The export reads every line item for ids.', file: 'src/lib/server/invoices\\.ts', lines: [30, 50] },
54
+ { id: 'R1-2', point: 'Drops the currency', file: 'src/checkout/total\\.ts', needs: 'a line window or a text pattern (the comment has no line)' },
55
+ { id: 'R1-B1', point: 'A retry posts the invoice twice.', file: '', needs: 'a file pattern and a text pattern (this point was made in the review body, not on a line)' },
56
+ { id: 'R1-B2', point: 'Quadratic scan in latestPayment.', file: '', needs: 'a file pattern and a text pattern (this point was made in the review body, not on a line)' },
57
+ ]);
58
+ expect(incomplete).toBe(3);
59
+ expect(calls.find(c => c[0] === 'gh' && c[1] === 'pr')).toContain('212');
60
+ const written = LedgerSchema.parse(JSON.parse(fs.readFileSync(path.join(repo, LEDGER_PATH), 'utf8')));
61
+ expect(ledgerProblems(written)).toHaveLength(3);
62
+ expect(() => loadLedger(repo)).toThrow('rows a person still has to complete');
63
+ });
64
+ it('keeps rounds already in the ledger and replaces its own', async () => {
65
+ fs.mkdirSync(path.join(repo, '.rigour'));
66
+ fs.writeFileSync(path.join(repo, LEDGER_PATH), JSON.stringify({ rounds: [
67
+ { id: 'pr212-r1', commit: 'old0000', base: 'main', points: [], must_not_flag: [] },
68
+ { id: 'pr7-r1', commit: 'other00', base: 'main', points: [{ id: 'P', point: 'p', file: 'x', text: 'x' }], must_not_flag: [] },
69
+ ] }));
70
+ await scaffoldLedger(repo, 212, config, fakeExec([]));
71
+ const written = LedgerSchema.parse(JSON.parse(fs.readFileSync(path.join(repo, LEDGER_PATH), 'utf8')));
72
+ expect(written.rounds.map(r => [r.id, r.commit])).toEqual([['pr7-r1', 'other00'], ['pr212-r1', 'c2c2c2c2c2']]);
73
+ });
74
+ });
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The panel measured against the ledger, per judge: which human points each judge raised on its
3
+ * own, how often two judges agree (Cohen's kappa over the ledger's points, where the set of items
4
+ * is fixed, so agreeing on "not raised" is meaningful), and what a caught point cost. A high kappa
5
+ * means the judges share blind spots and a second one adds little; a judge whose catches the others
6
+ * always make too is not earning its runs. This decides one, two or three judges with data.
7
+ */
8
+ import type { ReviewerResult } from './reviewer.js';
9
+ /** What each judge raised unprompted in one round, with what the round cost. */
10
+ export interface JudgeRun {
11
+ judges: string[];
12
+ raised: Array<{
13
+ judge: string;
14
+ file: string;
15
+ line: number | undefined;
16
+ text: string;
17
+ }>;
18
+ costUsd?: number;
19
+ runs: number;
20
+ }
21
+ export interface JudgeCatches {
22
+ judges: string[];
23
+ /** Ledger point ids each judge raised. */
24
+ caught: Record<string, string[]>;
25
+ points: string[];
26
+ costUsd?: number;
27
+ runs: number;
28
+ }
29
+ /** Each judge's own raises: the panel's record when there was one, else the items tagged with the judge who found them. */
30
+ export declare function judgedFrom(result: ReviewerResult): JudgeRun | undefined;
31
+ /** Across rounds: each judge's catches, every pair's kappa, and the cost of a caught point. */
32
+ export declare function formatJudges(rounds: Array<{
33
+ judges?: JudgeCatches;
34
+ points: Array<{
35
+ id: string;
36
+ caught: boolean;
37
+ }>;
38
+ }>): string;
@@ -0,0 +1,63 @@
1
+ /** Each judge's own raises: the panel's record when there was one, else the items tagged with the judge who found them. */
2
+ export function judgedFrom(result) {
3
+ if (result.reviewers.length < 2)
4
+ return undefined;
5
+ const raised = [];
6
+ if (result.panel) {
7
+ for (const d of result.panel) {
8
+ for (const judge of d.judges) {
9
+ const own = d.members?.find(m => m.judge === judge)?.item ?? d.item;
10
+ raised.push({ judge, file: own.file ?? '', line: own.line, text: [own.issue, own.consequence, own.evidence].filter(Boolean).join(' ') });
11
+ }
12
+ }
13
+ }
14
+ else {
15
+ for (const item of [...result.items, ...result.unverified, ...result.notes]) {
16
+ if (item.kind === 'prior' || !item.reviewer)
17
+ continue;
18
+ for (const judge of item.reviewer.split('+'))
19
+ raised.push({ judge, file: item.file ?? '', line: item.line, text: [item.issue, item.consequence, item.evidence].filter(Boolean).join(' ') });
20
+ }
21
+ }
22
+ return { judges: result.reviewers, raised, ...(result.costUsd !== undefined ? { costUsd: result.costUsd } : {}), runs: result.runs ?? result.reviewers.length };
23
+ }
24
+ /** Cohen's kappa for two raters' yes/no calls on the same items; 1 when they agree on everything, 0 at chance. */
25
+ function cohensKappa(a, b) {
26
+ const n = a.length;
27
+ if (n === 0 || n !== b.length)
28
+ return Number.NaN;
29
+ const agree = a.filter((x, i) => x === b[i]).length / n;
30
+ const pa = a.filter(Boolean).length / n;
31
+ const pb = b.filter(Boolean).length / n;
32
+ const chance = pa * pb + (1 - pa) * (1 - pb);
33
+ return chance === 1 ? (agree === 1 ? 1 : 0) : (agree - chance) / (1 - chance);
34
+ }
35
+ /** Across rounds: each judge's catches, every pair's kappa, and the cost of a caught point. */
36
+ export function formatJudges(rounds) {
37
+ const judged = rounds.map(r => r.judges).filter((j) => !!j);
38
+ if (!judged.length)
39
+ return '';
40
+ const names = [...new Set(judged.flatMap(j => j.judges))];
41
+ const total = judged.reduce((sum, j) => sum + j.points.length, 0);
42
+ const lines = ['', `Judges (${judged.length} round(s), ${total} human point(s))`];
43
+ for (const name of names) {
44
+ const caught = judged.reduce((sum, j) => sum + (j.caught[name]?.length ?? 0), 0);
45
+ const only = judged.reduce((sum, j) => sum + (j.caught[name] ?? []).filter(id => j.judges.filter(o => o !== name).every(o => !(j.caught[o] ?? []).includes(id))).length, 0);
46
+ lines.push(` ${name}: raised ${caught}/${total}; ${only} that no other judge raised`);
47
+ }
48
+ for (let i = 0; i < names.length; i++) {
49
+ for (let k = i + 1; k < names.length; k++) {
50
+ const both = judged.filter(j => j.judges.includes(names[i]) && j.judges.includes(names[k]));
51
+ const a = both.flatMap(j => j.points.map(p => (j.caught[names[i]] ?? []).includes(p)));
52
+ const b = both.flatMap(j => j.points.map(p => (j.caught[names[k]] ?? []).includes(p)));
53
+ const kappa = cohensKappa(a, b);
54
+ if (a.length)
55
+ lines.push(` ${names[i]} and ${names[k]}: kappa ${Number.isNaN(kappa) ? 'n/a' : kappa.toFixed(2)} over ${a.length} point(s)${kappa > 0.8 ? ' (they share blind spots: a second judge adds little)' : ''}`);
56
+ }
57
+ }
58
+ const cost = judged.reduce((sum, j) => sum + (j.costUsd ?? 0), 0);
59
+ const runs = judged.reduce((sum, j) => sum + j.runs, 0);
60
+ const caughtPoints = rounds.filter(r => r.judges).reduce((sum, r) => sum + r.points.filter(p => p.caught).length, 0);
61
+ lines.push(` ${runs} agent run(s)${cost ? `, $${cost.toFixed(2)} recorded` : ''}${cost && caughtPoints ? `, $${(cost / caughtPoints).toFixed(2)} per caught point` : ''}`);
62
+ return lines.join('\n');
63
+ }
@@ -0,0 +1 @@
1
+ export {};