@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.
- package/dist/context.test.js +15 -1
- package/dist/discovery.js +13 -1
- package/dist/gates/agent-team.d.ts +50 -0
- package/dist/gates/agent-team.js +159 -0
- package/dist/gates/agent-team.test.d.ts +1 -0
- package/dist/gates/agent-team.test.js +113 -0
- package/dist/gates/checkpoint.d.ts +72 -0
- package/dist/gates/checkpoint.js +231 -0
- package/dist/gates/checkpoint.test.d.ts +1 -0
- package/dist/gates/checkpoint.test.js +102 -0
- package/dist/gates/context.d.ts +35 -0
- package/dist/gates/context.js +151 -2
- package/dist/gates/runner.js +15 -0
- package/dist/gates/security-patterns.d.ts +48 -0
- package/dist/gates/security-patterns.js +236 -0
- package/dist/gates/security-patterns.test.d.ts +1 -0
- package/dist/gates/security-patterns.test.js +133 -0
- package/dist/services/adaptive-thresholds.d.ts +63 -0
- package/dist/services/adaptive-thresholds.js +204 -0
- package/dist/services/adaptive-thresholds.test.d.ts +1 -0
- package/dist/services/adaptive-thresholds.test.js +129 -0
- package/dist/templates/index.js +34 -0
- package/dist/types/fix-packet.d.ts +4 -4
- package/dist/types/index.d.ts +404 -0
- package/dist/types/index.js +36 -0
- package/package.json +1 -1
- package/src/context.test.ts +15 -1
- package/src/discovery.ts +14 -2
- package/src/gates/agent-team.test.ts +134 -0
- package/src/gates/agent-team.ts +210 -0
- package/src/gates/checkpoint.test.ts +135 -0
- package/src/gates/checkpoint.ts +311 -0
- package/src/gates/context.ts +200 -2
- package/src/gates/runner.ts +18 -0
- package/src/gates/security-patterns.test.ts +162 -0
- package/src/gates/security-patterns.ts +303 -0
- package/src/services/adaptive-thresholds.test.ts +189 -0
- package/src/services/adaptive-thresholds.ts +275 -0
- package/src/templates/index.ts +34 -0
- 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
|
+
}
|
package/src/templates/index.ts
CHANGED
|
@@ -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: {
|
package/src/types/index.ts
CHANGED
|
@@ -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({
|