@rigour-labs/core 2.18.0 → 2.18.2

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 (40) hide show
  1. package/dist/context.test.js +15 -1
  2. package/dist/discovery.js +13 -1
  3. package/dist/gates/agent-team.d.ts +50 -0
  4. package/dist/gates/agent-team.js +159 -0
  5. package/dist/gates/agent-team.test.d.ts +1 -0
  6. package/dist/gates/agent-team.test.js +113 -0
  7. package/dist/gates/checkpoint.d.ts +72 -0
  8. package/dist/gates/checkpoint.js +231 -0
  9. package/dist/gates/checkpoint.test.d.ts +1 -0
  10. package/dist/gates/checkpoint.test.js +102 -0
  11. package/dist/gates/context.d.ts +35 -0
  12. package/dist/gates/context.js +151 -2
  13. package/dist/gates/runner.js +15 -0
  14. package/dist/gates/security-patterns.d.ts +48 -0
  15. package/dist/gates/security-patterns.js +236 -0
  16. package/dist/gates/security-patterns.test.d.ts +1 -0
  17. package/dist/gates/security-patterns.test.js +133 -0
  18. package/dist/services/adaptive-thresholds.d.ts +63 -0
  19. package/dist/services/adaptive-thresholds.js +204 -0
  20. package/dist/services/adaptive-thresholds.test.d.ts +1 -0
  21. package/dist/services/adaptive-thresholds.test.js +129 -0
  22. package/dist/templates/index.js +34 -0
  23. package/dist/types/fix-packet.d.ts +4 -4
  24. package/dist/types/index.d.ts +404 -0
  25. package/dist/types/index.js +36 -0
  26. package/package.json +1 -1
  27. package/src/context.test.ts +15 -1
  28. package/src/discovery.ts +14 -2
  29. package/src/gates/agent-team.test.ts +134 -0
  30. package/src/gates/agent-team.ts +210 -0
  31. package/src/gates/checkpoint.test.ts +135 -0
  32. package/src/gates/checkpoint.ts +311 -0
  33. package/src/gates/context.ts +200 -2
  34. package/src/gates/runner.ts +18 -0
  35. package/src/gates/security-patterns.test.ts +162 -0
  36. package/src/gates/security-patterns.ts +303 -0
  37. package/src/services/adaptive-thresholds.test.ts +189 -0
  38. package/src/services/adaptive-thresholds.ts +275 -0
  39. package/src/templates/index.ts +34 -0
  40. package/src/types/index.ts +36 -0
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Adaptive Thresholds Service
3
+ *
4
+ * Dynamically adjusts quality gate thresholds based on:
5
+ * - Project maturity (age, commit count, file count)
6
+ * - Historical failure rates
7
+ * - Complexity tier (hobby/startup/enterprise)
8
+ * - Recent trends (improving/degrading)
9
+ *
10
+ * This enables Rigour to be "strict but fair" - new projects get
11
+ * more lenient thresholds while mature codebases are held to higher standards.
12
+ *
13
+ * @since v2.14.0
14
+ */
15
+
16
+ import * as fs from 'fs';
17
+ import * as path from 'path';
18
+ import { Logger } from '../utils/logger.js';
19
+
20
+ export type ComplexityTier = 'hobby' | 'startup' | 'enterprise';
21
+ export type QualityTrend = 'improving' | 'stable' | 'degrading';
22
+
23
+ export interface ProjectMetrics {
24
+ fileCount: number;
25
+ commitCount?: number;
26
+ ageInDays?: number;
27
+ testCoverage?: number;
28
+ recentFailureRate?: number;
29
+ }
30
+
31
+ export interface AdaptiveConfig {
32
+ enabled?: boolean;
33
+ base_coverage_threshold?: number;
34
+ base_quality_threshold?: number;
35
+ auto_detect_tier?: boolean;
36
+ forced_tier?: ComplexityTier;
37
+ }
38
+
39
+ export interface ThresholdAdjustments {
40
+ tier: ComplexityTier;
41
+ trend: QualityTrend;
42
+ coverageThreshold: number;
43
+ qualityThreshold: number;
44
+ securityBlockLevel: 'critical' | 'high' | 'medium' | 'low';
45
+ leniencyFactor: number; // 0.0 = strict, 1.0 = lenient
46
+ reasoning: string[];
47
+ }
48
+
49
+ // Historical failure data (persisted to .rigour/adaptive-history.json)
50
+ interface FailureHistory {
51
+ runs: {
52
+ timestamp: string;
53
+ passedGates: number;
54
+ failedGates: number;
55
+ totalFailures: number;
56
+ }[];
57
+ lastUpdated: string;
58
+ }
59
+
60
+ let cachedHistory: FailureHistory | null = null;
61
+
62
+ /**
63
+ * Load failure history from disk
64
+ */
65
+ function loadHistory(cwd: string): FailureHistory {
66
+ if (cachedHistory) return cachedHistory;
67
+
68
+ const historyPath = path.join(cwd, '.rigour', 'adaptive-history.json');
69
+ try {
70
+ if (fs.existsSync(historyPath)) {
71
+ cachedHistory = JSON.parse(fs.readFileSync(historyPath, 'utf-8'));
72
+ return cachedHistory!;
73
+ }
74
+ } catch (e) {
75
+ Logger.debug('Failed to load adaptive history, starting fresh');
76
+ }
77
+
78
+ cachedHistory = { runs: [], lastUpdated: new Date().toISOString() };
79
+ return cachedHistory;
80
+ }
81
+
82
+ /**
83
+ * Save failure history to disk
84
+ */
85
+ function saveHistory(cwd: string, history: FailureHistory): void {
86
+ const rigourDir = path.join(cwd, '.rigour');
87
+ if (!fs.existsSync(rigourDir)) {
88
+ fs.mkdirSync(rigourDir, { recursive: true });
89
+ }
90
+ const historyPath = path.join(rigourDir, 'adaptive-history.json');
91
+ fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
92
+ cachedHistory = history;
93
+ }
94
+
95
+ /**
96
+ * Record a gate run for historical tracking
97
+ */
98
+ export function recordGateRun(
99
+ cwd: string,
100
+ passedGates: number,
101
+ failedGates: number,
102
+ totalFailures: number
103
+ ): void {
104
+ const history = loadHistory(cwd);
105
+ history.runs.push({
106
+ timestamp: new Date().toISOString(),
107
+ passedGates,
108
+ failedGates,
109
+ totalFailures,
110
+ });
111
+
112
+ // Keep last 100 runs
113
+ if (history.runs.length > 100) {
114
+ history.runs = history.runs.slice(-100);
115
+ }
116
+
117
+ history.lastUpdated = new Date().toISOString();
118
+ saveHistory(cwd, history);
119
+ }
120
+
121
+ /**
122
+ * Get quality trend from historical data
123
+ */
124
+ export function getQualityTrend(cwd: string): QualityTrend {
125
+ const history = loadHistory(cwd);
126
+ if (history.runs.length < 5) return 'stable';
127
+
128
+ const recent = history.runs.slice(-10);
129
+ const older = history.runs.slice(-20, -10);
130
+
131
+ if (older.length === 0) return 'stable';
132
+
133
+ const recentFailRate = recent.reduce((sum, r) => sum + r.totalFailures, 0) / recent.length;
134
+ const olderFailRate = older.reduce((sum, r) => sum + r.totalFailures, 0) / older.length;
135
+
136
+ const delta = recentFailRate - olderFailRate;
137
+
138
+ if (delta < -2) return 'improving';
139
+ if (delta > 2) return 'degrading';
140
+ return 'stable';
141
+ }
142
+
143
+ /**
144
+ * Detect project complexity tier based on metrics
145
+ */
146
+ export function detectComplexityTier(metrics: ProjectMetrics): ComplexityTier {
147
+ // Enterprise: Large teams, many files, mature codebase
148
+ if (metrics.fileCount > 500 || (metrics.commitCount && metrics.commitCount > 1000)) {
149
+ return 'enterprise';
150
+ }
151
+
152
+ // Startup: Growing codebase, active development
153
+ if (metrics.fileCount > 50 || (metrics.commitCount && metrics.commitCount > 100)) {
154
+ return 'startup';
155
+ }
156
+
157
+ // Hobby: Small projects, early stage
158
+ return 'hobby';
159
+ }
160
+
161
+ /**
162
+ * Calculate adaptive thresholds based on project state
163
+ */
164
+ export function calculateAdaptiveThresholds(
165
+ cwd: string,
166
+ metrics: ProjectMetrics,
167
+ config: AdaptiveConfig = {}
168
+ ): ThresholdAdjustments {
169
+ const reasoning: string[] = [];
170
+
171
+ // Determine tier
172
+ const tier = config.forced_tier ??
173
+ (config.auto_detect_tier !== false ? detectComplexityTier(metrics) : 'startup');
174
+ reasoning.push(`Complexity tier: ${tier} (files: ${metrics.fileCount})`);
175
+
176
+ // Get trend
177
+ const trend = getQualityTrend(cwd);
178
+ reasoning.push(`Quality trend: ${trend}`);
179
+
180
+ // Base thresholds
181
+ let coverageThreshold = config.base_coverage_threshold ?? 80;
182
+ let qualityThreshold = config.base_quality_threshold ?? 80;
183
+ let securityBlockLevel: 'critical' | 'high' | 'medium' | 'low' = 'high';
184
+ let leniencyFactor = 0.5;
185
+
186
+ // Adjust by tier
187
+ switch (tier) {
188
+ case 'hobby':
189
+ // Lenient for small/new projects
190
+ coverageThreshold = Math.max(50, coverageThreshold - 30);
191
+ qualityThreshold = Math.max(60, qualityThreshold - 20);
192
+ securityBlockLevel = 'critical'; // Only block on critical
193
+ leniencyFactor = 0.8;
194
+ reasoning.push('Hobby tier: relaxed thresholds, only critical security blocks');
195
+ break;
196
+
197
+ case 'startup':
198
+ // Moderate strictness
199
+ coverageThreshold = Math.max(60, coverageThreshold - 15);
200
+ qualityThreshold = Math.max(70, qualityThreshold - 10);
201
+ securityBlockLevel = 'high';
202
+ leniencyFactor = 0.5;
203
+ reasoning.push('Startup tier: moderate thresholds, high+ security blocks');
204
+ break;
205
+
206
+ case 'enterprise':
207
+ // Strict standards
208
+ coverageThreshold = coverageThreshold;
209
+ qualityThreshold = qualityThreshold;
210
+ securityBlockLevel = 'medium';
211
+ leniencyFactor = 0.2;
212
+ reasoning.push('Enterprise tier: strict thresholds, medium+ security blocks');
213
+ break;
214
+ }
215
+
216
+ // Adjust by trend
217
+ if (trend === 'improving') {
218
+ // Reward improvement with slightly relaxed thresholds
219
+ coverageThreshold = Math.max(50, coverageThreshold - 5);
220
+ qualityThreshold = Math.max(60, qualityThreshold - 5);
221
+ leniencyFactor = Math.min(1, leniencyFactor + 0.1);
222
+ reasoning.push('Improving trend: bonus threshold relaxation (+5%)');
223
+ } else if (trend === 'degrading') {
224
+ // Tighten thresholds to encourage recovery
225
+ coverageThreshold = Math.min(95, coverageThreshold + 5);
226
+ qualityThreshold = Math.min(95, qualityThreshold + 5);
227
+ leniencyFactor = Math.max(0, leniencyFactor - 0.1);
228
+ reasoning.push('Degrading trend: tightened thresholds (-5%)');
229
+ }
230
+
231
+ // Recent failure rate adjustment
232
+ if (metrics.recentFailureRate !== undefined) {
233
+ if (metrics.recentFailureRate > 50) {
234
+ // High failure rate - be more lenient to avoid discouragement
235
+ leniencyFactor = Math.min(1, leniencyFactor + 0.2);
236
+ reasoning.push(`High failure rate (${metrics.recentFailureRate.toFixed(0)}%): increased leniency`);
237
+ } else if (metrics.recentFailureRate < 10) {
238
+ // Low failure rate - team is mature, can handle stricter gates
239
+ leniencyFactor = Math.max(0, leniencyFactor - 0.1);
240
+ reasoning.push(`Low failure rate (${metrics.recentFailureRate.toFixed(0)}%): stricter enforcement`);
241
+ }
242
+ }
243
+
244
+ return {
245
+ tier,
246
+ trend,
247
+ coverageThreshold: Math.round(coverageThreshold),
248
+ qualityThreshold: Math.round(qualityThreshold),
249
+ securityBlockLevel,
250
+ leniencyFactor: Math.round(leniencyFactor * 100) / 100,
251
+ reasoning,
252
+ };
253
+ }
254
+
255
+ /**
256
+ * Clear adaptive history (for testing)
257
+ */
258
+ export function clearAdaptiveHistory(cwd: string): void {
259
+ cachedHistory = null;
260
+ const historyPath = path.join(cwd, '.rigour', 'adaptive-history.json');
261
+ if (fs.existsSync(historyPath)) {
262
+ fs.unlinkSync(historyPath);
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Get summary of adaptive thresholds for logging
268
+ */
269
+ export function getAdaptiveSummary(adjustments: ThresholdAdjustments): string {
270
+ return `[${adjustments.tier.toUpperCase()}] ` +
271
+ `Coverage: ${adjustments.coverageThreshold}%, ` +
272
+ `Quality: ${adjustments.qualityThreshold}%, ` +
273
+ `Security: ${adjustments.securityBlockLevel}+, ` +
274
+ `Trend: ${adjustments.trend}`;
275
+ }
@@ -243,6 +243,10 @@ export const UNIVERSAL_CONFIG: Config = {
243
243
  sensitivity: 0.8,
244
244
  mining_depth: 100,
245
245
  ignored_patterns: [],
246
+ cross_file_patterns: true,
247
+ naming_consistency: true,
248
+ import_relationships: true,
249
+ max_cross_file_depth: 50,
246
250
  },
247
251
  environment: {
248
252
  enabled: true,
@@ -256,6 +260,36 @@ export const UNIVERSAL_CONFIG: Config = {
256
260
  auto_classify: true,
257
261
  doc_sources: {},
258
262
  },
263
+ agent_team: {
264
+ enabled: false,
265
+ max_concurrent_agents: 3,
266
+ cross_agent_pattern_check: true,
267
+ handoff_verification: true,
268
+ task_ownership: 'strict',
269
+ },
270
+ checkpoint: {
271
+ enabled: false,
272
+ interval_minutes: 15,
273
+ quality_threshold: 80,
274
+ drift_detection: true,
275
+ auto_save_on_failure: true,
276
+ },
277
+ security: {
278
+ enabled: false,
279
+ sql_injection: true,
280
+ xss: true,
281
+ path_traversal: true,
282
+ hardcoded_secrets: true,
283
+ insecure_randomness: true,
284
+ command_injection: true,
285
+ block_on_severity: 'high',
286
+ },
287
+ adaptive: {
288
+ enabled: false,
289
+ base_coverage_threshold: 80,
290
+ base_quality_threshold: 80,
291
+ auto_detect_tier: true,
292
+ },
259
293
  staleness: {
260
294
  enabled: false,
261
295
  rules: {
@@ -54,6 +54,11 @@ export const GatesSchema = z.object({
54
54
  sensitivity: z.number().min(0).max(1).optional().default(0.8), // 0.8 correlation threshold
55
55
  mining_depth: z.number().optional().default(100), // Number of files to sample
56
56
  ignored_patterns: z.array(z.string()).optional().default([]),
57
+ // v2.14+ Extended Context for frontier models
58
+ cross_file_patterns: z.boolean().optional().default(true),
59
+ naming_consistency: z.boolean().optional().default(true),
60
+ import_relationships: z.boolean().optional().default(true),
61
+ max_cross_file_depth: z.number().optional().default(50),
57
62
  }).optional().default({}),
58
63
  environment: z.object({
59
64
  enabled: z.boolean().optional().default(true),
@@ -67,6 +72,37 @@ export const GatesSchema = z.object({
67
72
  auto_classify: z.boolean().optional().default(true), // Auto-detect failure category from error message
68
73
  doc_sources: z.record(z.string()).optional().default({}), // Custom doc URLs per category
69
74
  }).optional().default({}),
75
+ agent_team: z.object({
76
+ enabled: z.boolean().optional().default(false),
77
+ max_concurrent_agents: z.number().optional().default(3),
78
+ cross_agent_pattern_check: z.boolean().optional().default(true),
79
+ handoff_verification: z.boolean().optional().default(true),
80
+ task_ownership: z.enum(['strict', 'collaborative']).optional().default('strict'),
81
+ }).optional().default({}),
82
+ checkpoint: z.object({
83
+ enabled: z.boolean().optional().default(false),
84
+ interval_minutes: z.number().optional().default(15),
85
+ quality_threshold: z.number().optional().default(80),
86
+ drift_detection: z.boolean().optional().default(true),
87
+ auto_save_on_failure: z.boolean().optional().default(true),
88
+ }).optional().default({}),
89
+ security: z.object({
90
+ enabled: z.boolean().optional().default(false),
91
+ sql_injection: z.boolean().optional().default(true),
92
+ xss: z.boolean().optional().default(true),
93
+ path_traversal: z.boolean().optional().default(true),
94
+ hardcoded_secrets: z.boolean().optional().default(true),
95
+ insecure_randomness: z.boolean().optional().default(true),
96
+ command_injection: z.boolean().optional().default(true),
97
+ block_on_severity: z.enum(['critical', 'high', 'medium', 'low']).optional().default('high'),
98
+ }).optional().default({}),
99
+ adaptive: z.object({
100
+ enabled: z.boolean().optional().default(false),
101
+ base_coverage_threshold: z.number().optional().default(80),
102
+ base_quality_threshold: z.number().optional().default(80),
103
+ auto_detect_tier: z.boolean().optional().default(true),
104
+ forced_tier: z.enum(['hobby', 'startup', 'enterprise']).optional(),
105
+ }).optional().default({}),
70
106
  });
71
107
 
72
108
  export const CommandsSchema = z.object({