@rigour-labs/core 6.6.6 → 6.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) 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.test.js +2 -2
  12. package/dist/gates/hallucinated-imports/index.d.ts +0 -2
  13. package/dist/gates/hallucinated-imports/index.js +2 -2
  14. package/dist/gates/hallucinated-imports/js-resolver.js +1 -0
  15. package/dist/gates/hallucinated-imports.test.js +10 -0
  16. package/dist/gates/runner.js +3 -4
  17. package/dist/gates/test-quality-matchers.d.ts +0 -4
  18. package/dist/gates/test-quality-matchers.js +0 -7
  19. package/dist/gates/test-quality.d.ts +0 -2
  20. package/dist/gates/test-quality.js +1 -10
  21. package/dist/hooks/checker.js +1 -1
  22. package/dist/hooks/index.d.ts +1 -2
  23. package/dist/hooks/index.js +0 -1
  24. package/dist/hooks/types.d.ts +0 -14
  25. package/dist/hooks/types.js +1 -15
  26. package/dist/index.d.ts +6 -1
  27. package/dist/index.js +6 -1
  28. package/dist/inference/cloud-provider.d.ts +2 -0
  29. package/dist/inference/cloud-provider.js +16 -6
  30. package/dist/inference/cloud-provider.test.js +21 -0
  31. package/dist/pattern-index/embeddings.d.ts +0 -5
  32. package/dist/pattern-index/embeddings.js +11 -4
  33. package/dist/pattern-index/semantic-runtime.d.ts +10 -0
  34. package/dist/pattern-index/semantic-runtime.js +46 -0
  35. package/dist/pattern-index/semantic-runtime.test.d.ts +1 -0
  36. package/dist/pattern-index/semantic-runtime.test.js +31 -0
  37. package/dist/review/backtest-judges.d.ts +38 -0
  38. package/dist/review/backtest-judges.js +63 -0
  39. package/dist/review/backtest-judges.test.d.ts +1 -0
  40. package/dist/review/backtest-judges.test.js +32 -0
  41. package/dist/review/backtest.d.ts +9 -4
  42. package/dist/review/backtest.js +19 -9
  43. package/dist/review/backtest.test.js +2 -2
  44. package/dist/review/baseline.js +2 -1
  45. package/dist/review/dead-code.test.js +7 -0
  46. package/dist/review/fix-scope.d.ts +21 -0
  47. package/dist/review/fix-scope.js +94 -0
  48. package/dist/review/fix-scope.test.d.ts +1 -0
  49. package/dist/review/fix-scope.test.js +104 -0
  50. package/dist/review/lint-overlay.js +1 -1
  51. package/dist/review/orphan-files.js +3 -2
  52. package/dist/review/partial-wiring.js +5 -3
  53. package/dist/review/query-patterns.js +3 -2
  54. package/dist/review/review.js +2 -1
  55. package/dist/review/reviewer/adapters.d.ts +23 -7
  56. package/dist/review/reviewer/adapters.js +32 -7
  57. package/dist/review/reviewer/adapters.test.d.ts +1 -0
  58. package/dist/review/reviewer/adapters.test.js +23 -0
  59. package/dist/review/reviewer/background.d.ts +8 -1
  60. package/dist/review/reviewer/background.js +34 -9
  61. package/dist/review/reviewer/background.test.js +16 -2
  62. package/dist/review/reviewer/consensus.d.ts +36 -0
  63. package/dist/review/reviewer/consensus.js +144 -0
  64. package/dist/review/reviewer/context.d.ts +75 -0
  65. package/dist/review/reviewer/context.js +140 -0
  66. package/dist/review/reviewer/context.test.d.ts +1 -0
  67. package/dist/review/reviewer/context.test.js +18 -0
  68. package/dist/review/reviewer/exec.js +6 -2
  69. package/dist/review/reviewer/exec.test.d.ts +1 -0
  70. package/dist/review/reviewer/exec.test.js +23 -0
  71. package/dist/review/reviewer/inputs.d.ts +2 -0
  72. package/dist/review/reviewer/inputs.js +5 -1
  73. package/dist/review/reviewer/panel.d.ts +67 -0
  74. package/dist/review/reviewer/panel.js +101 -0
  75. package/dist/review/reviewer/panel.test.d.ts +1 -0
  76. package/dist/review/reviewer/panel.test.js +110 -0
  77. package/dist/review/reviewer/prompt.d.ts +11 -0
  78. package/dist/review/reviewer/prompt.js +26 -4
  79. package/dist/review/reviewer/settings.d.ts +58 -0
  80. package/dist/review/reviewer/settings.js +152 -0
  81. package/dist/review/reviewer/settings.test.d.ts +1 -0
  82. package/dist/review/reviewer/settings.test.js +57 -0
  83. package/dist/review/reviewer/store.d.ts +25 -2
  84. package/dist/review/reviewer/store.js +43 -1
  85. package/dist/review/reviewer/usage.d.ts +2 -0
  86. package/dist/review/reviewer/usage.js +28 -0
  87. package/dist/review/reviewer/usage.test.d.ts +1 -0
  88. package/dist/review/reviewer/usage.test.js +14 -0
  89. package/dist/review/reviewer/verdict.d.ts +15 -1
  90. package/dist/review/reviewer/verdict.js +17 -7
  91. package/dist/review/reviewer.d.ts +45 -2
  92. package/dist/review/reviewer.js +186 -25
  93. package/dist/review/reviewer.test.js +127 -12
  94. package/dist/review/test-files.d.ts +5 -1
  95. package/dist/review/test-files.js +6 -2
  96. package/dist/review/toolchain.js +10 -3
  97. package/dist/review/toolchain.test.js +6 -3
  98. package/dist/review/typed/program.d.ts +0 -1
  99. package/dist/review/typed/program.js +3 -3
  100. package/dist/review/typed/redundancy.js +43 -4
  101. package/dist/review/typed/redundancy.test.js +58 -0
  102. package/dist/review/typed/schema-nullability.d.ts +6 -0
  103. package/dist/review/typed/schema-nullability.js +218 -0
  104. package/dist/review/typed/schema-nullability.test.d.ts +1 -0
  105. package/dist/review/typed/schema-nullability.test.js +63 -0
  106. package/dist/review/unused-exports.js +5 -6
  107. package/dist/semantic/learn/types.d.ts +2 -2
  108. package/dist/services/adaptive-thresholds.d.ts +2 -52
  109. package/dist/services/adaptive-thresholds.js +2 -147
  110. package/dist/services/adaptive-thresholds.test.js +1 -81
  111. package/dist/settings.d.ts +14 -6
  112. package/dist/settings.js +0 -12
  113. package/dist/storage/team-diagnostics.js +1 -1
  114. package/dist/storage/team-schema.d.ts +1 -1
  115. package/dist/storage/team-schema.js +2 -0
  116. package/dist/storage/team-store.js +26 -3
  117. package/dist/storage/team-sync-scope.test.js +35 -2
  118. package/dist/telemetry/telemetry.d.ts +5 -0
  119. package/dist/telemetry/telemetry.js +11 -1
  120. package/dist/templates/paradigms.js +0 -8
  121. package/dist/templates/presets.d.ts +0 -1
  122. package/dist/templates/presets.js +0 -23
  123. package/dist/templates/universal-config.js +8 -45
  124. package/dist/types/index.d.ts +108 -566
  125. package/dist/types/index.js +41 -66
  126. package/dist/types/index.test.d.ts +1 -0
  127. package/dist/types/index.test.js +33 -0
  128. package/dist/utils/command-line.d.ts +8 -0
  129. package/dist/utils/command-line.js +8 -0
  130. package/dist/utils/command-line.test.d.ts +1 -0
  131. package/dist/utils/command-line.test.js +8 -0
  132. package/dist/utils/profile.js +1 -1
  133. package/package.json +6 -7
@@ -1,17 +1,6 @@
1
1
  /**
2
- * Adaptive Thresholds Service (v2)
3
- *
4
- * Dynamically adjusts quality gate thresholds based on:
5
- * - Project maturity (age, commit count, file count)
6
- * - Historical failure rates with Z-score anomaly detection
7
- * - Complexity tier (hobby/startup/enterprise)
8
- * - Per-provenance trend analysis (ai-drift, structural, security separate)
9
- *
10
- * v2 upgrades:
11
- * - Z-score replaces naive delta comparison for trend detection
12
- * - Per-provenance failure tracking (AI drift vs structural vs security)
13
- * - Statistical anomaly detection normalizes across project sizes
14
- *
2
+ * Quality history: every gate run's failures by provenance, kept in .rigour/adaptive-history.json, and the
3
+ * trends read from it (Z-score of recent runs against the earlier baseline), overall and per provenance.
15
4
  */
16
5
  import * as fs from 'fs';
17
6
  import * as path from 'path';
@@ -182,123 +171,6 @@ export function getProvenanceTrends(cwd) {
182
171
  securityZScore: security.z,
183
172
  };
184
173
  }
185
- /**
186
- * Detect project complexity tier based on metrics
187
- */
188
- export function detectComplexityTier(metrics) {
189
- // Enterprise: Large teams, many files, mature codebase
190
- if (metrics.fileCount > 500 || (metrics.commitCount && metrics.commitCount > 1000)) {
191
- return 'enterprise';
192
- }
193
- // Startup: Growing codebase, active development
194
- if (metrics.fileCount > 50 || (metrics.commitCount && metrics.commitCount > 100)) {
195
- return 'startup';
196
- }
197
- // Hobby: Small projects, early stage
198
- return 'hobby';
199
- }
200
- /**
201
- * Calculate adaptive thresholds based on project state.
202
- * v5: Uses Z-score trending and per-provenance analysis.
203
- */
204
- export function calculateAdaptiveThresholds(cwd, metrics, config = {}) {
205
- const reasoning = [];
206
- // Determine tier
207
- const tier = config.forced_tier ??
208
- (config.auto_detect_tier !== false ? detectComplexityTier(metrics) : 'startup');
209
- reasoning.push(`Complexity tier: ${tier} (files: ${metrics.fileCount})`);
210
- // Get overall trend (Z-score based)
211
- const trend = getQualityTrend(cwd);
212
- reasoning.push(`Quality trend: ${trend} (Z-score analysis)`);
213
- // Get per-provenance trends
214
- const provenanceTrends = getProvenanceTrends(cwd);
215
- if (provenanceTrends.aiDrift !== 'stable') {
216
- reasoning.push(`AI drift trend: ${provenanceTrends.aiDrift} (Z=${provenanceTrends.aiDriftZScore})`);
217
- }
218
- if (provenanceTrends.structural !== 'stable') {
219
- reasoning.push(`Structural trend: ${provenanceTrends.structural} (Z=${provenanceTrends.structuralZScore})`);
220
- }
221
- if (provenanceTrends.security !== 'stable') {
222
- reasoning.push(`Security trend: ${provenanceTrends.security} (Z=${provenanceTrends.securityZScore})`);
223
- }
224
- // Base thresholds
225
- let coverageThreshold = config.base_coverage_threshold ?? 80;
226
- let qualityThreshold = config.base_quality_threshold ?? 80;
227
- let securityBlockLevel = 'high';
228
- let leniencyFactor = 0.5;
229
- // Adjust by tier
230
- switch (tier) {
231
- case 'hobby':
232
- coverageThreshold = Math.max(50, coverageThreshold - 30);
233
- qualityThreshold = Math.max(60, qualityThreshold - 20);
234
- securityBlockLevel = 'critical';
235
- leniencyFactor = 0.8;
236
- reasoning.push('Hobby tier: relaxed thresholds, only critical security blocks');
237
- break;
238
- case 'startup':
239
- coverageThreshold = Math.max(60, coverageThreshold - 15);
240
- qualityThreshold = Math.max(70, qualityThreshold - 10);
241
- securityBlockLevel = 'high';
242
- leniencyFactor = 0.5;
243
- reasoning.push('Startup tier: moderate thresholds, high+ security blocks');
244
- break;
245
- case 'enterprise':
246
- coverageThreshold = coverageThreshold;
247
- qualityThreshold = qualityThreshold;
248
- securityBlockLevel = 'medium';
249
- leniencyFactor = 0.2;
250
- reasoning.push('Enterprise tier: strict thresholds, medium+ security blocks');
251
- break;
252
- }
253
- // Adjust by overall trend
254
- if (trend === 'improving') {
255
- coverageThreshold = Math.max(50, coverageThreshold - 5);
256
- qualityThreshold = Math.max(60, qualityThreshold - 5);
257
- leniencyFactor = Math.min(1, leniencyFactor + 0.1);
258
- reasoning.push('Improving trend: bonus threshold relaxation (+5%)');
259
- }
260
- else if (trend === 'degrading') {
261
- coverageThreshold = Math.min(95, coverageThreshold + 5);
262
- qualityThreshold = Math.min(95, qualityThreshold + 5);
263
- leniencyFactor = Math.max(0, leniencyFactor - 0.1);
264
- reasoning.push('Degrading trend: tightened thresholds (-5%)');
265
- }
266
- // v5: Per-provenance adjustments
267
- // If AI drift is degrading but structural is stable, tighten AI-specific gates
268
- if (provenanceTrends.aiDrift === 'degrading' && provenanceTrends.structural !== 'degrading') {
269
- leniencyFactor = Math.max(0, leniencyFactor - 0.15);
270
- reasoning.push('AI drift degrading while structural stable: AI is the problem, tightening AI gates');
271
- }
272
- // If security is degrading, escalate security block level
273
- if (provenanceTrends.security === 'degrading') {
274
- if (securityBlockLevel === 'critical')
275
- securityBlockLevel = 'high';
276
- else if (securityBlockLevel === 'high')
277
- securityBlockLevel = 'medium';
278
- reasoning.push(`Security trend degrading: escalated block level to ${securityBlockLevel}+`);
279
- }
280
- // Recent failure rate adjustment
281
- if (metrics.recentFailureRate !== undefined) {
282
- if (metrics.recentFailureRate > 50) {
283
- leniencyFactor = Math.min(1, leniencyFactor + 0.2);
284
- reasoning.push(`High failure rate (${metrics.recentFailureRate.toFixed(0)}%): increased leniency`);
285
- }
286
- else if (metrics.recentFailureRate < 10) {
287
- leniencyFactor = Math.max(0, leniencyFactor - 0.1);
288
- reasoning.push(`Low failure rate (${metrics.recentFailureRate.toFixed(0)}%): stricter enforcement`);
289
- }
290
- }
291
- return {
292
- tier,
293
- trend,
294
- coverageThreshold: Math.round(coverageThreshold),
295
- qualityThreshold: Math.round(qualityThreshold),
296
- securityBlockLevel,
297
- leniencyFactor: Math.round(leniencyFactor * 100) / 100,
298
- reasoning,
299
- provenanceTrends,
300
- };
301
- }
302
174
  /**
303
175
  * Clear adaptive history (for testing)
304
176
  */
@@ -309,20 +181,3 @@ export function clearAdaptiveHistory(cwd) {
309
181
  fs.unlinkSync(historyPath);
310
182
  }
311
183
  }
312
- /**
313
- * Get summary of adaptive thresholds for logging
314
- */
315
- export function getAdaptiveSummary(adjustments) {
316
- let summary = `[${adjustments.tier.toUpperCase()}] ` +
317
- `Coverage: ${adjustments.coverageThreshold}%, ` +
318
- `Quality: ${adjustments.qualityThreshold}%, ` +
319
- `Security: ${adjustments.securityBlockLevel}+, ` +
320
- `Trend: ${adjustments.trend}`;
321
- if (adjustments.provenanceTrends) {
322
- const pt = adjustments.provenanceTrends;
323
- if (pt.aiDrift !== 'stable' || pt.structural !== 'stable' || pt.security !== 'stable') {
324
- summary += ` | AI:${pt.aiDrift}(Z=${pt.aiDriftZScore}) Struct:${pt.structural}(Z=${pt.structuralZScore}) Sec:${pt.security}(Z=${pt.securityZScore})`;
325
- }
326
- }
327
- return summary;
328
- }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { detectComplexityTier, calculateAdaptiveThresholds, recordGateRun, getQualityTrend, clearAdaptiveHistory, getAdaptiveSummary, } from './adaptive-thresholds.js';
2
+ import { recordGateRun, getQualityTrend, clearAdaptiveHistory, } from './adaptive-thresholds.js';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
5
5
  import * as os from 'os';
@@ -12,49 +12,6 @@ describe('AdaptiveThresholds', () => {
12
12
  clearAdaptiveHistory(testDir);
13
13
  fs.rmSync(testDir, { recursive: true, force: true });
14
14
  });
15
- describe('detectComplexityTier', () => {
16
- it('should detect hobby tier for small projects', () => {
17
- const tier = detectComplexityTier({ fileCount: 20 });
18
- expect(tier).toBe('hobby');
19
- });
20
- it('should detect startup tier for medium projects', () => {
21
- const tier = detectComplexityTier({ fileCount: 100 });
22
- expect(tier).toBe('startup');
23
- });
24
- it('should detect enterprise tier for large projects', () => {
25
- const tier = detectComplexityTier({ fileCount: 600 });
26
- expect(tier).toBe('enterprise');
27
- });
28
- it('should consider commit count for tier detection', () => {
29
- const tier = detectComplexityTier({ fileCount: 50, commitCount: 1500 });
30
- expect(tier).toBe('enterprise');
31
- });
32
- });
33
- describe('calculateAdaptiveThresholds', () => {
34
- it('should return lenient thresholds for hobby tier', () => {
35
- const adjustments = calculateAdaptiveThresholds(testDir, { fileCount: 20 });
36
- expect(adjustments.tier).toBe('hobby');
37
- expect(adjustments.coverageThreshold).toBeLessThan(80);
38
- expect(adjustments.securityBlockLevel).toBe('critical');
39
- expect(adjustments.leniencyFactor).toBeGreaterThan(0.5);
40
- });
41
- it('should return strict thresholds for enterprise tier', () => {
42
- const adjustments = calculateAdaptiveThresholds(testDir, { fileCount: 600 });
43
- expect(adjustments.tier).toBe('enterprise');
44
- expect(adjustments.coverageThreshold).toBe(80);
45
- expect(adjustments.securityBlockLevel).toBe('medium');
46
- expect(adjustments.leniencyFactor).toBeLessThan(0.5);
47
- });
48
- it('should respect forced_tier config', () => {
49
- const adjustments = calculateAdaptiveThresholds(testDir, { fileCount: 20 }, { forced_tier: 'enterprise' });
50
- expect(adjustments.tier).toBe('enterprise');
51
- });
52
- it('should include reasoning for adjustments', () => {
53
- const adjustments = calculateAdaptiveThresholds(testDir, { fileCount: 100 });
54
- expect(adjustments.reasoning.length).toBeGreaterThan(0);
55
- expect(adjustments.reasoning.some(r => r.includes('tier'))).toBe(true);
56
- });
57
- });
58
15
  describe('historical tracking', () => {
59
16
  it('should record gate runs', () => {
60
17
  recordGateRun(testDir, 5, 2, 10);
@@ -93,41 +50,4 @@ describe('AdaptiveThresholds', () => {
93
50
  expect(trend).toBe('degrading');
94
51
  });
95
52
  });
96
- describe('trend-based adjustments', () => {
97
- it('should relax thresholds for improving trend', () => {
98
- // Baseline: 15 runs with high failures (with variance)
99
- for (let i = 0; i < 15; i++) {
100
- recordGateRun(testDir, 3, 5, 15 + (i % 5) * 3);
101
- }
102
- // Recent: 5 runs with very low failures
103
- for (let i = 0; i < 5; i++) {
104
- recordGateRun(testDir, 7, 0, 1);
105
- }
106
- const adjustments = calculateAdaptiveThresholds(testDir, { fileCount: 100 });
107
- expect(adjustments.trend).toBe('improving');
108
- expect(adjustments.reasoning.some(r => r.includes('bonus'))).toBe(true);
109
- });
110
- it('should tighten thresholds for degrading trend', () => {
111
- // Baseline: 15 runs with low failures (with variance)
112
- for (let i = 0; i < 15; i++) {
113
- recordGateRun(testDir, 7, 1, 1 + (i % 3));
114
- }
115
- // Recent: 5 runs with high failures
116
- for (let i = 0; i < 5; i++) {
117
- recordGateRun(testDir, 3, 5, 25);
118
- }
119
- const adjustments = calculateAdaptiveThresholds(testDir, { fileCount: 100 });
120
- expect(adjustments.trend).toBe('degrading');
121
- expect(adjustments.reasoning.some(r => r.includes('tightened'))).toBe(true);
122
- });
123
- });
124
- describe('getAdaptiveSummary', () => {
125
- it('should return formatted summary string', () => {
126
- const adjustments = calculateAdaptiveThresholds(testDir, { fileCount: 100 });
127
- const summary = getAdaptiveSummary(adjustments);
128
- expect(summary).toContain('STARTUP');
129
- expect(summary).toContain('Coverage:');
130
- expect(summary).toContain('Quality:');
131
- });
132
- });
133
53
  });
@@ -18,8 +18,6 @@ export interface RigourSettings {
18
18
  defaultProvider?: string;
19
19
  defaultModel?: string;
20
20
  apiBaseUrl?: string;
21
- maxTokens?: number;
22
- temperature?: number;
23
21
  };
24
22
  agents?: {
25
23
  [agentName: string]: {
@@ -33,10 +31,24 @@ export interface RigourSettings {
33
31
  colorOutput?: boolean;
34
32
  verboseOutput?: boolean;
35
33
  };
34
+ reviewer?: UserReviewerSettings;
36
35
  cursor?: {
37
36
  apiKey?: string;
38
37
  };
39
38
  }
39
+ /** What a person may set for their own reviewer runs. They cannot set a team floor (`required`). */
40
+ export interface UserReviewerSettings {
41
+ enabled?: boolean;
42
+ mode?: 'single' | 'cross' | 'full';
43
+ panel?: boolean;
44
+ judges?: 2 | 3;
45
+ escalate?: 'always' | 'risk';
46
+ /** A lower cap for your own runs; never above the team's. */
47
+ max_runs_per_day?: number;
48
+ max_usd_per_day?: number;
49
+ reviewers?: string[];
50
+ models?: Record<string, string>;
51
+ }
40
52
  /**
41
53
  * Resolved deep options from CLI flags merged with settings.json
42
54
  */
@@ -45,8 +57,6 @@ export interface ResolvedDeepOptions {
45
57
  provider?: string;
46
58
  apiBaseUrl?: string;
47
59
  modelName?: string;
48
- maxTokens?: number;
49
- temperature?: number;
50
60
  }
51
61
  /**
52
62
  * CLI options that may override settings
@@ -56,8 +66,6 @@ export interface CLIDeepOptions {
56
66
  provider?: string;
57
67
  apiBaseUrl?: string;
58
68
  modelName?: string;
59
- maxTokens?: number;
60
- temperature?: number;
61
69
  }
62
70
  /**
63
71
  * Get the settings file path: ~/.rigour/settings.json
package/dist/settings.js CHANGED
@@ -102,12 +102,6 @@ export function resolveDeepOptions(cliOptions) {
102
102
  if (settings.deep?.apiBaseUrl) {
103
103
  result.apiBaseUrl = settings.deep.apiBaseUrl;
104
104
  }
105
- if (settings.deep?.maxTokens) {
106
- result.maxTokens = settings.deep.maxTokens;
107
- }
108
- if (settings.deep?.temperature) {
109
- result.temperature = settings.deep.temperature;
110
- }
111
105
  // 2. Apply provider selection (settings or default)
112
106
  let selectedProvider = settings.deep?.defaultProvider || 'anthropic';
113
107
  if (cliOptions.provider) {
@@ -141,12 +135,6 @@ export function resolveDeepOptions(cliOptions) {
141
135
  if (cliOptions.apiBaseUrl) {
142
136
  result.apiBaseUrl = cliOptions.apiBaseUrl;
143
137
  }
144
- if (cliOptions.maxTokens) {
145
- result.maxTokens = cliOptions.maxTokens;
146
- }
147
- if (cliOptions.temperature) {
148
- result.temperature = cliOptions.temperature;
149
- }
150
138
  return result;
151
139
  }
152
140
  /**
@@ -54,5 +54,5 @@ export async function diagnoseMissingMembership(pool) {
54
54
  + `cannot see its membership. Run \`rigour team init-schema\` with the administrator URL to add the read policies.`;
55
55
  }
56
56
  return 'This database role has no membership for the configured organization, team and actor. '
57
- + 'Ask the administrator to add it (see docs/ENTERPRISE.md).';
57
+ + 'Ask the administrator to add it (see docs/TEAM_DATABASE.md).';
58
58
  }
@@ -3,4 +3,4 @@
3
3
  * (`rigour team init-schema`) adds anything missing, including the RLS
4
4
  * policies, without touching data.
5
5
  */
6
- export declare const TEAM_SCHEMA = "\nCREATE SCHEMA IF NOT EXISTS rigour;\nCREATE TABLE IF NOT EXISTS rigour.meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS rigour.memberships (\n db_role NAME NOT NULL,\n organization_id TEXT NOT NULL,\n team_id TEXT NOT NULL,\n actor_id TEXT NOT NULL,\n role TEXT NOT NULL CHECK (role IN ('member', 'sme', 'owner')),\n PRIMARY KEY (db_role, team_id)\n);\nCREATE TABLE IF NOT EXISTS rigour.lessons (\n id TEXT PRIMARY KEY,\n organization_id TEXT NOT NULL,\n team_id TEXT NOT NULL,\n repository_id TEXT NOT NULL,\n actor_id TEXT NOT NULL,\n visibility TEXT NOT NULL CHECK (visibility IN ('personal', 'team')),\n state TEXT NOT NULL CHECK (state IN ('candidate', 'validated', 'promoted', 'rejected', 'superseded')),\n kind TEXT NOT NULL,\n subject TEXT NOT NULL,\n evidence_json JSONB NOT NULL,\n confidence DOUBLE PRECISION NOT NULL,\n source TEXT NOT NULL,\n supersedes_id TEXT,\n created_at BIGINT NOT NULL,\n updated_at BIGINT NOT NULL\n);\nALTER TABLE rigour.lessons ENABLE ROW LEVEL SECURITY;\nDROP POLICY IF EXISTS lessons_read ON rigour.lessons;\nCREATE POLICY lessons_read ON rigour.lessons FOR SELECT USING (\n EXISTS (\n SELECT 1 FROM rigour.memberships membership\n WHERE membership.db_role = current_user\n AND membership.organization_id = lessons.organization_id\n AND membership.team_id = lessons.team_id\n AND ((lessons.visibility = 'team' AND lessons.state = 'promoted')\n OR membership.actor_id = lessons.actor_id)\n )\n);\nDROP POLICY IF EXISTS lessons_write ON rigour.lessons;\nCREATE POLICY lessons_write ON rigour.lessons FOR ALL USING (\n EXISTS (\n SELECT 1 FROM rigour.memberships membership\n WHERE membership.db_role = current_user\n AND membership.organization_id = lessons.organization_id\n AND membership.team_id = lessons.team_id\n AND membership.actor_id = lessons.actor_id\n AND (lessons.visibility = 'personal' OR membership.role IN ('sme', 'owner'))\n )\n) WITH CHECK (\n EXISTS (\n SELECT 1 FROM rigour.memberships membership\n WHERE membership.db_role = current_user\n AND membership.organization_id = lessons.organization_id\n AND membership.team_id = lessons.team_id\n AND membership.actor_id = lessons.actor_id\n AND (lessons.visibility = 'personal' OR membership.role IN ('sme', 'owner'))\n )\n);\n-- Row-level security on every team table. A host (for example Supabase's\n-- security advisor) may enable RLS on its own; without a policy each member\n-- login would then see no rows at all. Members read only their own\n-- membership rows (the lessons policies read memberships the same way), and\n-- meta holds only the schema version.\nALTER TABLE rigour.memberships ENABLE ROW LEVEL SECURITY;\nDROP POLICY IF EXISTS memberships_read_own ON rigour.memberships;\nCREATE POLICY memberships_read_own ON rigour.memberships FOR SELECT USING (db_role = current_user);\nALTER TABLE rigour.meta ENABLE ROW LEVEL SECURITY;\nDROP POLICY IF EXISTS meta_read ON rigour.meta;\nCREATE POLICY meta_read ON rigour.meta FOR SELECT USING (true);\nINSERT INTO rigour.meta (key, value) VALUES ('schema_version', '1')\nON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;\n";
6
+ export declare const TEAM_SCHEMA = "\nCREATE SCHEMA IF NOT EXISTS rigour;\nCREATE TABLE IF NOT EXISTS rigour.meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS rigour.memberships (\n db_role NAME NOT NULL,\n organization_id TEXT NOT NULL,\n team_id TEXT NOT NULL,\n actor_id TEXT NOT NULL,\n role TEXT NOT NULL CHECK (role IN ('member', 'sme', 'owner')),\n PRIMARY KEY (db_role, team_id)\n);\nCREATE TABLE IF NOT EXISTS rigour.lessons (\n id TEXT PRIMARY KEY,\n organization_id TEXT NOT NULL,\n team_id TEXT NOT NULL,\n repository_id TEXT NOT NULL,\n actor_id TEXT NOT NULL,\n visibility TEXT NOT NULL CHECK (visibility IN ('personal', 'team')),\n state TEXT NOT NULL CHECK (state IN ('candidate', 'validated', 'promoted', 'rejected', 'superseded')),\n kind TEXT NOT NULL,\n subject TEXT NOT NULL,\n evidence_json JSONB NOT NULL,\n confidence DOUBLE PRECISION NOT NULL,\n source TEXT NOT NULL,\n supersedes_id TEXT,\n created_at BIGINT NOT NULL,\n updated_at BIGINT NOT NULL\n);\n-- Every sync reads a team's lessons changed since its last read.\nCREATE INDEX IF NOT EXISTS lessons_team_updated ON rigour.lessons (organization_id, team_id, updated_at);\nALTER TABLE rigour.lessons ENABLE ROW LEVEL SECURITY;\nDROP POLICY IF EXISTS lessons_read ON rigour.lessons;\nCREATE POLICY lessons_read ON rigour.lessons FOR SELECT USING (\n EXISTS (\n SELECT 1 FROM rigour.memberships membership\n WHERE membership.db_role = current_user\n AND membership.organization_id = lessons.organization_id\n AND membership.team_id = lessons.team_id\n AND ((lessons.visibility = 'team' AND lessons.state = 'promoted')\n OR membership.actor_id = lessons.actor_id)\n )\n);\nDROP POLICY IF EXISTS lessons_write ON rigour.lessons;\nCREATE POLICY lessons_write ON rigour.lessons FOR ALL USING (\n EXISTS (\n SELECT 1 FROM rigour.memberships membership\n WHERE membership.db_role = current_user\n AND membership.organization_id = lessons.organization_id\n AND membership.team_id = lessons.team_id\n AND membership.actor_id = lessons.actor_id\n AND (lessons.visibility = 'personal' OR membership.role IN ('sme', 'owner'))\n )\n) WITH CHECK (\n EXISTS (\n SELECT 1 FROM rigour.memberships membership\n WHERE membership.db_role = current_user\n AND membership.organization_id = lessons.organization_id\n AND membership.team_id = lessons.team_id\n AND membership.actor_id = lessons.actor_id\n AND (lessons.visibility = 'personal' OR membership.role IN ('sme', 'owner'))\n )\n);\n-- Row-level security on every team table. A host (for example Supabase's\n-- security advisor) may enable RLS on its own; without a policy each member\n-- login would then see no rows at all. Members read only their own\n-- membership rows (the lessons policies read memberships the same way), and\n-- meta holds only the schema version.\nALTER TABLE rigour.memberships ENABLE ROW LEVEL SECURITY;\nDROP POLICY IF EXISTS memberships_read_own ON rigour.memberships;\nCREATE POLICY memberships_read_own ON rigour.memberships FOR SELECT USING (db_role = current_user);\nALTER TABLE rigour.meta ENABLE ROW LEVEL SECURITY;\nDROP POLICY IF EXISTS meta_read ON rigour.meta;\nCREATE POLICY meta_read ON rigour.meta FOR SELECT USING (true);\nINSERT INTO rigour.meta (key, value) VALUES ('schema_version', '1')\nON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;\n";
@@ -34,6 +34,8 @@ CREATE TABLE IF NOT EXISTS rigour.lessons (
34
34
  created_at BIGINT NOT NULL,
35
35
  updated_at BIGINT NOT NULL
36
36
  );
37
+ -- Every sync reads a team's lessons changed since its last read.
38
+ CREATE INDEX IF NOT EXISTS lessons_team_updated ON rigour.lessons (organization_id, team_id, updated_at);
37
39
  ALTER TABLE rigour.lessons ENABLE ROW LEVEL SECURITY;
38
40
  DROP POLICY IF EXISTS lessons_read ON rigour.lessons;
39
41
  CREATE POLICY lessons_read ON rigour.lessons FOR SELECT USING (
@@ -208,6 +208,13 @@ async function splitOutbox(db, queued, config) {
208
208
  }
209
209
  return { sendable, held };
210
210
  }
211
+ /** How far before the last pull a sync reads again, for members whose clocks run behind. */
212
+ const PULL_OVERLAP_MS = 15 * 60_000;
213
+ /** Postgres refused the row itself: insufficient privilege (row-level security) or an integrity constraint (class 23). */
214
+ function refusedRow(error) {
215
+ const code = error?.code;
216
+ return typeof code === 'string' && (code === '42501' || code.startsWith('23'));
217
+ }
211
218
  export async function syncTeamOutbox(options = {}) {
212
219
  const config = await loadTeamConfiguration();
213
220
  if (!config?.databaseUrl)
@@ -263,14 +270,27 @@ export async function syncTeamOutbox(options = {}) {
263
270
  synced++;
264
271
  }
265
272
  catch (error) {
266
- await db.run('UPDATE sync_outbox SET attempts = attempts + 1, last_error = ? WHERE id = ?', String(error instanceof Error ? error.message : error).slice(0, 1_000), item.id);
273
+ const message = String(error instanceof Error ? error.message : error).slice(0, 1_000);
274
+ // The database refused this one row (row-level security, a constraint): it will refuse it every time,
275
+ // so it is set aside with the reason and the rest of the queue goes on. Anything else (the
276
+ // connection, the server) stops the sync and the item is tried again next time.
277
+ if (refusedRow(error)) {
278
+ await db.run('UPDATE sync_outbox SET synced_at = ?, last_error = ? WHERE id = ?', Date.now(), `refused by the team database: ${message}`, item.id);
279
+ continue;
280
+ }
281
+ await db.run('UPDATE sync_outbox SET attempts = attempts + 1, last_error = ? WHERE id = ?', message, item.id);
267
282
  throw error;
268
283
  }
269
284
  }
285
+ // Only what changed since the last pull, less a margin: updated_at comes from each member's clock, and
286
+ // re-reading a few minutes is harmless (the local write is an upsert) where a skewed clock would lose a lesson.
287
+ const mark = `team_pulled:${config.organizationId}/${config.teamId}/${config.actorId}`;
288
+ const last = Number((await db.get('SELECT value FROM meta WHERE key = ?', mark))?.value ?? 0);
270
289
  const remote = await pool.query(`SELECT * FROM rigour.lessons
271
290
  WHERE team_id = $1 AND organization_id = $3
272
291
  AND ((actor_id = $2) OR (visibility = 'team' AND state = 'promoted'))
273
- ORDER BY updated_at ASC`, [config.teamId, config.actorId, config.organizationId]);
292
+ AND updated_at > $4
293
+ ORDER BY updated_at ASC`, [config.teamId, config.actorId, config.organizationId, Math.max(0, last - PULL_OVERLAP_MS)]);
274
294
  for (const row of remote.rows) {
275
295
  const evidence = typeof row.evidence_json === 'string' ? JSON.parse(row.evidence_json) : row.evidence_json;
276
296
  await db.run(`INSERT INTO lessons (
@@ -283,8 +303,11 @@ export async function syncTeamOutbox(options = {}) {
283
303
  updated_at=excluded.updated_at`, row.id, row.repository_id, row.actor_id, row.team_id, row.visibility, row.state, row.kind, row.subject, await encryptLocalPayload(evidence), row.confidence, row.source, row.supersedes_id, Number(row.created_at), Number(row.updated_at));
284
304
  pulled++;
285
305
  }
286
- if (pulled > 0)
306
+ if (pulled > 0) {
307
+ const newest = Math.max(last, ...remote.rows.map((row) => Number(row.updated_at)));
308
+ await db.run('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value', mark, String(newest));
287
309
  await db.run("DELETE FROM context_cache WHERE cache_type = 'semantic'");
310
+ }
288
311
  }
289
312
  finally {
290
313
  await pool.end();
@@ -1,11 +1,15 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  /** The outbox and repositories a machine holds, and what reached the team database. */
3
- const state = vi.hoisted(() => ({ outbox: [], repos: new Map(), sent: [] }));
3
+ const state = vi.hoisted(() => ({ outbox: [], repos: new Map(), sent: [], refuse: new Set(), meta: new Map(), remote: [], since: [] }));
4
4
  vi.mock('./db.js', () => ({
5
5
  openDatabase: async () => ({
6
6
  all: async () => state.outbox.filter(o => o.synced_at == null),
7
- get: async (_sql, id) => (state.repos.has(id) ? { canonical_uri: state.repos.get(id) } : undefined),
7
+ get: async (sql, id) => sql.includes('FROM meta')
8
+ ? (state.meta.has(id) ? { value: state.meta.get(id) } : undefined)
9
+ : (state.repos.has(id) ? { canonical_uri: state.repos.get(id) } : undefined),
8
10
  run: async (sql, ...p) => {
11
+ if (sql.startsWith('INSERT INTO meta'))
12
+ state.meta.set(p[0], p[1]);
9
13
  if (sql.startsWith('UPDATE sync_outbox SET synced_at')) {
10
14
  const id = p.at(-1);
11
15
  Object.assign(state.outbox.find(o => o.id === id) ?? {}, { synced_at: p[0], last_error: p.length === 3 ? p[1] : null });
@@ -20,9 +24,15 @@ vi.mock('pg', () => ({
20
24
  Pool: class {
21
25
  async query(sql, params) {
22
26
  if (sql.includes('INSERT INTO rigour.lessons')) {
27
+ if (state.refuse.has(params[0]))
28
+ throw Object.assign(new Error('new row violates row-level security policy for table "lessons"'), { code: '42501' });
23
29
  state.sent.push(params[0]);
24
30
  return { rowCount: 1, rows: [] };
25
31
  }
32
+ if (sql.includes('FROM rigour.lessons')) {
33
+ state.since.push(params[3]);
34
+ return { rowCount: 0, rows: state.remote.filter(row => row.updated_at > params[3]) };
35
+ }
26
36
  return { rowCount: 1, rows: [] };
27
37
  }
28
38
  async end() { }
@@ -40,6 +50,10 @@ beforeEach(() => {
40
50
  delete process.env.RIGOUR_TEAM_SYNC_PERSONAL;
41
51
  state.outbox = [];
42
52
  state.sent = [];
53
+ state.refuse = new Set();
54
+ state.meta = new Map();
55
+ state.remote = [];
56
+ state.since = [];
43
57
  state.repos = new Map([['r-team', 'https://github.com/acme/web'], ['r-other', 'https://github.com/other-org/app']]);
44
58
  queue('l-team', 'r-team', 'team');
45
59
  queue('l-other', 'r-other', 'team');
@@ -60,6 +74,25 @@ describe('team sync scope', () => {
60
74
  expect(reasons['outbox-l-unknown']).toContain('repository unknown');
61
75
  expect(await syncTeamOutbox({ dryRun: true })).toMatchObject({ pending: 0, withheld: 0 }); // never retried
62
76
  });
77
+ it('sets aside a lesson the database refuses and sends the rest of the queue', async () => {
78
+ state.outbox = [];
79
+ queue('l-refused', 'r-team', 'team'); // queued first: it must not hold back what comes after
80
+ queue('l-team', 'r-team', 'team');
81
+ state.refuse.add('l-refused');
82
+ expect(await syncTeamOutbox()).toMatchObject({ synced: 1 });
83
+ expect(state.sent).toEqual(['l-team']);
84
+ expect(state.outbox.find(o => o.id === 'outbox-l-refused').last_error).toContain('refused by the team database: new row violates row-level security');
85
+ expect(await syncTeamOutbox({ dryRun: true })).toMatchObject({ pending: 0 }); // not tried again
86
+ });
87
+ it('pulls only what changed since the last pull, re-reading a margin for slow clocks', async () => {
88
+ const lesson = (id, updated) => ({ id, repository_id: 'r-team', actor_id: 'dev-2', team_id: 'web', visibility: 'team', state: 'promoted', kind: 'k', subject: 's', evidence_json: {}, confidence: 1, source: 'x', supersedes_id: null, created_at: updated, updated_at: updated });
89
+ const HOUR = 3_600_000;
90
+ state.remote = [lesson('a', 10 * HOUR), lesson('b', 20 * HOUR)];
91
+ expect(await syncTeamOutbox()).toMatchObject({ pulled: 2 });
92
+ expect(state.since).toEqual([0]); // the first pull reads everything
93
+ expect(await syncTeamOutbox()).toMatchObject({ pulled: 1 }); // only b, inside the 15-minute margin
94
+ expect(state.since[1]).toBe(20 * HOUR - 15 * 60_000);
95
+ });
63
96
  it('sends nothing when the team lists no repositories', async () => {
64
97
  delete process.env.RIGOUR_TEAM_REPOSITORIES;
65
98
  expect(await syncTeamOutbox({ dryRun: true })).toMatchObject({ pending: 0, withheld: 4 });
@@ -15,6 +15,9 @@ export interface TelemetryDeps {
15
15
  export declare function telemetryToken(env?: Env): string;
16
16
  export declare function readTelemetryState(home?: string): TelemetryState;
17
17
  export declare function setTelemetryEnabled(enabled: boolean, home?: string): TelemetryState;
18
+ /** Opted out by the environment, whatever the stored answer. */
19
+ /** The cross-tool opt-out (consoledonottrack.com): any value but "0" means no telemetry and no update check. */
20
+ export declare function doNotTrack(env?: Env): boolean;
18
21
  export declare function isTelemetryEnabled(deps?: TelemetryDeps): boolean;
19
22
  /** Ask only once, only a person at a terminal, only when sending would be possible at all. */
20
23
  export declare function shouldAskTelemetry(isTty: boolean, deps?: TelemetryDeps): boolean;
@@ -24,5 +27,7 @@ export declare function countUsage(name: string, by?: number, deps?: TelemetryDe
24
27
  /** Send the day's counters as one `daily_usage` event once they are a day old, then start a new day. */
25
28
  export declare function flushDailyUsage(deps?: TelemetryDeps): Promise<boolean>;
26
29
  /** A duration as a coarse bucket, so timing never identifies a run. */
30
+ /** A model run's cost as a coarse bucket: never the amount. */
31
+ export declare function costBucket(usd: number | undefined): string | undefined;
27
32
  export declare function durationBucket(ms: number): string;
28
33
  export {};
@@ -50,8 +50,12 @@ function isCi(env) {
50
50
  return CI_MARKERS.some(name => !!env[name]);
51
51
  }
52
52
  /** Opted out by the environment, whatever the stored answer. */
53
+ /** The cross-tool opt-out (consoledonottrack.com): any value but "0" means no telemetry and no update check. */
54
+ export function doNotTrack(env = process.env) {
55
+ return !!env.DO_NOT_TRACK && env.DO_NOT_TRACK !== '0';
56
+ }
53
57
  function vetoed(env) {
54
- return (!!env.DO_NOT_TRACK && env.DO_NOT_TRACK !== '0') || env.RIGOUR_TELEMETRY === '0' || !telemetryToken(env);
58
+ return doNotTrack(env) || env.RIGOUR_TELEMETRY === '0' || !telemetryToken(env);
55
59
  }
56
60
  export function isTelemetryEnabled(deps = {}) {
57
61
  const env = deps.env ?? process.env;
@@ -141,6 +145,12 @@ function writeJson(target, value) {
141
145
  fs.writeFileSync(target, JSON.stringify(value, null, 2) + '\n');
142
146
  }
143
147
  /** A duration as a coarse bucket, so timing never identifies a run. */
148
+ /** A model run's cost as a coarse bucket: never the amount. */
149
+ export function costBucket(usd) {
150
+ if (typeof usd !== 'number')
151
+ return undefined;
152
+ return usd < 0.1 ? '<$0.10' : usd < 0.5 ? '$0.10-0.50' : usd < 2 ? '$0.50-2' : '>$2';
153
+ }
144
154
  export function durationBucket(ms) {
145
155
  if (ms < 1000)
146
156
  return '<1s';
@@ -13,10 +13,6 @@ export const PARADIGM_TEMPLATES = [
13
13
  complexity: 10,
14
14
  max_methods: 10,
15
15
  max_params: 5,
16
- max_nesting: 4,
17
- max_inheritance_depth: 3,
18
- max_class_dependencies: 5,
19
- max_function_lines: 60,
20
16
  },
21
17
  },
22
18
  },
@@ -35,10 +31,6 @@ export const PARADIGM_TEMPLATES = [
35
31
  complexity: 8,
36
32
  max_methods: 15, // Functions, not classes
37
33
  max_params: 4,
38
- max_nesting: 3,
39
- max_inheritance_depth: 3,
40
- max_class_dependencies: 5,
41
- max_function_lines: 40,
42
34
  },
43
35
  },
44
36
  },
@@ -7,7 +7,6 @@ export interface Template {
7
7
  paradigm?: string;
8
8
  commands?: Record<string, unknown>;
9
9
  gates?: Partial<Gates>;
10
- planned?: string[];
11
10
  ignore?: string[];
12
11
  };
13
12
  }