@rigour-labs/cli 4.2.2 → 4.3.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.
package/dist/cli.js CHANGED
@@ -14,6 +14,7 @@ import { demoCommand } from './commands/demo.js';
14
14
  import { hooksInitCommand, hooksCheckCommand } from './commands/hooks.js';
15
15
  import { settingsShowCommand, settingsSetKeyCommand, settingsRemoveKeyCommand, settingsSetCommand, settingsGetCommand, settingsResetCommand, settingsPathCommand } from './commands/settings.js';
16
16
  import { doctorCommand } from './commands/doctor.js';
17
+ import { brainCommand } from './commands/brain.js';
17
18
  import { checkForUpdates } from './utils/version.js';
18
19
  import { getCliVersion } from './utils/cli-version.js';
19
20
  import chalk from 'chalk';
@@ -21,6 +22,7 @@ const CLI_VERSION = getCliVersion();
21
22
  const program = new Command();
22
23
  program.addCommand(indexCommand);
23
24
  program.addCommand(studioCommand);
25
+ program.addCommand(brainCommand);
24
26
  program
25
27
  .name('rigour')
26
28
  .description('🛡️ Rigour: The Quality Gate Loop for AI-Assisted Engineering')
@@ -91,12 +93,23 @@ program
91
93
  .option('--ci', 'CI mode (minimal output, non-zero exit on fail)')
92
94
  .option('--json', 'Output report in JSON format')
93
95
  .option('-c, --config <path>', 'Path to custom rigour.yml configuration (optional)')
96
+ .option('--deep', 'Enable deep LLM-powered analysis (local, 350MB one-time download)')
97
+ .option('--pro', 'Use larger model for deep analysis (900MB, higher quality)')
98
+ .option('-k, --api-key <key>', 'Use cloud API key instead of local model (BYOK)')
99
+ .option('--provider <name>', 'Cloud provider: claude, openai, gemini, groq, mistral, together, deepseek, ollama')
100
+ .option('--api-base-url <url>', 'Custom API base URL')
101
+ .option('--model-name <name>', 'Override cloud model name')
102
+ .option('--agents <count>', 'Number of parallel agents for deep scan (cloud-only)', '1')
94
103
  .addHelpText('after', `
95
104
  Examples:
96
- $ rigour scan # Zero-config scan in current repo
97
- $ rigour scan ./src # Scan only src
98
- $ rigour scan --json # Machine-readable output
99
- $ rigour scan --ci # CI-friendly output
105
+ $ rigour scan # Zero-config AST scan
106
+ $ rigour scan --deep # Zero-config + local LLM deep analysis
107
+ $ rigour scan --deep --pro # Zero-config + larger local model
108
+ $ rigour scan --deep -k sk-ant-xxx # Zero-config + Claude API
109
+ $ rigour scan --deep --provider groq -k gsk_xxx # Zero-config + Groq
110
+ $ rigour scan ./src --deep # Deep scan specific directory
111
+ $ rigour scan --json # Machine-readable output
112
+ $ rigour scan --ci # CI-friendly output
100
113
  `)
101
114
  .action(async (files, options) => {
102
115
  await scanCommand(process.cwd(), files, options);
@@ -150,19 +163,22 @@ program
150
163
  .option('--cinematic', 'Screen-recording mode: typewriter effects, simulated AI agent, before/after scores')
151
164
  .option('--hooks', 'Focus on real-time hooks catching issues as AI writes code')
152
165
  .option('--speed <speed>', 'Pacing: fast, normal, slow (default: normal)', 'normal')
166
+ .option('--repo <url>', 'Clone a real GitHub repo and inject drift into it (festival mode)')
153
167
  .addHelpText('after', `
154
168
  Examples:
155
- $ rigour demo # Run the flagship demo
156
- $ rigour demo --cinematic # Screen-recording optimized (great for GIFs)
157
- $ rigour demo --cinematic --speed slow # Slower pacing for presentations
158
- $ rigour demo --hooks # Focus on hooks catching issues
159
- $ npx @rigour-labs/cli demo # Try without installing
169
+ $ rigour demo # Flagship demo (synthetic project)
170
+ $ rigour demo --cinematic # Screen-recording optimized
171
+ $ rigour demo --cinematic --speed slow # Slower pacing for presentations
172
+ $ rigour demo --cinematic --repo https://github.com/fastapi/fastapi # Live demo on real repo
173
+ $ rigour demo --hooks # Focus on real-time hooks
174
+ $ npx @rigour-labs/cli demo # Try without installing
160
175
  `)
161
176
  .action(async (options) => {
162
177
  await demoCommand({
163
178
  cinematic: !!options.cinematic,
164
179
  hooks: !!options.hooks,
165
180
  speed: options.speed || 'normal',
181
+ repo: options.repo,
166
182
  });
167
183
  });
168
184
  program
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const brainCommand: Command;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Brain Command — inspect, compact, and reset the local project memory.
3
+ * Storage lives at ~/.rigour/rigour.db (SQLite, WAL mode).
4
+ */
5
+ import chalk from 'chalk';
6
+ import { Command } from 'commander';
7
+ export const brainCommand = new Command('brain')
8
+ .description('Manage Rigour Brain — local project memory (SQLite)')
9
+ .addHelpText('after', `
10
+ Examples:
11
+ $ rigour brain # Show memory status
12
+ $ rigour brain --compact # Prune old data, reclaim disk
13
+ $ rigour brain --compact --retain 30 # Keep only last 30 days
14
+ $ rigour brain --reset # Wipe all memory and start fresh
15
+ `)
16
+ .option('--compact', 'Prune old findings, weak patterns, and reclaim disk space')
17
+ .option('--retain <days>', 'Days of findings to keep during compact (default: 90)', '90')
18
+ .option('--reset', 'Delete the entire database and start fresh')
19
+ .action(async (options) => {
20
+ const core = await import('@rigour-labs/core');
21
+ if (options.reset) {
22
+ await handleReset(core);
23
+ return;
24
+ }
25
+ if (options.compact) {
26
+ await handleCompact(core, parseInt(options.retain, 10));
27
+ return;
28
+ }
29
+ await handleStatus(core);
30
+ });
31
+ /** Show brain status: DB size, scan count, patterns, hard rules. */
32
+ async function handleStatus(core) {
33
+ const sizeBytes = core.getDatabaseSize();
34
+ if (sizeBytes === 0) {
35
+ console.log(chalk.yellow('No Rigour Brain database found.'));
36
+ console.log(chalk.dim('Run `rigour scan` or `rigour check` to start building local memory.'));
37
+ return;
38
+ }
39
+ console.log(chalk.bold.cyan('\n🧠 Rigour Brain — Local Memory Status\n'));
40
+ console.log(chalk.dim(` Database: ~/.rigour/rigour.db`));
41
+ console.log(chalk.dim(` Size: ${formatBytes(sizeBytes)}\n`));
42
+ const cwd = process.cwd();
43
+ const stats = core.getProjectStats(cwd);
44
+ if (!stats) {
45
+ console.log(chalk.yellow(' SQLite not available (better-sqlite3 not installed).'));
46
+ console.log(chalk.dim(' Run: npm install better-sqlite3'));
47
+ return;
48
+ }
49
+ if (stats.totalScans === 0) {
50
+ console.log(chalk.dim(' No scans recorded for this project yet.'));
51
+ console.log(chalk.dim(' Run `rigour scan` to start learning.\n'));
52
+ return;
53
+ }
54
+ console.log(` Scans recorded: ${chalk.green(stats.totalScans)}`);
55
+ console.log(` Learned patterns: ${chalk.green(stats.learnedPatterns)}`);
56
+ console.log(` Hard rules (≥0.9): ${chalk.green(stats.hardRules)}`);
57
+ if (stats.topPatterns.length > 0) {
58
+ console.log(chalk.bold('\n Top Patterns:'));
59
+ for (const p of stats.topPatterns) {
60
+ const bar = strengthBar(p.strength);
61
+ console.log(` ${bar} ${p.name} ${chalk.dim(`(seen ${p.timesSeen}x)`)}`);
62
+ }
63
+ }
64
+ console.log('');
65
+ }
66
+ /** Compact: prune old findings, weak patterns, VACUUM. */
67
+ async function handleCompact(core, retainDays) {
68
+ console.log(chalk.bold.cyan(`\n🧠 Compacting Rigour Brain (retain ${retainDays} days)...\n`));
69
+ const result = core.compactDatabase(retainDays);
70
+ console.log(` Findings pruned: ${chalk.yellow(result.pruned)}`);
71
+ console.log(` Patterns removed: ${chalk.yellow(result.patternsDecayed)}`);
72
+ console.log(` Size before: ${formatBytes(result.sizeBefore)}`);
73
+ console.log(` Size after: ${chalk.green(formatBytes(result.sizeAfter))}`);
74
+ const saved = result.sizeBefore - result.sizeAfter;
75
+ if (saved > 0) {
76
+ console.log(` Space saved: ${chalk.green(formatBytes(saved))}`);
77
+ }
78
+ console.log('');
79
+ }
80
+ /** Reset: delete the entire database. */
81
+ async function handleReset(core) {
82
+ const sizeBytes = core.getDatabaseSize();
83
+ if (sizeBytes === 0) {
84
+ console.log(chalk.yellow('No Rigour Brain database found. Nothing to reset.'));
85
+ return;
86
+ }
87
+ console.log(chalk.bold.red(`\n⚠️ Resetting Rigour Brain`));
88
+ console.log(chalk.dim(` This will delete all scan history, patterns, and learned rules.`));
89
+ console.log(chalk.dim(` Database size: ${formatBytes(sizeBytes)}\n`));
90
+ core.resetDatabase();
91
+ console.log(chalk.green(' ✓ Brain reset complete. All memory cleared.\n'));
92
+ }
93
+ /** Format bytes as human-readable string. */
94
+ function formatBytes(bytes) {
95
+ if (bytes === 0)
96
+ return '0 B';
97
+ const units = ['B', 'KB', 'MB', 'GB'];
98
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
99
+ return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
100
+ }
101
+ /** Visual strength bar for patterns. */
102
+ function strengthBar(strength) {
103
+ const filled = Math.round(strength * 10);
104
+ const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
105
+ const color = strength >= 0.9 ? chalk.green : strength >= 0.7 ? chalk.yellow : chalk.dim;
106
+ return color(`[${bar}]`);
107
+ }
@@ -2,6 +2,7 @@ export interface DemoOptions {
2
2
  cinematic?: boolean;
3
3
  hooks?: boolean;
4
4
  speed?: 'fast' | 'normal' | 'slow';
5
+ repo?: string;
5
6
  }
6
7
  export declare function getMultiplier(options: DemoOptions): number;
7
8
  export declare function sleep(ms: number): Promise<void>;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Drift injection patterns for live demo on real repos.
3
+ * Each injection is a file to write with intentional code issues.
4
+ */
5
+ export interface DriftInjection {
6
+ filename: string;
7
+ description: string;
8
+ gate: string;
9
+ severity: 'critical' | 'high' | 'medium';
10
+ hookMessage: string;
11
+ code: string;
12
+ }
13
+ /**
14
+ * Universal injections that work in any TypeScript/JavaScript repo.
15
+ */
16
+ export declare const TS_INJECTIONS: DriftInjection[];
17
+ /**
18
+ * Python/FastAPI injections.
19
+ */
20
+ export declare const PYTHON_INJECTIONS: DriftInjection[];
21
+ /**
22
+ * Fixed versions of TS injections (for before/after demo).
23
+ */
24
+ export declare const TS_FIXES: Record<string, string>;
25
+ /**
26
+ * Detect which injection set to use based on repo contents.
27
+ */
28
+ export declare function detectInjectionSet(languages: string[]): DriftInjection[];
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Drift injection patterns for live demo on real repos.
3
+ * Each injection is a file to write with intentional code issues.
4
+ */
5
+ /**
6
+ * Universal injections that work in any TypeScript/JavaScript repo.
7
+ */
8
+ export const TS_INJECTIONS = [
9
+ {
10
+ filename: 'src/auth-handler.ts',
11
+ description: 'AI writes authentication with hardcoded secret',
12
+ gate: 'security-patterns',
13
+ severity: 'critical',
14
+ hookMessage: 'Possible hardcoded secret or API key detected',
15
+ code: [
16
+ 'import express from \'express\';',
17
+ '',
18
+ 'const API_SECRET = "sk-live-4f3c2b1a0987654321abcdef";',
19
+ 'const DB_PASSWORD = "super_secret_p@ssw0rd!";',
20
+ '',
21
+ 'export function authenticate(req: express.Request) {',
22
+ ' const token = req.headers.authorization;',
23
+ ' if (token === API_SECRET) {',
24
+ ' return { authenticated: true, role: \'admin\' };',
25
+ ' }',
26
+ ' return { authenticated: false };',
27
+ '}',
28
+ ].join('\n'),
29
+ },
30
+ {
31
+ filename: 'src/ai-data-loader.ts',
32
+ description: 'AI hallucinates a non-existent package',
33
+ gate: 'hallucinated-imports',
34
+ severity: 'critical',
35
+ hookMessage: "Import 'ai-data-magic' does not resolve to any installed or known package",
36
+ code: [
37
+ 'import { z } from \'zod\';',
38
+ 'import { magicParser } from \'ai-data-magic\';',
39
+ 'import { ultraCache } from \'quantum-cache-pro\';',
40
+ '',
41
+ 'const schema = z.object({',
42
+ ' name: z.string(),',
43
+ ' email: z.string().email(),',
44
+ '});',
45
+ '',
46
+ 'export function loadData(raw: unknown) {',
47
+ ' return schema.parse(raw);',
48
+ '}',
49
+ ].join('\n'),
50
+ },
51
+ {
52
+ filename: 'src/api-handler.ts',
53
+ description: 'AI forgets to await an async function',
54
+ gate: 'promise-safety',
55
+ severity: 'high',
56
+ hookMessage: 'Unhandled promise — fetchUser() called without await or .catch()',
57
+ code: [
58
+ 'export async function fetchUser(id: string) {',
59
+ ' const res = await fetch(`/api/users/${id}`);',
60
+ ' return res.json();',
61
+ '}',
62
+ '',
63
+ 'export function handleRequest(req: any, res: any) {',
64
+ ' fetchUser(req.params.id); // floating promise!',
65
+ ' res.send(\'Processing...\');',
66
+ '}',
67
+ ].join('\n'),
68
+ },
69
+ ];
70
+ /**
71
+ * Python/FastAPI injections.
72
+ */
73
+ export const PYTHON_INJECTIONS = [
74
+ {
75
+ filename: 'src/middleware/cors_config.py',
76
+ description: 'AI sets wildcard CORS with credentials',
77
+ gate: 'security-patterns',
78
+ severity: 'critical',
79
+ hookMessage: 'Wildcard CORS with allow_credentials=True — any origin can steal session tokens',
80
+ code: [
81
+ 'from fastapi.middleware.cors import CORSMiddleware',
82
+ '',
83
+ 'def setup_cors(app):',
84
+ ' app.add_middleware(',
85
+ ' CORSMiddleware,',
86
+ ' allow_origins=["*"],',
87
+ ' allow_credentials=True,',
88
+ ' allow_methods=["*"],',
89
+ ' allow_headers=["*"],',
90
+ ' )',
91
+ ].join('\n'),
92
+ },
93
+ {
94
+ filename: 'src/middleware/logging_middleware.py',
95
+ description: 'AI logs full request bodies including passwords',
96
+ gate: 'security-patterns',
97
+ severity: 'high',
98
+ hookMessage: 'Request body logged — passwords, tokens, PII exposed in log output',
99
+ code: [
100
+ 'import logging',
101
+ 'from starlette.middleware.base import BaseHTTPMiddleware',
102
+ 'from starlette.requests import Request',
103
+ '',
104
+ 'logger = logging.getLogger(__name__)',
105
+ '',
106
+ 'class LoggingMiddleware(BaseHTTPMiddleware):',
107
+ ' async def dispatch(self, request: Request, call_next):',
108
+ ' body = await request.body()',
109
+ ' logger.info("Body: %s Headers: %s", body, dict(request.headers))',
110
+ ' response = await call_next(request)',
111
+ ' return response',
112
+ ].join('\n'),
113
+ },
114
+ {
115
+ filename: 'src/config.py',
116
+ description: 'AI hardcodes secrets in config class',
117
+ gate: 'security-patterns',
118
+ severity: 'critical',
119
+ hookMessage: 'Hardcoded SECRET_KEY and database credentials in source code',
120
+ code: [
121
+ 'class Config:',
122
+ ' SECRET_KEY = "super-secret-key-12345"',
123
+ ' DATABASE_URL = "postgresql://admin:p@ssw0rd@prod-db:5432/app"',
124
+ ' DEBUG = True',
125
+ ' SESSION_COOKIE_SECURE = False',
126
+ ' SESSION_COOKIE_HTTPONLY = False',
127
+ ].join('\n'),
128
+ },
129
+ ];
130
+ /**
131
+ * Fixed versions of TS injections (for before/after demo).
132
+ */
133
+ export const TS_FIXES = {
134
+ 'src/auth-handler.ts': [
135
+ 'import express from \'express\';',
136
+ '',
137
+ 'export function authenticate(req: express.Request) {',
138
+ ' const token = req.headers.authorization;',
139
+ ' if (!token) return { authenticated: false };',
140
+ ' return { authenticated: validateToken(token) };',
141
+ '}',
142
+ '',
143
+ 'function validateToken(token: string): boolean {',
144
+ ' return token.startsWith(\'Bearer \') && token.length > 20;',
145
+ '}',
146
+ ].join('\n'),
147
+ 'src/ai-data-loader.ts': [
148
+ 'import { z } from \'zod\';',
149
+ '',
150
+ 'const schema = z.object({',
151
+ ' name: z.string(),',
152
+ ' email: z.string().email(),',
153
+ '});',
154
+ '',
155
+ 'export function loadData(raw: unknown) {',
156
+ ' return schema.parse(raw);',
157
+ '}',
158
+ ].join('\n'),
159
+ 'src/api-handler.ts': [
160
+ 'import express from \'express\';',
161
+ '',
162
+ 'export async function fetchUser(id: string) {',
163
+ ' const res = await fetch(`/api/users/${id}`);',
164
+ ' if (!res.ok) throw new Error(`HTTP ${res.status}`);',
165
+ ' return res.json();',
166
+ '}',
167
+ '',
168
+ 'export async function handleRequest(req: express.Request, res: express.Response) {',
169
+ ' try {',
170
+ ' const data = await fetchUser(req.params.id);',
171
+ ' res.json(data);',
172
+ ' } catch (error) {',
173
+ ' res.status(500).json({ error: \'Failed to fetch user\' });',
174
+ ' }',
175
+ '}',
176
+ ].join('\n'),
177
+ };
178
+ /**
179
+ * Detect which injection set to use based on repo contents.
180
+ */
181
+ export function detectInjectionSet(languages) {
182
+ if (languages.includes('Python')) {
183
+ return PYTHON_INJECTIONS;
184
+ }
185
+ return TS_INJECTIONS;
186
+ }
@@ -0,0 +1,6 @@
1
+ export declare function scaffoldDemoProject(dir: string): Promise<void>;
2
+ export declare function buildDemoConfig(): Record<string, unknown>;
3
+ export declare function buildDemoPackageJson(): Record<string, unknown>;
4
+ export declare function writeIssueFiles(dir: string): Promise<void>;
5
+ export declare function writeGodFile(dir: string): Promise<void>;
6
+ export declare function generateDemoAudit(dir: string, report: any, outputPath: string): Promise<void>;
@@ -0,0 +1,170 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import yaml from 'yaml';
4
+ // ── Scaffold demo project ───────────────────────────────────────────
5
+ export async function scaffoldDemoProject(dir) {
6
+ const config = buildDemoConfig();
7
+ await fs.writeFile(path.join(dir, 'rigour.yml'), yaml.stringify(config));
8
+ await fs.writeJson(path.join(dir, 'package.json'), buildDemoPackageJson(), { spaces: 2 });
9
+ await fs.ensureDir(path.join(dir, 'src'));
10
+ await fs.ensureDir(path.join(dir, 'docs'));
11
+ await writeIssueFiles(dir);
12
+ await writeGodFile(dir);
13
+ await fs.writeFile(path.join(dir, 'README.md'), '# Demo Project\n\nThis is a demo project for Rigour.\n');
14
+ }
15
+ export function buildDemoConfig() {
16
+ return {
17
+ version: 1,
18
+ preset: 'api',
19
+ gates: {
20
+ max_file_lines: 300,
21
+ forbid_todos: true,
22
+ forbid_fixme: true,
23
+ ast: { complexity: 10, max_params: 5 },
24
+ security: { enabled: true, block_on_severity: 'high' },
25
+ hallucinated_imports: { enabled: true, severity: 'critical' },
26
+ promise_safety: { enabled: true, severity: 'high' },
27
+ },
28
+ hooks: { enabled: true, tools: ['claude'] },
29
+ ignore: ['.git/**', 'node_modules/**'],
30
+ output: { report_path: 'rigour-report.json' },
31
+ };
32
+ }
33
+ export function buildDemoPackageJson() {
34
+ return {
35
+ name: 'rigour-demo',
36
+ version: '1.0.0',
37
+ dependencies: { express: '^4.18.0', zod: '^3.22.0' },
38
+ };
39
+ }
40
+ export async function writeIssueFiles(dir) {
41
+ await writeAuthFile(dir);
42
+ await writeApiHandlerFile(dir);
43
+ await writeDataLoaderFile(dir);
44
+ await writeUtilsFile(dir);
45
+ }
46
+ async function writeAuthFile(dir) {
47
+ await fs.writeFile(path.join(dir, 'src', 'auth.ts'), `
48
+ import express from 'express';
49
+
50
+ const API_KEY = "sk-live-4f3c2b1a0987654321abcdef";
51
+ const DB_PASSWORD = "super_secret_p@ssw0rd!";
52
+
53
+ export function authenticate(req: express.Request) {
54
+ const token = req.headers.authorization;
55
+ if (token === API_KEY) {
56
+ return { authenticated: true };
57
+ }
58
+ return { authenticated: false };
59
+ }
60
+
61
+ export function connectDatabase() {
62
+ return { host: 'prod-db.internal', password: DB_PASSWORD };
63
+ }
64
+ `.trim());
65
+ }
66
+ async function writeApiHandlerFile(dir) {
67
+ await fs.writeFile(path.join(dir, 'src', 'api-handler.ts'), `
68
+ import express from 'express';
69
+
70
+ export async function fetchUserData(userId: string) {
71
+ const response = await fetch(\`https://api.example.com/users/\${userId}\`);
72
+ return response.json();
73
+ }
74
+
75
+ export function handleRequest(req: express.Request, res: express.Response) {
76
+ fetchUserData(req.params.id);
77
+ res.send('Processing...');
78
+ }
79
+
80
+ export function batchProcess(ids: string[]) {
81
+ ids.forEach(id => fetchUserData(id));
82
+ }
83
+ `.trim());
84
+ }
85
+ async function writeDataLoaderFile(dir) {
86
+ await fs.writeFile(path.join(dir, 'src', 'data-loader.ts'), `
87
+ import { z } from 'zod';
88
+ import { magicParser } from 'ai-data-magic';
89
+ import { ultraCache } from 'quantum-cache-pro';
90
+
91
+ const schema = z.object({
92
+ name: z.string(),
93
+ email: z.string().email(),
94
+ });
95
+
96
+ export function loadData(raw: unknown) {
97
+ const parsed = schema.parse(raw);
98
+ return parsed;
99
+ }
100
+ `.trim());
101
+ }
102
+ async function writeUtilsFile(dir) {
103
+ await fs.writeFile(path.join(dir, 'src', 'utils.ts'), `
104
+ // NOTE: Claude suggested this but I need to review
105
+ // NOTE: This function has edge cases
106
+ export function formatDate(date: Date): string {
107
+ return date.toISOString().split('T')[0];
108
+ }
109
+
110
+ export function sanitizeInput(input: string): string {
111
+ // NOTE: Add proper sanitization
112
+ return input.trim();
113
+ }
114
+ `.trim());
115
+ }
116
+ export async function writeGodFile(dir) {
117
+ const lines = [
118
+ '// Auto-generated data processing module',
119
+ 'export class DataProcessor {',
120
+ ];
121
+ for (let i = 0; i < 60; i++) {
122
+ lines.push(` process${i}(data: any) {`);
123
+ lines.push(` const result = data.map((x: any) => x * ${i + 1});`);
124
+ lines.push(` if (result.length > ${i * 10}) {`);
125
+ lines.push(` return result.slice(0, ${i * 10});`);
126
+ lines.push(` }`);
127
+ lines.push(` return result;`);
128
+ lines.push(` }`);
129
+ }
130
+ lines.push('}');
131
+ await fs.writeFile(path.join(dir, 'src', 'god-file.ts'), lines.join('\n'));
132
+ }
133
+ // ── Audit report generator ──────────────────────────────────────────
134
+ export async function generateDemoAudit(dir, report, outputPath) {
135
+ const stats = report.stats || {};
136
+ const failures = report.failures || [];
137
+ const lines = [];
138
+ lines.push('# Rigour Audit Report — Demo');
139
+ lines.push('');
140
+ lines.push(`**Generated:** ${new Date().toISOString()}`);
141
+ lines.push(`**Status:** ${report.status}`);
142
+ lines.push(`**Score:** ${stats.score ?? 100}/100`);
143
+ if (stats.ai_health_score !== undefined) {
144
+ lines.push(`**AI Health:** ${stats.ai_health_score}/100`);
145
+ }
146
+ if (stats.structural_score !== undefined) {
147
+ lines.push(`**Structural:** ${stats.structural_score}/100`);
148
+ }
149
+ lines.push('');
150
+ lines.push('## Violations');
151
+ lines.push('');
152
+ for (let i = 0; i < failures.length; i++) {
153
+ const f = failures[i];
154
+ lines.push(`### ${i + 1}. [${(f.severity || 'medium').toUpperCase()}] ${f.title}`);
155
+ lines.push(`- **ID:** \`${f.id}\``);
156
+ lines.push(`- **Provenance:** ${f.provenance || 'traditional'}`);
157
+ lines.push(`- **Details:** ${f.details}`);
158
+ if (f.files?.length) {
159
+ lines.push(`- **Files:** ${f.files.join(', ')}`);
160
+ }
161
+ if (f.hint) {
162
+ lines.push(`- **Hint:** ${f.hint}`);
163
+ }
164
+ lines.push('');
165
+ }
166
+ lines.push('---');
167
+ lines.push('*Generated by Rigour — https://rigour.run*');
168
+ lines.push('*Research: https://zenodo.org/records/18673564*');
169
+ await fs.writeFile(outputPath, lines.join('\n'));
170
+ }
@@ -1,11 +1,7 @@
1
1
  import type { DemoOptions } from './demo-helpers.js';
2
+ import { scaffoldDemoProject, generateDemoAudit } from './demo-scaffold.js';
3
+ export { scaffoldDemoProject, generateDemoAudit };
2
4
  export declare function runHooksDemo(demoDir: string, options: DemoOptions): Promise<void>;
3
5
  export declare function simulateAgentWrite(filename: string, codeLines: string[], gate: string, file: string, message: string, severity: string, options: DemoOptions): Promise<void>;
4
6
  export declare function runFullGates(demoDir: string, options: DemoOptions): Promise<void>;
5
7
  export declare function runBeforeAfterDemo(demoDir: string, options: DemoOptions): Promise<void>;
6
- export declare function scaffoldDemoProject(dir: string): Promise<void>;
7
- export declare function buildDemoConfig(): Record<string, unknown>;
8
- export declare function buildDemoPackageJson(): Record<string, unknown>;
9
- export declare function writeIssueFiles(dir: string): Promise<void>;
10
- export declare function writeGodFile(dir: string): Promise<void>;
11
- export declare function generateDemoAudit(dir: string, report: any, outputPath: string): Promise<void>;
@@ -6,6 +6,9 @@ import { GateRunner, ConfigSchema } from '@rigour-labs/core';
6
6
  import { recordScore } from '@rigour-labs/core';
7
7
  import { pause, typewrite } from './demo-helpers.js';
8
8
  import { simulateCodeWrite, simulateHookCatch, renderScoreBar, renderTrendChart, displayGateResults, } from './demo-display.js';
9
+ import { scaffoldDemoProject, generateDemoAudit } from './demo-scaffold.js';
10
+ // Re-export scaffolding functions for backward compatibility
11
+ export { scaffoldDemoProject, generateDemoAudit };
9
12
  // ── Hooks demo: simulate AI agent → hook catches ────────────────────
10
13
  export async function runHooksDemo(demoDir, options) {
11
14
  const divider = chalk.cyan('━'.repeat(50));
@@ -195,162 +198,3 @@ export async function handleRequest(req: express.Request, res: express.Response)
195
198
  console.error(chalk.red(`Re-check error: ${msg}`));
196
199
  }
197
200
  }
198
- // ── Scaffold demo project ───────────────────────────────────────────
199
- export async function scaffoldDemoProject(dir) {
200
- const config = buildDemoConfig();
201
- await fs.writeFile(path.join(dir, 'rigour.yml'), yaml.stringify(config));
202
- await fs.writeJson(path.join(dir, 'package.json'), buildDemoPackageJson(), { spaces: 2 });
203
- await fs.ensureDir(path.join(dir, 'src'));
204
- await fs.ensureDir(path.join(dir, 'docs'));
205
- await writeIssueFiles(dir);
206
- await writeGodFile(dir);
207
- await fs.writeFile(path.join(dir, 'README.md'), '# Demo Project\n\nThis is a demo project for Rigour.\n');
208
- }
209
- export function buildDemoConfig() {
210
- return {
211
- version: 1,
212
- preset: 'api',
213
- gates: {
214
- max_file_lines: 300,
215
- forbid_todos: true,
216
- forbid_fixme: true,
217
- ast: { complexity: 10, max_params: 5 },
218
- security: { enabled: true, block_on_severity: 'high' },
219
- hallucinated_imports: { enabled: true, severity: 'critical' },
220
- promise_safety: { enabled: true, severity: 'high' },
221
- },
222
- hooks: { enabled: true, tools: ['claude'] },
223
- ignore: ['.git/**', 'node_modules/**'],
224
- output: { report_path: 'rigour-report.json' },
225
- };
226
- }
227
- export function buildDemoPackageJson() {
228
- return {
229
- name: 'rigour-demo',
230
- version: '1.0.0',
231
- dependencies: { express: '^4.18.0', zod: '^3.22.0' },
232
- };
233
- }
234
- export async function writeIssueFiles(dir) {
235
- // Issue 1: Hardcoded API key
236
- await fs.writeFile(path.join(dir, 'src', 'auth.ts'), `
237
- import express from 'express';
238
-
239
- const API_KEY = "sk-live-4f3c2b1a0987654321abcdef";
240
- const DB_PASSWORD = "super_secret_p@ssw0rd!";
241
-
242
- export function authenticate(req: express.Request) {
243
- const token = req.headers.authorization;
244
- if (token === API_KEY) {
245
- return { authenticated: true };
246
- }
247
- return { authenticated: false };
248
- }
249
-
250
- export function connectDatabase() {
251
- return { host: 'prod-db.internal', password: DB_PASSWORD };
252
- }
253
- `.trim());
254
- // Issue 2: Unhandled promise
255
- await fs.writeFile(path.join(dir, 'src', 'api-handler.ts'), `
256
- import express from 'express';
257
-
258
- export async function fetchUserData(userId: string) {
259
- const response = await fetch(\`https://api.example.com/users/\${userId}\`);
260
- return response.json();
261
- }
262
-
263
- export function handleRequest(req: express.Request, res: express.Response) {
264
- fetchUserData(req.params.id);
265
- res.send('Processing...');
266
- }
267
-
268
- export function batchProcess(ids: string[]) {
269
- ids.forEach(id => fetchUserData(id));
270
- }
271
- `.trim());
272
- // Issue 3: Hallucinated import
273
- await fs.writeFile(path.join(dir, 'src', 'data-loader.ts'), `
274
- import { z } from 'zod';
275
- import { magicParser } from 'ai-data-magic';
276
- import { ultraCache } from 'quantum-cache-pro';
277
-
278
- const schema = z.object({
279
- name: z.string(),
280
- email: z.string().email(),
281
- });
282
-
283
- export function loadData(raw: unknown) {
284
- const parsed = schema.parse(raw);
285
- return parsed;
286
- }
287
- `.trim());
288
- // Issue 4: Placeholder markers
289
- await fs.writeFile(path.join(dir, 'src', 'utils.ts'), `
290
- // NOTE: Claude suggested this but I need to review
291
- // NOTE: This function has edge cases
292
- export function formatDate(date: Date): string {
293
- return date.toISOString().split('T')[0];
294
- }
295
-
296
- export function sanitizeInput(input: string): string {
297
- // NOTE: Add proper sanitization
298
- return input.trim();
299
- }
300
- `.trim());
301
- }
302
- export async function writeGodFile(dir) {
303
- const lines = [
304
- '// Auto-generated data processing module',
305
- 'export class DataProcessor {',
306
- ];
307
- for (let i = 0; i < 60; i++) {
308
- lines.push(` process${i}(data: any) {`);
309
- lines.push(` const result = data.map((x: any) => x * ${i + 1});`);
310
- lines.push(` if (result.length > ${i * 10}) {`);
311
- lines.push(` return result.slice(0, ${i * 10});`);
312
- lines.push(` }`);
313
- lines.push(` return result;`);
314
- lines.push(` }`);
315
- }
316
- lines.push('}');
317
- await fs.writeFile(path.join(dir, 'src', 'god-file.ts'), lines.join('\n'));
318
- }
319
- // ── Audit report generator ──────────────────────────────────────────
320
- export async function generateDemoAudit(dir, report, outputPath) {
321
- const stats = report.stats || {};
322
- const failures = report.failures || [];
323
- const lines = [];
324
- lines.push('# Rigour Audit Report — Demo');
325
- lines.push('');
326
- lines.push(`**Generated:** ${new Date().toISOString()}`);
327
- lines.push(`**Status:** ${report.status}`);
328
- lines.push(`**Score:** ${stats.score ?? 100}/100`);
329
- if (stats.ai_health_score !== undefined) {
330
- lines.push(`**AI Health:** ${stats.ai_health_score}/100`);
331
- }
332
- if (stats.structural_score !== undefined) {
333
- lines.push(`**Structural:** ${stats.structural_score}/100`);
334
- }
335
- lines.push('');
336
- lines.push('## Violations');
337
- lines.push('');
338
- for (let i = 0; i < failures.length; i++) {
339
- const f = failures[i];
340
- lines.push(`### ${i + 1}. [${(f.severity || 'medium').toUpperCase()}] ${f.title}`);
341
- lines.push(`- **ID:** \`${f.id}\``);
342
- lines.push(`- **Provenance:** ${f.provenance || 'traditional'}`);
343
- lines.push(`- **Details:** ${f.details}`);
344
- if (f.files?.length) {
345
- lines.push(`- **Files:** ${f.files.join(', ')}`);
346
- }
347
- if (f.hint) {
348
- lines.push(`- **Hint:** ${f.hint}`);
349
- }
350
- lines.push('');
351
- }
352
- lines.push('---');
353
- lines.push('*Generated by Rigour — https://rigour.run*');
354
- lines.push('*Research: https://zenodo.org/records/18673564*');
355
- await fs.writeFile(outputPath, lines.join('\n'));
356
- }
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * rigour demo
3
3
  *
4
- * Creates a temp project with intentional AI-generated code issues,
5
- * runs Rigour against it, and shows the full experience.
6
- * @since v2.17.0 (extended v3.0.0)
4
+ * Creates a temp project (or clones a real repo) with intentional
5
+ * AI-generated code issues, runs Rigour against it in real time.
6
+ * @since v2.17.0 (extended v3.0.0, repo mode v3.1.0)
7
7
  */
8
8
  import type { DemoOptions } from './demo-helpers.js';
9
9
  export type { DemoOptions } from './demo-helpers.js';
@@ -1,22 +1,31 @@
1
1
  /**
2
2
  * rigour demo
3
3
  *
4
- * Creates a temp project with intentional AI-generated code issues,
5
- * runs Rigour against it, and shows the full experience.
6
- * @since v2.17.0 (extended v3.0.0)
4
+ * Creates a temp project (or clones a real repo) with intentional
5
+ * AI-generated code issues, runs Rigour against it in real time.
6
+ * @since v2.17.0 (extended v3.0.0, repo mode v3.1.0)
7
7
  */
8
8
  import path from 'path';
9
9
  import fs from 'fs-extra';
10
10
  import os from 'os';
11
11
  import chalk from 'chalk';
12
+ import { execSync } from 'child_process';
12
13
  import { pause, typewrite } from './demo-helpers.js';
13
- import { printBanner, printPlantedIssues, printClosing } from './demo-display.js';
14
+ import { printBanner, printPlantedIssues, printClosing, simulateCodeWrite, simulateHookCatch } from './demo-display.js';
14
15
  import { runHooksDemo, runFullGates, runBeforeAfterDemo, scaffoldDemoProject } from './demo-scenarios.js';
15
- // ── Main demo command ───────────────────────────────────────────────
16
+ import { TS_INJECTIONS, PYTHON_INJECTIONS, TS_FIXES } from './demo-injections.js';
16
17
  export async function demoCommand(options = {}) {
17
18
  const isCinematic = !!options.cinematic;
18
19
  const showHooks = !!options.hooks || isCinematic;
19
20
  printBanner(isCinematic);
21
+ if (options.repo) {
22
+ await runRepoDemo(options);
23
+ return;
24
+ }
25
+ // Original synthetic demo flow
26
+ await runSyntheticDemo(options, isCinematic, showHooks);
27
+ }
28
+ async function runSyntheticDemo(options, isCinematic, showHooks) {
20
29
  if (isCinematic) {
21
30
  await typewrite(chalk.bold('Rigour Demo — Watch AI code governance in real time.\n'), options);
22
31
  await pause(800, options);
@@ -24,7 +33,6 @@ export async function demoCommand(options = {}) {
24
33
  else {
25
34
  console.log(chalk.bold('Rigour Demo — See AI code governance in action.\n'));
26
35
  }
27
- // 1. Create temp project
28
36
  const demoDir = path.join(os.tmpdir(), `rigour-demo-${Date.now()}`);
29
37
  await fs.ensureDir(demoDir);
30
38
  if (isCinematic) {
@@ -36,19 +44,97 @@ export async function demoCommand(options = {}) {
36
44
  }
37
45
  await scaffoldDemoProject(demoDir);
38
46
  console.log(chalk.green('✓ Demo project scaffolded.\n'));
39
- // 2. Simulate AI agent writing flawed code (cinematic/hooks mode)
40
47
  if (showHooks) {
41
48
  await runHooksDemo(demoDir, options);
42
49
  }
43
50
  else {
44
51
  printPlantedIssues();
45
52
  }
46
- // 3. Run full quality gates
47
53
  await runFullGates(demoDir, options);
48
- // 4. Show "after fix" improvement (cinematic only)
49
54
  if (isCinematic) {
50
55
  await runBeforeAfterDemo(demoDir, options);
51
56
  }
52
- // 5. Closing
53
57
  printClosing(isCinematic);
54
58
  }
59
+ async function runRepoDemo(options) {
60
+ const repoUrl = options.repo;
61
+ const repoName = extractRepoName(repoUrl);
62
+ await typewrite(chalk.bold(`Rigour Live Demo — Real repo, real issues, real-time.\n`), options);
63
+ await pause(600, options);
64
+ // 1. Clone
65
+ const demoDir = path.join(os.tmpdir(), `rigour-demo-${repoName}-${Date.now()}`);
66
+ await typewrite(chalk.dim(`Cloning ${repoUrl}...`), options);
67
+ if (!/^https?:\/\/[^\s;|&]+$/.test(repoUrl)) {
68
+ console.error(chalk.red('Invalid repo URL. Use https://github.com/owner/repo format.'));
69
+ return;
70
+ }
71
+ execSync(`git clone --depth 1 ${repoUrl} ${demoDir}`, { stdio: 'pipe' });
72
+ console.log(chalk.green(`✓ Cloned ${repoName}\n`));
73
+ await pause(400, options);
74
+ // 2. Detect language, ensure rigour.yml, pick injections
75
+ const isPython = await detectPythonRepo(demoDir);
76
+ const injections = isPython ? PYTHON_INJECTIONS : TS_INJECTIONS;
77
+ // Ensure a rigour.yml exists (real repos may not have one)
78
+ if (!await fs.pathExists(path.join(demoDir, 'rigour.yml'))) {
79
+ const { buildDemoConfig } = await import('./demo-scaffold.js');
80
+ const yaml = await import('yaml');
81
+ await fs.writeFile(path.join(demoDir, 'rigour.yml'), yaml.default.stringify(buildDemoConfig()));
82
+ }
83
+ // 3. Show "AI agent modifying codebase..."
84
+ const divider = chalk.cyan('━'.repeat(50));
85
+ console.log(divider);
86
+ await typewrite(chalk.bold.magenta(' Simulating AI agent writing code with hooks active...\n'), options);
87
+ await pause(600, options);
88
+ // 4. Inject drift and simulate hook catches
89
+ for (const injection of injections) {
90
+ await injectAndCatch(demoDir, injection, options);
91
+ }
92
+ console.log('');
93
+ console.log(chalk.magenta.bold(` Hooks caught ${injections.length} issues in real time — before the commit.`));
94
+ console.log(divider);
95
+ console.log('');
96
+ await pause(1000, options);
97
+ // 5. Run full gates
98
+ await runFullGates(demoDir, options);
99
+ // 6. Fix and show improvement (cinematic)
100
+ if (options.cinematic && !isPython) {
101
+ await runRepoBeforeAfter(demoDir, options);
102
+ }
103
+ // 7. Closing
104
+ printClosing(true);
105
+ }
106
+ async function injectAndCatch(demoDir, injection, options) {
107
+ const filePath = path.join(demoDir, injection.filename);
108
+ await fs.ensureDir(path.dirname(filePath));
109
+ console.log(chalk.blue.bold(` Agent: Write → ${injection.filename}`));
110
+ await simulateCodeWrite(injection.filename, injection.code.split('\n'), options);
111
+ await fs.writeFile(filePath, injection.code);
112
+ await simulateHookCatch(injection.gate, injection.filename, injection.hookMessage, injection.severity, options);
113
+ console.log('');
114
+ }
115
+ async function runRepoBeforeAfter(demoDir, options) {
116
+ console.log(chalk.bold.green('Simulating agent fixing issues...\n'));
117
+ await pause(600, options);
118
+ for (const [filename, fixedCode] of Object.entries(TS_FIXES)) {
119
+ await typewrite(chalk.dim(` Agent: Fixing ${filename}...`), options);
120
+ await fs.writeFile(path.join(demoDir, filename), fixedCode);
121
+ console.log(chalk.green(` ✓ Fixed: ${filename}`));
122
+ await pause(300, options);
123
+ }
124
+ console.log('');
125
+ await pause(500, options);
126
+ console.log(chalk.bold.blue('Re-running quality gates after fixes...\n'));
127
+ await runFullGates(demoDir, options);
128
+ }
129
+ function extractRepoName(url) {
130
+ const match = url.match(/([^/]+?)(?:\.git)?$/);
131
+ return match ? match[1] : 'repo';
132
+ }
133
+ async function detectPythonRepo(dir) {
134
+ const pyFiles = ['requirements.txt', 'setup.py', 'pyproject.toml', 'Pipfile'];
135
+ for (const f of pyFiles) {
136
+ if (await fs.pathExists(path.join(dir, f)))
137
+ return true;
138
+ }
139
+ return false;
140
+ }
@@ -0,0 +1,8 @@
1
+ import type { Report, Config } from '@rigour-labs/core';
2
+ import type { DeepOptions } from '@rigour-labs/core';
3
+ import type { ScanOptions, StackSignals } from './scan.js';
4
+ export declare function buildDeepOpts(options: ScanOptions, isSilent: boolean): DeepOptions & {
5
+ onProgress?: (msg: string) => void;
6
+ };
7
+ export declare function persistDeepResults(cwd: string, report: Report, isDeep: boolean, options: ScanOptions): void;
8
+ export declare function renderDeepScanResults(report: Report, stackSignals: StackSignals, config: Config, cwd: string): void;
@@ -0,0 +1,164 @@
1
+ import chalk from 'chalk';
2
+ import { getScoreTrend, resolveDeepOptions } from '@rigour-labs/core';
3
+ import { extractHallucinatedImports, renderCoverageWarnings } from './scan.js';
4
+ export function buildDeepOpts(options, isSilent) {
5
+ const resolved = resolveDeepOptions({
6
+ apiKey: options.apiKey,
7
+ provider: options.provider,
8
+ apiBaseUrl: options.apiBaseUrl,
9
+ modelName: options.modelName,
10
+ });
11
+ const hasApiKey = !!resolved.apiKey;
12
+ const agentCount = Math.max(1, parseInt(options.agents || '1', 10) || 1);
13
+ return {
14
+ enabled: true,
15
+ pro: !!options.pro,
16
+ apiKey: resolved.apiKey,
17
+ provider: hasApiKey ? (resolved.provider || 'claude') : 'local',
18
+ apiBaseUrl: resolved.apiBaseUrl,
19
+ modelName: resolved.modelName,
20
+ agents: agentCount > 1 ? agentCount : undefined,
21
+ onProgress: isSilent ? undefined : (msg) => {
22
+ process.stderr.write(msg + '\n');
23
+ },
24
+ };
25
+ }
26
+ export function persistDeepResults(cwd, report, isDeep, options) {
27
+ if (!isDeep)
28
+ return;
29
+ try {
30
+ import('@rigour-labs/core').then(({ openDatabase, insertScan, insertFindings }) => {
31
+ const db = openDatabase();
32
+ if (!db)
33
+ return;
34
+ const repoName = require('path').basename(cwd);
35
+ const scanId = insertScan(db, repoName, report, {
36
+ deepTier: report.stats.deep?.tier || (options.pro ? 'pro' : 'deep'),
37
+ deepModel: report.stats.deep?.model,
38
+ });
39
+ insertFindings(db, scanId, report.failures);
40
+ db.close();
41
+ }).catch(() => { });
42
+ }
43
+ catch { /* silent */ }
44
+ }
45
+ function severityIcon(s) {
46
+ switch (s) {
47
+ case 'critical': return chalk.red.bold('CRIT');
48
+ case 'high': return chalk.red('HIGH');
49
+ case 'medium': return chalk.yellow('MED ');
50
+ case 'low': return chalk.dim('LOW ');
51
+ case 'info': return chalk.dim('INFO');
52
+ default: return chalk.yellow('MED ');
53
+ }
54
+ }
55
+ export function renderDeepScanResults(report, stackSignals, config, cwd) {
56
+ const stats = report.stats;
57
+ const aiHealth = stats.ai_health_score ?? 100;
58
+ const codeQuality = stats.code_quality_score ?? stats.structural_score ?? 100;
59
+ const overall = stats.score ?? 100;
60
+ const scoreColor = (s) => s >= 80 ? chalk.green : s >= 60 ? chalk.yellow : chalk.red;
61
+ console.log(` ${chalk.bold('AI Health:')} ${scoreColor(aiHealth).bold(aiHealth + '/100')}`);
62
+ console.log(` ${chalk.bold('Code Quality:')} ${scoreColor(codeQuality).bold(codeQuality + '/100')}`);
63
+ console.log(` ${chalk.bold('Overall:')} ${scoreColor(overall).bold(overall + '/100')}`);
64
+ console.log('');
65
+ const isLocal = stats.deep?.tier ? stats.deep.tier !== 'cloud' : true;
66
+ if (isLocal) {
67
+ console.log(chalk.green(' 🔒 Local sidecar execution. Code remains on this machine.'));
68
+ }
69
+ else {
70
+ console.log(chalk.yellow(` ☁️ Cloud execution. Code context sent to ${stats.deep?.tier || 'cloud'} API.`));
71
+ }
72
+ if (stats.deep) {
73
+ const model = stats.deep.model || 'unknown';
74
+ const ms = stats.deep.total_ms ? ` ${(stats.deep.total_ms / 1000).toFixed(1)}s` : '';
75
+ console.log(chalk.dim(` Model: ${model} (${stats.deep.tier})${ms}`));
76
+ }
77
+ console.log('');
78
+ renderScaryHeadlines(report.failures);
79
+ renderCategorizedFindings(report.failures);
80
+ renderCoverageWarnings(stackSignals);
81
+ const trend = getScoreTrend(cwd);
82
+ if (trend && trend.recentScores.length >= 3) {
83
+ const arrow = trend.direction === 'improving' ? '↑' : trend.direction === 'degrading' ? '↓' : '→';
84
+ const color = trend.direction === 'improving' ? chalk.green : trend.direction === 'degrading' ? chalk.red : chalk.dim;
85
+ console.log(color(`\nTrend: ${trend.recentScores.join(' → ')} ${arrow}`));
86
+ }
87
+ console.log(chalk.yellow(`\nFull report: ${config.output.report_path}`));
88
+ if (report.status === 'FAIL') {
89
+ console.log(chalk.yellow('Fix packet: rigour-fix-packet.json'));
90
+ }
91
+ console.log(chalk.dim(`Finished in ${(stats.duration_ms / 1000).toFixed(1)}s | Score: ${overall}/100`));
92
+ console.log('');
93
+ if (report.status === 'FAIL') {
94
+ console.log(chalk.bold('Next steps:'));
95
+ console.log(` ${chalk.cyan('rigour explain')} — plain-English fix suggestions`);
96
+ console.log(` ${chalk.cyan('rigour init')} — add quality gates to your project`);
97
+ }
98
+ else {
99
+ console.log(chalk.green.bold('✓ This repo is clean.'));
100
+ }
101
+ }
102
+ function renderScaryHeadlines(failures) {
103
+ const secrets = failures.filter(f => f.id === 'security-patterns' && f.severity === 'critical');
104
+ const fakeImports = extractHallucinatedImports(failures);
105
+ const phantoms = failures.filter(f => f.id === 'phantom-apis');
106
+ let count = 0;
107
+ if (secrets.length > 0) {
108
+ console.log(chalk.red.bold(`🔑 HARDCODED SECRETS: ${secrets.length} credential(s) exposed`));
109
+ count++;
110
+ }
111
+ if (fakeImports.length > 0) {
112
+ const unique = [...new Set(fakeImports)];
113
+ console.log(chalk.red.bold(`📦 HALLUCINATED PACKAGES: ${unique.length} import(s) don't exist`));
114
+ count++;
115
+ }
116
+ if (phantoms.length > 0) {
117
+ console.log(chalk.red.bold(`👻 PHANTOM APIs: ${phantoms.length} call(s) to non-existent methods`));
118
+ count++;
119
+ }
120
+ if (count > 0)
121
+ console.log('');
122
+ }
123
+ function renderCategorizedFindings(failures) {
124
+ const deep = failures.filter((f) => f.provenance === 'deep-analysis');
125
+ const aiDrift = failures.filter((f) => f.provenance === 'ai-drift');
126
+ const security = failures.filter((f) => f.provenance === 'security');
127
+ const other = failures.filter((f) => f.provenance !== 'deep-analysis' && f.provenance !== 'ai-drift' && f.provenance !== 'security');
128
+ if (deep.length > 0) {
129
+ console.log(chalk.bold(` ── Deep Analysis (${deep.length} verified) ──\n`));
130
+ for (const f of deep.slice(0, 6)) {
131
+ const desc = (f.details || f.title).substring(0, 120);
132
+ console.log(` ${severityIcon(f.severity)} [${f.id}] ${f.files?.[0] || ''}`);
133
+ console.log(` ${desc}`);
134
+ if (f.hint)
135
+ console.log(` → ${f.hint}`);
136
+ console.log('');
137
+ }
138
+ }
139
+ if (aiDrift.length > 0) {
140
+ console.log(chalk.bold(` ── AI Drift (${aiDrift.length}) ──\n`));
141
+ for (const f of aiDrift.slice(0, 5)) {
142
+ console.log(` ${severityIcon(f.severity)} ${f.title}`);
143
+ if (f.files?.length)
144
+ console.log(chalk.dim(` ${f.files.slice(0, 2).join(', ')}`));
145
+ console.log('');
146
+ }
147
+ }
148
+ if (security.length > 0) {
149
+ console.log(chalk.bold(` ── Security (${security.length}) ──\n`));
150
+ for (const f of security.slice(0, 5)) {
151
+ console.log(` ${severityIcon(f.severity)} ${f.title}`);
152
+ if (f.hint)
153
+ console.log(chalk.cyan(` ${f.hint}`));
154
+ console.log('');
155
+ }
156
+ }
157
+ if (other.length > 0) {
158
+ console.log(chalk.bold(` ── Quality (${other.length}) ──\n`));
159
+ for (const f of other.slice(0, 5)) {
160
+ console.log(` ${severityIcon(f.severity)} [${f.id}] ${f.title}`);
161
+ console.log('');
162
+ }
163
+ }
164
+ }
@@ -1,6 +1,22 @@
1
+ import { type Failure } from '@rigour-labs/core';
1
2
  export interface ScanOptions {
2
3
  ci?: boolean;
3
4
  json?: boolean;
4
5
  config?: string;
6
+ deep?: boolean;
7
+ pro?: boolean;
8
+ apiKey?: string;
9
+ provider?: string;
10
+ apiBaseUrl?: string;
11
+ modelName?: string;
12
+ agents?: string;
13
+ }
14
+ export interface StackSignals {
15
+ languages: string[];
16
+ hasDocker: boolean;
17
+ hasTerraform: boolean;
18
+ hasSql: boolean;
5
19
  }
6
20
  export declare function scanCommand(cwd: string, files?: string[], options?: ScanOptions): Promise<void>;
21
+ export declare function renderCoverageWarnings(stackSignals: StackSignals): void;
22
+ export declare function extractHallucinatedImports(failures: Failure[]): string[];
@@ -4,6 +4,7 @@ import chalk from 'chalk';
4
4
  import yaml from 'yaml';
5
5
  import { globby } from 'globby';
6
6
  import { GateRunner, ConfigSchema, DiscoveryService, FixPacketService, recordScore, getScoreTrend, } from '@rigour-labs/core';
7
+ import { buildDeepOpts, persistDeepResults, renderDeepScanResults } from './scan-deep.js';
7
8
  // Exit codes per spec
8
9
  const EXIT_PASS = 0;
9
10
  const EXIT_FAIL = 1;
@@ -48,43 +49,30 @@ export async function scanCommand(cwd, files = [], options = {}) {
48
49
  try {
49
50
  const scanCtx = await resolveScanConfig(cwd, options);
50
51
  const stackSignals = await detectStackSignals(cwd);
51
- if (!options.ci && !options.json) {
52
- renderScanHeader(scanCtx, stackSignals);
52
+ const isDeep = !!options.deep || !!options.pro || !!options.apiKey;
53
+ const isSilent = !!options.ci || !!options.json;
54
+ if (!isSilent) {
55
+ renderScanHeader(scanCtx, stackSignals, isDeep);
53
56
  }
54
57
  const runner = new GateRunner(scanCtx.config);
55
- const report = await runner.run(cwd, files.length > 0 ? files : undefined);
56
- // Write machine report and score history
57
- const reportPath = path.join(cwd, scanCtx.config.output.report_path);
58
- await fs.writeJson(reportPath, report, { spaces: 2 });
59
- recordScore(cwd, report);
60
- // Generate fix packet on failure
61
- if (report.status === 'FAIL') {
62
- const fixPacketService = new FixPacketService();
63
- const fixPacket = fixPacketService.generate(report, scanCtx.config);
64
- const fixPacketPath = path.join(cwd, 'rigour-fix-packet.json');
65
- await fs.writeJson(fixPacketPath, fixPacket, { spaces: 2 });
66
- }
58
+ const deepOpts = isDeep ? buildDeepOpts(options, isSilent) : undefined;
59
+ const report = await runner.run(cwd, files.length > 0 ? files : undefined, deepOpts);
60
+ await writeReportArtifacts(cwd, report, scanCtx.config);
61
+ persistDeepResults(cwd, report, isDeep, options);
67
62
  if (options.json) {
68
- process.stdout.write(JSON.stringify({
69
- mode: scanCtx.mode,
70
- preset: scanCtx.detectedPreset ?? scanCtx.config.preset,
71
- paradigm: scanCtx.detectedParadigm ?? scanCtx.config.paradigm,
72
- stack: stackSignals,
73
- report,
74
- }, null, 2) + '\n');
75
- process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
63
+ outputJson(scanCtx, stackSignals, report);
64
+ return;
76
65
  }
77
66
  if (options.ci) {
78
- const score = report.stats.score ?? 0;
79
- if (report.status === 'PASS') {
80
- console.log(`PASS (${score}/100)`);
81
- }
82
- else {
83
- console.log(`FAIL: ${report.failures.length} violation(s) | Score: ${score}/100`);
84
- }
85
- process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
67
+ outputCi(report);
68
+ return;
69
+ }
70
+ if (isDeep) {
71
+ renderDeepScanResults(report, stackSignals, scanCtx.config, cwd);
72
+ }
73
+ else {
74
+ renderScanResults(report, stackSignals, scanCtx.config.output.report_path, cwd);
86
75
  }
87
- renderScanResults(report, stackSignals, scanCtx.config.output.report_path, cwd);
88
76
  process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
89
77
  }
90
78
  catch (error) {
@@ -99,6 +87,36 @@ export async function scanCommand(cwd, files = [], options = {}) {
99
87
  process.exit(EXIT_INTERNAL_ERROR);
100
88
  }
101
89
  }
90
+ async function writeReportArtifacts(cwd, report, config) {
91
+ const reportPath = path.join(cwd, config.output.report_path);
92
+ await fs.writeJson(reportPath, report, { spaces: 2 });
93
+ recordScore(cwd, report);
94
+ if (report.status === 'FAIL') {
95
+ const fixPacketService = new FixPacketService();
96
+ const fixPacket = fixPacketService.generate(report, config);
97
+ await fs.writeJson(path.join(cwd, 'rigour-fix-packet.json'), fixPacket, { spaces: 2 });
98
+ }
99
+ }
100
+ function outputJson(scanCtx, stackSignals, report) {
101
+ process.stdout.write(JSON.stringify({
102
+ mode: scanCtx.mode,
103
+ preset: scanCtx.detectedPreset ?? scanCtx.config.preset,
104
+ paradigm: scanCtx.detectedParadigm ?? scanCtx.config.paradigm,
105
+ stack: stackSignals,
106
+ report,
107
+ }, null, 2) + '\n');
108
+ process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
109
+ }
110
+ function outputCi(report) {
111
+ const score = report.stats.score ?? 0;
112
+ if (report.status === 'PASS') {
113
+ console.log(`PASS (${score}/100)`);
114
+ }
115
+ else {
116
+ console.log(`FAIL: ${report.failures.length} violation(s) | Score: ${score}/100`);
117
+ }
118
+ process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
119
+ }
102
120
  async function resolveScanConfig(cwd, options) {
103
121
  const explicitConfig = options.config ? path.resolve(cwd, options.config) : undefined;
104
122
  const defaultConfig = path.join(cwd, 'rigour.yml');
@@ -142,9 +160,12 @@ async function detectStackSignals(cwd) {
142
160
  hasSql: sqlMatches.length > 0,
143
161
  };
144
162
  }
145
- function renderScanHeader(scanCtx, stackSignals) {
146
- console.log(chalk.bold.cyan('\nRigour Scan'));
147
- console.log(chalk.dim('Zero-config security and AI-drift sweep using existing Rigour gates.\n'));
163
+ function renderScanHeader(scanCtx, stackSignals, isDeep = false) {
164
+ console.log(chalk.bold.cyan('\nRigour Scan') + (isDeep ? chalk.blue.bold(' + Deep Analysis') : ''));
165
+ const desc = isDeep
166
+ ? 'Zero-config sweep with LLM-powered deep analysis.'
167
+ : 'Zero-config security and AI-drift sweep using existing Rigour gates.';
168
+ console.log(chalk.dim(desc + '\n'));
148
169
  const modeLabel = scanCtx.mode === 'existing-config'
149
170
  ? `Using existing config: ${path.basename(scanCtx.configPath || 'rigour.yml')}`
150
171
  : 'Auto-discovered config (no rigour.yml required)';
@@ -244,7 +265,7 @@ function renderScanResults(report, stackSignals, reportPath, cwd) {
244
265
  console.log(` ${chalk.cyan('rigour init')} — write quality gates to rigour.yml + CI config`);
245
266
  }
246
267
  }
247
- function renderCoverageWarnings(stackSignals) {
268
+ export function renderCoverageWarnings(stackSignals) {
248
269
  const gaps = [];
249
270
  for (const language of stackSignals.languages) {
250
271
  const supportedBy = Object.entries(HEADLINE_GATE_SUPPORT)
@@ -265,7 +286,7 @@ function renderCoverageWarnings(stackSignals) {
265
286
  gaps.forEach(gap => console.log(chalk.yellow(` - ${gap}`)));
266
287
  }
267
288
  }
268
- function extractHallucinatedImports(failures) {
289
+ export function extractHallucinatedImports(failures) {
269
290
  const fakeImports = [];
270
291
  for (const failure of failures) {
271
292
  if (failure.id !== 'hallucinated-imports')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/cli",
3
- "version": "4.2.2",
3
+ "version": "4.3.0",
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.2.2"
47
+ "@rigour-labs/core": "4.3.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/fs-extra": "^11.0.4",