@rigour-labs/cli 4.3.6 → 5.0.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.
@@ -2,7 +2,7 @@ import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import chalk from 'chalk';
4
4
  import yaml from 'yaml';
5
- import { GateRunner, ConfigSchema, recordScore, getScoreTrend, resolveDeepOptions } from '@rigour-labs/core';
5
+ import { GateRunner, ConfigSchema, recordScore, getScoreTrend, resolveDeepOptions, getProvenanceTrends, getQualityTrend } from '@rigour-labs/core';
6
6
  import inquirer from 'inquirer';
7
7
  import { randomUUID } from 'crypto';
8
8
  // Exit codes per spec
@@ -270,6 +270,34 @@ function renderDeepOutput(report, config, options, resolvedDeepMode) {
270
270
  console.log(chalk.dim(` Model: ${model} (${tier}) ${inferenceSec}`));
271
271
  }
272
272
  console.log('');
273
+ // ── Temporal Drift Trends ──
274
+ try {
275
+ const driftCwd = process.cwd();
276
+ const trend = getQualityTrend(driftCwd);
277
+ const provTrends = getProvenanceTrends(driftCwd);
278
+ const hasTrends = trend !== 'stable' || provTrends.aiDrift !== 'stable' || provTrends.structural !== 'stable' || provTrends.security !== 'stable';
279
+ if (hasTrends) {
280
+ const trendIcon = trend === 'improving' ? chalk.green('↑') : trend === 'degrading' ? chalk.red('↓') : chalk.dim('→');
281
+ const trendColor = trend === 'improving' ? chalk.green : trend === 'degrading' ? chalk.red : chalk.dim;
282
+ console.log(` ${chalk.bold('Trend:')} ${trendIcon} ${trendColor(trend.toUpperCase())} (Z-score analysis)`);
283
+ if (provTrends.aiDrift !== 'stable') {
284
+ const c = provTrends.aiDrift === 'degrading' ? chalk.red : chalk.green;
285
+ console.log(` ${chalk.dim(' AI Drift:')} ${c(provTrends.aiDrift)} (Z=${provTrends.aiDriftZScore})`);
286
+ }
287
+ if (provTrends.structural !== 'stable') {
288
+ const c = provTrends.structural === 'degrading' ? chalk.red : chalk.green;
289
+ console.log(` ${chalk.dim(' Structural:')} ${c(provTrends.structural)} (Z=${provTrends.structuralZScore})`);
290
+ }
291
+ if (provTrends.security !== 'stable') {
292
+ const c = provTrends.security === 'degrading' ? chalk.red : chalk.green;
293
+ console.log(` ${chalk.dim(' Security:')} ${c(provTrends.security)} (Z=${provTrends.securityZScore})`);
294
+ }
295
+ console.log('');
296
+ }
297
+ }
298
+ catch {
299
+ // Not enough historical data — skip silently
300
+ }
273
301
  // Categorize findings by provenance
274
302
  const deepFailures = report.failures.filter((f) => f.provenance === 'deep-analysis');
275
303
  const aiDriftFailures = report.failures.filter((f) => f.provenance === 'ai-drift');
@@ -386,6 +414,16 @@ function renderStandardOutput(report, config) {
386
414
  console.log('Severity: ' + parts.join(', ') + '\n');
387
415
  }
388
416
  }
417
+ // ── Temporal Drift Trends (standard mode) ──
418
+ try {
419
+ const trend = getQualityTrend(process.cwd());
420
+ if (trend !== 'stable') {
421
+ const trendIcon = trend === 'improving' ? chalk.green('↑') : chalk.red('↓');
422
+ const trendColor = trend === 'improving' ? chalk.green : chalk.red;
423
+ console.log(`Trend: ${trendIcon} ${trendColor(trend)} (Z-score)\n`);
424
+ }
425
+ }
426
+ catch { /* ignore */ }
389
427
  for (const failure of report.failures) {
390
428
  const sev = severityIcon(failure.severity);
391
429
  const prov = failure.provenance ? chalk.dim(`[${failure.provenance}]`) : '';
@@ -8,4 +8,5 @@ export declare function printPlantedIssues(): void;
8
8
  export declare function displayGateResults(report: any, cinematic: boolean): void;
9
9
  export declare function printSeverityBreakdown(stats: any): void;
10
10
  export declare function printFailure(failure: any): void;
11
+ export declare function displayDriftSummary(): void;
11
12
  export declare function printClosing(cinematic: boolean): void;
@@ -122,6 +122,16 @@ export function displayGateResults(report, cinematic) {
122
122
  else {
123
123
  console.log(chalk.green.bold('✔ PASS — All quality gates satisfied.\n'));
124
124
  }
125
+ // Provenance breakdown
126
+ if (stats.provenance_breakdown) {
127
+ const parts = Object.entries(stats.provenance_breakdown)
128
+ .filter(([, count]) => count > 0)
129
+ .map(([prov, count]) => chalk.dim(`${prov}: ${count}`));
130
+ if (parts.length > 0) {
131
+ console.log(chalk.bold(' Provenance: ') + parts.join(' | '));
132
+ console.log('');
133
+ }
134
+ }
125
135
  console.log(chalk.dim(`Finished in ${stats.duration_ms}ms\n`));
126
136
  }
127
137
  export function printSeverityBreakdown(stats) {
@@ -152,15 +162,58 @@ export function printFailure(failure) {
152
162
  console.log(chalk.cyan(` ${failure.hint}`));
153
163
  }
154
164
  }
165
+ // ── Drift Detection Summary (for demo/Nikhil) ──────────────────────
166
+ export function displayDriftSummary() {
167
+ console.log(chalk.bold.cyan('\n ── v5.1 Drift Detection Engine ──\n'));
168
+ console.log(chalk.white(' Seven production-grade detection systems:\n'));
169
+ console.log(chalk.bold(' 1. EWMA Checkpoint Monitoring'));
170
+ console.log(chalk.dim(' Before: Linear regression on 5 points (one outlier = broken)'));
171
+ console.log(chalk.green(' After: EWMA (α=0.3) — 70% history, 30% new → noise-resistant'));
172
+ console.log(chalk.dim(' Detects: sudden drops AND gradual decline separately\n'));
173
+ console.log(chalk.bold(' 2. Z-Score Adaptive Thresholds'));
174
+ console.log(chalk.dim(' Before: Moving window delta (100→108 = "degrading")'));
175
+ console.log(chalk.green(' After: Z-score normalization — size-independent anomaly detection'));
176
+ console.log(chalk.dim(' Per-provenance: AI drift vs structural vs security tracked independently\n'));
177
+ console.log(chalk.bold(' 3. Three-Pass Duplicate Detection'));
178
+ console.log(chalk.dim(' Pass 1: MD5 hash → exact duplicates (O(n), <10ms)'));
179
+ console.log(chalk.dim(' Pass 2: Jaccard on tree-sitter AST node multisets → structural near-duplicates'));
180
+ console.log(chalk.green(' Pass 3: Semantic embedding (all-MiniLM-L6-v2, 384D) → intent-level duplicates'));
181
+ console.log(chalk.dim(' Catches: .find() vs .filter()[0] — same intent, different AST\n'));
182
+ console.log(chalk.bold(' 4. Temporal Drift Engine'));
183
+ console.log(chalk.dim(' Cross-session trend analysis from SQLite brain'));
184
+ console.log(chalk.green(' Per-provenance EWMA streams + monthly rollups + anomaly detection'));
185
+ console.log(chalk.dim(' Answers: "Is AI getting worse?" independently from "Is code quality dropping?"\n'));
186
+ console.log(chalk.bold(' 5. Dependency Bloat Detection'));
187
+ console.log(chalk.dim(' Before: Only checked forbidden package list'));
188
+ console.log(chalk.green(' After: Unused deps + heavy alternatives + duplicate purpose detection'));
189
+ console.log(chalk.dim(' Catches: axios AND got both installed (AI sessions chose different HTTP clients)\n'));
190
+ console.log(chalk.bold(' 6. Style Drift Detection'));
191
+ console.log(chalk.dim(' Before: No style enforcement beyond linters'));
192
+ console.log(chalk.green(' After: Fingerprint naming, error handling, import style, quotes → compare to baseline'));
193
+ console.log(chalk.dim(' Catches: AI switching from camelCase to snake_case mid-project\n'));
194
+ console.log(chalk.bold(' 7. Logic Drift Foundation'));
195
+ console.log(chalk.dim(' Before: No detection of subtle logic changes'));
196
+ console.log(chalk.green(' After: Tracks comparison operators, branch counts, return counts per function'));
197
+ console.log(chalk.dim(' Catches: >= silently became > (off-by-one), return statements added/removed\n'));
198
+ }
155
199
  export function printClosing(cinematic) {
156
200
  const divider = chalk.bold.cyan('━'.repeat(50));
201
+ // Show drift detection summary in cinematic mode
202
+ if (cinematic) {
203
+ displayDriftSummary();
204
+ }
157
205
  console.log(divider);
158
- console.log(chalk.bold('What Rigour does:'));
159
- console.log(chalk.dim(' Catches AI drift (hallucinated imports, unhandled promises)'));
160
- console.log(chalk.dim(' Blocks security issues (hardcoded keys, injection patterns)'));
206
+ console.log(chalk.bold('What Rigour does (27+ gates):'));
207
+ console.log(chalk.dim(' Catches AI drift (hallucinated imports, duplicates, logic mutations)'));
208
+ console.log(chalk.dim(' Blocks security issues (hardcoded keys, injection patterns, DLP)'));
161
209
  console.log(chalk.dim(' Enforces structure (file size, complexity, documentation)'));
210
+ console.log(chalk.dim(' Detects dependency bloat (unused, heavy, duplicate purpose)'));
211
+ console.log(chalk.dim(' Monitors style drift (naming, error handling, imports)'));
212
+ console.log(chalk.dim(' Tracks logic mutations (operator changes, branch count shifts)'));
162
213
  console.log(chalk.dim(' Generates audit-ready evidence (scores, trends, reports)'));
163
214
  console.log(chalk.dim(' Real-time hooks for Claude, Cursor, Cline, Windsurf'));
215
+ console.log(chalk.dim(' Three-pass duplication: MD5 → AST Jaccard → Semantic Embedding'));
216
+ console.log(chalk.dim(' EWMA + Z-score + temporal drift engine (v5.1)'));
164
217
  console.log(divider);
165
218
  console.log('');
166
219
  if (cinematic) {
@@ -19,6 +19,7 @@ import chalk from 'chalk';
19
19
  import { randomUUID } from 'crypto';
20
20
  import { runHookChecker, scanInputForCredentials, formatDLPAlert, createDLPAuditEntry } from '@rigour-labs/core';
21
21
  // ── Studio event logging ─────────────────────────────────────────────
22
+ const MAX_EVENT_LOG_LINES = 2000;
22
23
  async function logStudioEvent(cwd, event) {
23
24
  try {
24
25
  const rigourDir = path.join(cwd, '.rigour');
@@ -30,11 +31,30 @@ async function logStudioEvent(cwd, event) {
30
31
  ...event,
31
32
  }) + '\n';
32
33
  await fs.appendFile(eventsPath, logEntry);
34
+ // Rotate: keep last MAX_EVENT_LOG_LINES entries to prevent unbounded growth
35
+ await rotateEventLog(eventsPath);
33
36
  }
34
37
  catch {
35
38
  // Silent fail
36
39
  }
37
40
  }
41
+ async function rotateEventLog(eventsPath) {
42
+ try {
43
+ const stat = await fs.stat(eventsPath);
44
+ // Only check rotation when file exceeds ~500KB (avoids reading on every append)
45
+ if (stat.size < 512 * 1024)
46
+ return;
47
+ const content = await fs.readFile(eventsPath, 'utf-8');
48
+ const lines = content.trim().split('\n');
49
+ if (lines.length > MAX_EVENT_LOG_LINES) {
50
+ const trimmed = lines.slice(-MAX_EVENT_LOG_LINES).join('\n') + '\n';
51
+ await fs.writeFile(eventsPath, trimmed);
52
+ }
53
+ }
54
+ catch {
55
+ // Silent fail — rotation is best-effort
56
+ }
57
+ }
38
58
  // ── Tool detection ───────────────────────────────────────────────────
39
59
  const TOOL_MARKERS = {
40
60
  claude: ['CLAUDE.md', '.claude'],
@@ -249,6 +269,8 @@ process.stdin.on('end', async () => {
249
269
  const proc = spawnSync(
250
270
  command,
251
271
  [...baseArgs, '--mode', 'dlp', '--stdin'],
272
+ // Note: joining with \\n is safe — credential patterns match within single values.
273
+ // A credential split across two toolInput fields would be malformed regardless.
252
274
  { input: textsToScan.join('\\n'), encoding: 'utf-8', timeout: 3000 }
253
275
  );
254
276
  if (proc.error) throw proc.error;
@@ -344,6 +344,19 @@ async function setupApiAndLaunch(apiPort, studioPort, eventsPath, cwd, studioPro
344
344
  res.end(e.message);
345
345
  }
346
346
  }
347
+ else if (url.pathname === '/api/drift') {
348
+ try {
349
+ const { generateTemporalDriftReport } = await import('@rigour-labs/core');
350
+ const report = generateTemporalDriftReport(cwd);
351
+ res.writeHead(200, { 'Content-Type': 'application/json' });
352
+ res.end(JSON.stringify(report || { totalScans: 0 }));
353
+ }
354
+ catch (e) {
355
+ // SQLite not available or no data
356
+ res.writeHead(200, { 'Content-Type': 'application/json' });
357
+ res.end(JSON.stringify({ totalScans: 0 }));
358
+ }
359
+ }
347
360
  else if (url.pathname === '/api/arbitrate' && req.method === 'POST') {
348
361
  let body = '';
349
362
  req.on('data', chunk => body += chunk);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/cli",
3
- "version": "4.3.6",
3
+ "version": "5.0.1",
4
4
  "description": "CLI quality gates for AI-generated code. Forces AI agents (Claude, Cursor, Copilot) to meet strict engineering standards with PASS/FAIL enforcement.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://rigour.run",
@@ -44,7 +44,7 @@
44
44
  "inquirer": "9.2.16",
45
45
  "ora": "^8.0.1",
46
46
  "yaml": "^2.8.2",
47
- "@rigour-labs/core": "4.3.6"
47
+ "@rigour-labs/core": "5.0.1"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/fs-extra": "^11.0.4",