@rigour-labs/cli 3.0.5 → 3.0.6
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/commands/demo-display.d.ts +11 -0
- package/dist/commands/demo-display.js +181 -0
- package/dist/commands/demo-helpers.d.ts +9 -0
- package/dist/commands/demo-helpers.js +28 -0
- package/dist/commands/demo-scenarios.d.ts +11 -0
- package/dist/commands/demo-scenarios.js +356 -0
- package/dist/commands/demo.d.ts +2 -15
- package/dist/commands/demo.js +5 -569
- package/dist/commands/scan.js +53 -10
- package/package.json +2 -2
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DemoOptions } from './demo-helpers.js';
|
|
2
|
+
export declare function simulateCodeWrite(filename: string, lines: string[], options: DemoOptions): Promise<void>;
|
|
3
|
+
export declare function simulateHookCatch(gate: string, file: string, message: string, severity: string, options: DemoOptions): Promise<void>;
|
|
4
|
+
export declare function renderScoreBar(score: number, label: string, width?: number): string;
|
|
5
|
+
export declare function renderTrendChart(scores: number[]): string;
|
|
6
|
+
export declare function printBanner(cinematic: boolean): void;
|
|
7
|
+
export declare function printPlantedIssues(): void;
|
|
8
|
+
export declare function displayGateResults(report: any, cinematic: boolean): void;
|
|
9
|
+
export declare function printSeverityBreakdown(stats: any): void;
|
|
10
|
+
export declare function printFailure(failure: any): void;
|
|
11
|
+
export declare function printClosing(cinematic: boolean): void;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { pause, getMultiplier, sleep } from './demo-helpers.js';
|
|
3
|
+
// ── Simulated code writing ──────────────────────────────────────────
|
|
4
|
+
export async function simulateCodeWrite(filename, lines, options) {
|
|
5
|
+
const isCinematic = !!options.cinematic;
|
|
6
|
+
const lineDelay = isCinematic ? 40 * getMultiplier(options) : 0;
|
|
7
|
+
console.log(chalk.dim(`\n ${chalk.white('▸')} Writing ${chalk.cyan(filename)}...`));
|
|
8
|
+
if (isCinematic) {
|
|
9
|
+
await pause(200, options);
|
|
10
|
+
}
|
|
11
|
+
for (const line of lines) {
|
|
12
|
+
if (isCinematic) {
|
|
13
|
+
process.stdout.write(chalk.dim(` ${line}\n`));
|
|
14
|
+
await sleep(lineDelay);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (!isCinematic) {
|
|
18
|
+
const preview = lines.slice(0, 3).join('\n ');
|
|
19
|
+
console.log(chalk.dim(` ${preview}`));
|
|
20
|
+
if (lines.length > 3) {
|
|
21
|
+
console.log(chalk.dim(` ... (${lines.length} lines)`));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// ── Hook simulation ─────────────────────────────────────────────────
|
|
26
|
+
export async function simulateHookCatch(gate, file, message, severity, options) {
|
|
27
|
+
if (options.cinematic) {
|
|
28
|
+
await pause(300, options);
|
|
29
|
+
}
|
|
30
|
+
const sevColor = severity === 'critical' ? chalk.red.bold
|
|
31
|
+
: severity === 'high' ? chalk.red
|
|
32
|
+
: chalk.yellow;
|
|
33
|
+
const hookPrefix = chalk.magenta.bold('[rigour/hook]');
|
|
34
|
+
const sevLabel = sevColor(severity.toUpperCase());
|
|
35
|
+
const gateLabel = chalk.red(`[${gate}]`);
|
|
36
|
+
console.log(` ${hookPrefix} ${sevLabel} ${gateLabel} ${chalk.white(file)}`);
|
|
37
|
+
console.log(` ${chalk.dim('→')} ${message}`);
|
|
38
|
+
if (options.cinematic) {
|
|
39
|
+
await pause(400, options);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// ── ASCII score bar ─────────────────────────────────────────────────
|
|
43
|
+
export function renderScoreBar(score, label, width = 30) {
|
|
44
|
+
const filled = Math.round((score / 100) * width);
|
|
45
|
+
const empty = width - filled;
|
|
46
|
+
const color = score >= 80 ? chalk.green : score >= 50 ? chalk.yellow : chalk.red;
|
|
47
|
+
const bar = color('█'.repeat(filled)) + chalk.dim('░'.repeat(empty));
|
|
48
|
+
return ` ${label.padEnd(14)} ${bar} ${color.bold(`${score}/100`)}`;
|
|
49
|
+
}
|
|
50
|
+
// ── ASCII trend chart ───────────────────────────────────────────────
|
|
51
|
+
export function renderTrendChart(scores) {
|
|
52
|
+
const height = 8;
|
|
53
|
+
const lines = [];
|
|
54
|
+
const maxScore = 100;
|
|
55
|
+
lines.push(chalk.dim(' Score Trend:'));
|
|
56
|
+
for (let row = height; row >= 0; row--) {
|
|
57
|
+
const threshold = (row / height) * maxScore;
|
|
58
|
+
let line = chalk.dim(String(Math.round(threshold)).padStart(3) + ' │');
|
|
59
|
+
for (const score of scores) {
|
|
60
|
+
if (score >= threshold) {
|
|
61
|
+
const color = score >= 80 ? chalk.green : score >= 50 ? chalk.yellow : chalk.red;
|
|
62
|
+
line += color(' ██');
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
line += ' ';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
lines.push(line);
|
|
69
|
+
}
|
|
70
|
+
lines.push(chalk.dim(' └' + '───'.repeat(scores.length)));
|
|
71
|
+
const labels = scores.map((_, i) => ` R${i + 1}`);
|
|
72
|
+
lines.push(chalk.dim(' ' + labels.join('')));
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
75
|
+
// ── Banner ───────────────────────────────────────────────────────────
|
|
76
|
+
export function printBanner(cinematic) {
|
|
77
|
+
const banner = chalk.bold.cyan(`
|
|
78
|
+
____ _
|
|
79
|
+
/ __ \\(_)____ ___ __ __ _____
|
|
80
|
+
/ /_/ // // __ \`/ / / / / // ___/
|
|
81
|
+
/ _, _// // /_/ // /_/ / / // /
|
|
82
|
+
/_/ |_|/_/ \\__, / \\__,_/_/ /_/
|
|
83
|
+
/____/
|
|
84
|
+
`);
|
|
85
|
+
console.log(banner);
|
|
86
|
+
}
|
|
87
|
+
// ── Planted issues (non-cinematic) ──────────────────────────────────
|
|
88
|
+
export function printPlantedIssues() {
|
|
89
|
+
console.log(chalk.bold.yellow('Planted issues:'));
|
|
90
|
+
console.log(chalk.dim(' 1. src/auth.ts — Hardcoded API key (security)'));
|
|
91
|
+
console.log(chalk.dim(' 2. src/api-handler.ts — Unhandled promise (AI drift)'));
|
|
92
|
+
console.log(chalk.dim(' 3. src/data-loader.ts — Hallucinated import (AI drift)'));
|
|
93
|
+
console.log(chalk.dim(' 4. src/utils.ts — TODO marker left by AI'));
|
|
94
|
+
console.log(chalk.dim(' 5. src/god-file.ts — 350+ lines (structural)'));
|
|
95
|
+
console.log('');
|
|
96
|
+
}
|
|
97
|
+
// ── Hooks demo: simulate AI agent → hook catches ────────────────────
|
|
98
|
+
// ── Closing section ─────────────────────────────────────────────────
|
|
99
|
+
export function displayGateResults(report, cinematic) {
|
|
100
|
+
const stats = report.stats;
|
|
101
|
+
if (report.status === 'FAIL') {
|
|
102
|
+
console.log(chalk.red.bold('✘ FAIL — Quality gate violations found.\n'));
|
|
103
|
+
// Score bars
|
|
104
|
+
if (stats.score !== undefined) {
|
|
105
|
+
console.log(renderScoreBar(stats.score, 'Overall'));
|
|
106
|
+
}
|
|
107
|
+
if (stats.ai_health_score !== undefined) {
|
|
108
|
+
console.log(renderScoreBar(stats.ai_health_score, 'AI Health'));
|
|
109
|
+
}
|
|
110
|
+
if (stats.structural_score !== undefined) {
|
|
111
|
+
console.log(renderScoreBar(stats.structural_score, 'Structural'));
|
|
112
|
+
}
|
|
113
|
+
console.log('');
|
|
114
|
+
// Severity breakdown
|
|
115
|
+
printSeverityBreakdown(stats);
|
|
116
|
+
// Violations list
|
|
117
|
+
for (const failure of report.failures) {
|
|
118
|
+
printFailure(failure);
|
|
119
|
+
}
|
|
120
|
+
console.log('');
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
console.log(chalk.green.bold('✔ PASS — All quality gates satisfied.\n'));
|
|
124
|
+
}
|
|
125
|
+
console.log(chalk.dim(`Finished in ${stats.duration_ms}ms\n`));
|
|
126
|
+
}
|
|
127
|
+
export function printSeverityBreakdown(stats) {
|
|
128
|
+
if (!stats.severity_breakdown) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const parts = Object.entries(stats.severity_breakdown)
|
|
132
|
+
.filter(([, count]) => count > 0)
|
|
133
|
+
.map(([sev, count]) => {
|
|
134
|
+
const color = sev === 'critical' ? chalk.red.bold
|
|
135
|
+
: sev === 'high' ? chalk.red
|
|
136
|
+
: sev === 'medium' ? chalk.yellow
|
|
137
|
+
: chalk.dim;
|
|
138
|
+
return color(`${sev}: ${count}`);
|
|
139
|
+
});
|
|
140
|
+
if (parts.length > 0) {
|
|
141
|
+
console.log('Severity: ' + parts.join(', ') + '\n');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export function printFailure(failure) {
|
|
145
|
+
const sevLabel = failure.severity === 'critical' ? chalk.red.bold('CRIT')
|
|
146
|
+
: failure.severity === 'high' ? chalk.red('HIGH')
|
|
147
|
+
: failure.severity === 'medium' ? chalk.yellow('MED ')
|
|
148
|
+
: chalk.dim('LOW ');
|
|
149
|
+
const prov = failure.provenance ? chalk.dim(`[${failure.provenance}]`) : '';
|
|
150
|
+
console.log(` ${sevLabel} ${prov} ${chalk.red(`[${failure.id}]`)} ${failure.title}`);
|
|
151
|
+
if (failure.hint) {
|
|
152
|
+
console.log(chalk.cyan(` ${failure.hint}`));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export function printClosing(cinematic) {
|
|
156
|
+
const divider = chalk.bold.cyan('━'.repeat(50));
|
|
157
|
+
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)'));
|
|
161
|
+
console.log(chalk.dim(' Enforces structure (file size, complexity, documentation)'));
|
|
162
|
+
console.log(chalk.dim(' Generates audit-ready evidence (scores, trends, reports)'));
|
|
163
|
+
console.log(chalk.dim(' Real-time hooks for Claude, Cursor, Cline, Windsurf'));
|
|
164
|
+
console.log(divider);
|
|
165
|
+
console.log('');
|
|
166
|
+
if (cinematic) {
|
|
167
|
+
console.log(chalk.bold('Peer-reviewed research:'));
|
|
168
|
+
console.log(chalk.white(' Deterministic Quality Gates for AI-Generated Code'));
|
|
169
|
+
console.log(chalk.dim(' https://zenodo.org/records/18673564'));
|
|
170
|
+
console.log('');
|
|
171
|
+
}
|
|
172
|
+
console.log(chalk.bold('Get started:'));
|
|
173
|
+
console.log(chalk.white(' $ npx @rigour-labs/cli init'));
|
|
174
|
+
console.log(chalk.white(' $ npx @rigour-labs/cli check'));
|
|
175
|
+
console.log(chalk.white(' $ npx @rigour-labs/cli hooks init'));
|
|
176
|
+
console.log('');
|
|
177
|
+
console.log(chalk.dim('GitHub: https://github.com/rigour-labs/rigour'));
|
|
178
|
+
console.log(chalk.dim('Docs: https://docs.rigour.run'));
|
|
179
|
+
console.log(chalk.dim('Paper: https://zenodo.org/records/18673564\n'));
|
|
180
|
+
console.log(chalk.dim.italic('If this saved you from a bad commit, star the repo ⭐'));
|
|
181
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface DemoOptions {
|
|
2
|
+
cinematic?: boolean;
|
|
3
|
+
hooks?: boolean;
|
|
4
|
+
speed?: 'fast' | 'normal' | 'slow';
|
|
5
|
+
}
|
|
6
|
+
export declare function getMultiplier(options: DemoOptions): number;
|
|
7
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
8
|
+
export declare function pause(ms: number, options: DemoOptions): Promise<void>;
|
|
9
|
+
export declare function typewrite(text: string, options: DemoOptions, charDelay?: number): Promise<void>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const SPEED_MULTIPLIERS = {
|
|
2
|
+
fast: 0.3,
|
|
3
|
+
normal: 1.0,
|
|
4
|
+
slow: 1.8,
|
|
5
|
+
};
|
|
6
|
+
// ── Timing helpers ──────────────────────────────────────────────────
|
|
7
|
+
export function getMultiplier(options) {
|
|
8
|
+
return SPEED_MULTIPLIERS[options.speed || 'normal'] || 1.0;
|
|
9
|
+
}
|
|
10
|
+
export function sleep(ms) {
|
|
11
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
12
|
+
}
|
|
13
|
+
export async function pause(ms, options) {
|
|
14
|
+
await sleep(ms * getMultiplier(options));
|
|
15
|
+
}
|
|
16
|
+
// ── Typewriter effect ───────────────────────────────────────────────
|
|
17
|
+
export async function typewrite(text, options, charDelay = 18) {
|
|
18
|
+
if (!options.cinematic) {
|
|
19
|
+
process.stdout.write(text + '\n');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const delay = charDelay * getMultiplier(options);
|
|
23
|
+
for (const char of text) {
|
|
24
|
+
process.stdout.write(char);
|
|
25
|
+
await sleep(delay);
|
|
26
|
+
}
|
|
27
|
+
process.stdout.write('\n');
|
|
28
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DemoOptions } from './demo-helpers.js';
|
|
2
|
+
export declare function runHooksDemo(demoDir: string, options: DemoOptions): Promise<void>;
|
|
3
|
+
export declare function simulateAgentWrite(filename: string, codeLines: string[], gate: string, file: string, message: string, severity: string, options: DemoOptions): Promise<void>;
|
|
4
|
+
export declare function runFullGates(demoDir: string, options: DemoOptions): Promise<void>;
|
|
5
|
+
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>;
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import yaml from 'yaml';
|
|
5
|
+
import { GateRunner, ConfigSchema } from '@rigour-labs/core';
|
|
6
|
+
import { recordScore } from '@rigour-labs/core';
|
|
7
|
+
import { pause, typewrite } from './demo-helpers.js';
|
|
8
|
+
import { simulateCodeWrite, simulateHookCatch, renderScoreBar, renderTrendChart, displayGateResults, } from './demo-display.js';
|
|
9
|
+
// ── Hooks demo: simulate AI agent → hook catches ────────────────────
|
|
10
|
+
export async function runHooksDemo(demoDir, options) {
|
|
11
|
+
const divider = chalk.cyan('━'.repeat(50));
|
|
12
|
+
console.log(divider);
|
|
13
|
+
console.log(chalk.bold.magenta(' Simulating AI agent writing code with hooks active...\n'));
|
|
14
|
+
if (options.cinematic) {
|
|
15
|
+
await pause(600, options);
|
|
16
|
+
}
|
|
17
|
+
// Step 1: AI writes auth.ts with hardcoded key
|
|
18
|
+
await simulateAgentWrite('src/auth.ts', [
|
|
19
|
+
'import express from \'express\';',
|
|
20
|
+
'',
|
|
21
|
+
'const API_KEY = "sk-live-4f3c2b1a0987654321abcdef";',
|
|
22
|
+
'',
|
|
23
|
+
'export function authenticate(req: express.Request) {',
|
|
24
|
+
' return req.headers.authorization === API_KEY;',
|
|
25
|
+
'}',
|
|
26
|
+
], 'security-patterns', 'src/auth.ts:3', 'Possible hardcoded secret or API key', 'critical', options);
|
|
27
|
+
// Step 2: AI writes data-loader.ts with hallucinated import
|
|
28
|
+
await simulateAgentWrite('src/data-loader.ts', [
|
|
29
|
+
'import { z } from \'zod\';',
|
|
30
|
+
'import { magicParser } from \'ai-data-magic\';',
|
|
31
|
+
'',
|
|
32
|
+
'export function loadData(raw: unknown) {',
|
|
33
|
+
' return z.object({ name: z.string() }).parse(raw);',
|
|
34
|
+
'}',
|
|
35
|
+
], 'hallucinated-imports', 'src/data-loader.ts:2', 'Import \'ai-data-magic\' does not resolve to an existing package', 'high', options);
|
|
36
|
+
// Step 3: AI writes api-handler.ts with unhandled promise
|
|
37
|
+
await simulateAgentWrite('src/api-handler.ts', [
|
|
38
|
+
'export async function fetchUser(id: string) {',
|
|
39
|
+
' const res = await fetch(`/api/users/${id}`);',
|
|
40
|
+
' return res.json();',
|
|
41
|
+
'}',
|
|
42
|
+
'',
|
|
43
|
+
'export function handleRequest(req: any, res: any) {',
|
|
44
|
+
' fetchUser(req.params.id); // floating promise',
|
|
45
|
+
' res.send(\'Processing...\');',
|
|
46
|
+
'}',
|
|
47
|
+
], 'promise-safety', 'src/api-handler.ts:7', 'Unhandled promise — fetchUser() called without await or .catch()', 'medium', options);
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log(chalk.magenta.bold(` Hooks caught 3 issues in real time — before the agent finished.`));
|
|
50
|
+
console.log(divider);
|
|
51
|
+
console.log('');
|
|
52
|
+
if (options.cinematic) {
|
|
53
|
+
await pause(1000, options);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export async function simulateAgentWrite(filename, codeLines, gate, file, message, severity, options) {
|
|
57
|
+
console.log(chalk.blue.bold(` Agent: Write → ${filename}`));
|
|
58
|
+
await simulateCodeWrite(filename, codeLines, options);
|
|
59
|
+
await simulateHookCatch(gate, file, message, severity, options);
|
|
60
|
+
console.log('');
|
|
61
|
+
}
|
|
62
|
+
// ── Full gate run ───────────────────────────────────────────────────
|
|
63
|
+
export async function runFullGates(demoDir, options) {
|
|
64
|
+
const isCinematic = !!options.cinematic;
|
|
65
|
+
console.log(chalk.bold.blue('Running full Rigour quality gates...\n'));
|
|
66
|
+
if (isCinematic) {
|
|
67
|
+
await pause(500, options);
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const configContent = await fs.readFile(path.join(demoDir, 'rigour.yml'), 'utf-8');
|
|
71
|
+
const rawConfig = yaml.parse(configContent);
|
|
72
|
+
const config = ConfigSchema.parse(rawConfig);
|
|
73
|
+
const runner = new GateRunner(config);
|
|
74
|
+
const report = await runner.run(demoDir);
|
|
75
|
+
recordScore(demoDir, report);
|
|
76
|
+
const reportPath = path.join(demoDir, config.output.report_path);
|
|
77
|
+
await fs.writeJson(reportPath, report, { spaces: 2 });
|
|
78
|
+
displayGateResults(report, isCinematic);
|
|
79
|
+
await generateArtifacts(demoDir, report, config);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
83
|
+
console.error(chalk.red(`Demo error: ${msg}`));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function generateArtifacts(demoDir, report, config) {
|
|
87
|
+
if (report.status === 'FAIL') {
|
|
88
|
+
const { FixPacketService } = await import('@rigour-labs/core');
|
|
89
|
+
const fixPacketService = new FixPacketService();
|
|
90
|
+
const fixPacket = fixPacketService.generate(report, config);
|
|
91
|
+
const fixPacketPath = path.join(demoDir, 'rigour-fix-packet.json');
|
|
92
|
+
await fs.writeJson(fixPacketPath, fixPacket, { spaces: 2 });
|
|
93
|
+
console.log(chalk.green('✓ Fix packet generated: rigour-fix-packet.json'));
|
|
94
|
+
}
|
|
95
|
+
const auditPath = path.join(demoDir, 'rigour-audit-report.md');
|
|
96
|
+
await generateDemoAudit(demoDir, report, auditPath);
|
|
97
|
+
console.log(chalk.green('✓ Audit report exported: rigour-audit-report.md'));
|
|
98
|
+
console.log('');
|
|
99
|
+
}
|
|
100
|
+
// ── Before/After improvement demo ───────────────────────────────────
|
|
101
|
+
export async function runBeforeAfterDemo(demoDir, options) {
|
|
102
|
+
console.log(chalk.bold.green('Simulating agent fixing issues...\n'));
|
|
103
|
+
await pause(600, options);
|
|
104
|
+
// Fix the auth.ts — remove hardcoded key
|
|
105
|
+
await typewrite(chalk.dim(' Agent: Removing hardcoded API key from src/auth.ts...'), options);
|
|
106
|
+
await fs.writeFile(path.join(demoDir, 'src', 'auth.ts'), `
|
|
107
|
+
import express from 'express';
|
|
108
|
+
|
|
109
|
+
export function authenticate(req: express.Request) {
|
|
110
|
+
const token = req.headers.authorization;
|
|
111
|
+
if (!token) {
|
|
112
|
+
return { authenticated: false };
|
|
113
|
+
}
|
|
114
|
+
// Validate against secure key store
|
|
115
|
+
return { authenticated: validateToken(token) };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function validateToken(token: string): boolean {
|
|
119
|
+
return token.startsWith('Bearer ') && token.length > 20;
|
|
120
|
+
}
|
|
121
|
+
`.trim());
|
|
122
|
+
console.log(chalk.green(' ✓ Fixed: API key moved to environment variable'));
|
|
123
|
+
await pause(300, options);
|
|
124
|
+
// Fix data-loader.ts — remove hallucinated import
|
|
125
|
+
await typewrite(chalk.dim(' Agent: Removing hallucinated import from src/data-loader.ts...'), options);
|
|
126
|
+
await fs.writeFile(path.join(demoDir, 'src', 'data-loader.ts'), `
|
|
127
|
+
import { z } from 'zod';
|
|
128
|
+
|
|
129
|
+
const schema = z.object({
|
|
130
|
+
name: z.string(),
|
|
131
|
+
email: z.string().email(),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export function loadData(raw: unknown) {
|
|
135
|
+
return schema.parse(raw);
|
|
136
|
+
}
|
|
137
|
+
`.trim());
|
|
138
|
+
console.log(chalk.green(' ✓ Fixed: Removed non-existent package imports'));
|
|
139
|
+
await pause(300, options);
|
|
140
|
+
// Fix api-handler.ts — add error handling
|
|
141
|
+
await typewrite(chalk.dim(' Agent: Adding error handling to src/api-handler.ts...'), options);
|
|
142
|
+
await fs.writeFile(path.join(demoDir, 'src', 'api-handler.ts'), `
|
|
143
|
+
import express from 'express';
|
|
144
|
+
|
|
145
|
+
export async function fetchUserData(userId: string) {
|
|
146
|
+
const response = await fetch(\`https://api.example.com/users/\${userId}\`);
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
throw new Error(\`Failed to fetch user: \${response.status}\`);
|
|
149
|
+
}
|
|
150
|
+
return response.json();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function handleRequest(req: express.Request, res: express.Response) {
|
|
154
|
+
try {
|
|
155
|
+
const data = await fetchUserData(req.params.id);
|
|
156
|
+
res.json(data);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
res.status(500).json({ error: 'Failed to fetch user data' });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
`.trim());
|
|
162
|
+
console.log(chalk.green(' ✓ Fixed: Added proper await and error handling'));
|
|
163
|
+
console.log('');
|
|
164
|
+
await pause(500, options);
|
|
165
|
+
// Re-run gates to show improvement
|
|
166
|
+
console.log(chalk.bold.blue('Re-running quality gates after fixes...\n'));
|
|
167
|
+
await pause(400, options);
|
|
168
|
+
try {
|
|
169
|
+
const configContent = await fs.readFile(path.join(demoDir, 'rigour.yml'), 'utf-8');
|
|
170
|
+
const rawConfig = yaml.parse(configContent);
|
|
171
|
+
const config = ConfigSchema.parse(rawConfig);
|
|
172
|
+
const runner = new GateRunner(config);
|
|
173
|
+
const report2 = await runner.run(demoDir);
|
|
174
|
+
recordScore(demoDir, report2);
|
|
175
|
+
const score1 = 35; // approximate first-run score
|
|
176
|
+
const score2 = report2.stats.score ?? 75;
|
|
177
|
+
const remaining = report2.failures.length;
|
|
178
|
+
console.log(chalk.bold('Score improvement:\n'));
|
|
179
|
+
console.log(renderScoreBar(score1, 'Before'));
|
|
180
|
+
console.log(renderScoreBar(score2, 'After'));
|
|
181
|
+
console.log('');
|
|
182
|
+
// Trend chart
|
|
183
|
+
console.log(renderTrendChart([score1, score2]));
|
|
184
|
+
console.log('');
|
|
185
|
+
if (remaining > 0) {
|
|
186
|
+
console.log(chalk.yellow(` ${remaining} issue(s) remaining (structural, TODOs)`));
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
console.log(chalk.green.bold(' All issues resolved!'));
|
|
190
|
+
}
|
|
191
|
+
console.log('');
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
195
|
+
console.error(chalk.red(`Re-check error: ${msg}`));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
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: TODO markers
|
|
289
|
+
await fs.writeFile(path.join(dir, 'src', 'utils.ts'), `
|
|
290
|
+
// TODO: Claude suggested this but I need to review
|
|
291
|
+
// FIXME: 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
|
+
// TODO: 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
|
+
}
|
package/dist/commands/demo.d.ts
CHANGED
|
@@ -3,21 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Creates a temp project with intentional AI-generated code issues,
|
|
5
5
|
* runs Rigour against it, and shows the full experience.
|
|
6
|
-
*
|
|
7
|
-
* Modes:
|
|
8
|
-
* Default: Fast demo — scaffold → check → results
|
|
9
|
-
* --cinematic: Screen-recording optimized — typewriter effects, pauses,
|
|
10
|
-
* simulated AI agent writing code, hooks catching issues
|
|
11
|
-
* --hooks: Focus on the real-time hooks experience
|
|
12
|
-
* --speed: Control pacing (fast / normal / slow)
|
|
13
|
-
*
|
|
14
|
-
* The "flagship demo" — one command to understand Rigour.
|
|
15
|
-
*
|
|
16
6
|
* @since v2.17.0 (extended v3.0.0)
|
|
17
7
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
hooks?: boolean;
|
|
21
|
-
speed?: 'fast' | 'normal' | 'slow';
|
|
22
|
-
}
|
|
8
|
+
import type { DemoOptions } from './demo-helpers.js';
|
|
9
|
+
export type { DemoOptions } from './demo-helpers.js';
|
|
23
10
|
export declare function demoCommand(options?: DemoOptions): Promise<void>;
|
package/dist/commands/demo.js
CHANGED
|
@@ -3,125 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Creates a temp project with intentional AI-generated code issues,
|
|
5
5
|
* runs Rigour against it, and shows the full experience.
|
|
6
|
-
*
|
|
7
|
-
* Modes:
|
|
8
|
-
* Default: Fast demo — scaffold → check → results
|
|
9
|
-
* --cinematic: Screen-recording optimized — typewriter effects, pauses,
|
|
10
|
-
* simulated AI agent writing code, hooks catching issues
|
|
11
|
-
* --hooks: Focus on the real-time hooks experience
|
|
12
|
-
* --speed: Control pacing (fast / normal / slow)
|
|
13
|
-
*
|
|
14
|
-
* The "flagship demo" — one command to understand Rigour.
|
|
15
|
-
*
|
|
16
6
|
* @since v2.17.0 (extended v3.0.0)
|
|
17
7
|
*/
|
|
18
|
-
import fs from 'fs-extra';
|
|
19
8
|
import path from 'path';
|
|
20
|
-
import
|
|
21
|
-
import yaml from 'yaml';
|
|
9
|
+
import fs from 'fs-extra';
|
|
22
10
|
import os from 'os';
|
|
23
|
-
import
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
normal: 1.0,
|
|
28
|
-
slow: 1.8,
|
|
29
|
-
};
|
|
30
|
-
// ── Timing helpers ──────────────────────────────────────────────────
|
|
31
|
-
function getMultiplier(options) {
|
|
32
|
-
return SPEED_MULTIPLIERS[options.speed || 'normal'] || 1.0;
|
|
33
|
-
}
|
|
34
|
-
function sleep(ms) {
|
|
35
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
36
|
-
}
|
|
37
|
-
async function pause(ms, options) {
|
|
38
|
-
await sleep(ms * getMultiplier(options));
|
|
39
|
-
}
|
|
40
|
-
// ── Typewriter effect ───────────────────────────────────────────────
|
|
41
|
-
async function typewrite(text, options, charDelay = 18) {
|
|
42
|
-
if (!options.cinematic) {
|
|
43
|
-
process.stdout.write(text + '\n');
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
const delay = charDelay * getMultiplier(options);
|
|
47
|
-
for (const char of text) {
|
|
48
|
-
process.stdout.write(char);
|
|
49
|
-
await sleep(delay);
|
|
50
|
-
}
|
|
51
|
-
process.stdout.write('\n');
|
|
52
|
-
}
|
|
53
|
-
// ── Simulated code writing ──────────────────────────────────────────
|
|
54
|
-
async function simulateCodeWrite(filename, lines, options) {
|
|
55
|
-
const isCinematic = !!options.cinematic;
|
|
56
|
-
const lineDelay = isCinematic ? 40 * getMultiplier(options) : 0;
|
|
57
|
-
console.log(chalk.dim(`\n ${chalk.white('▸')} Writing ${chalk.cyan(filename)}...`));
|
|
58
|
-
if (isCinematic) {
|
|
59
|
-
await pause(200, options);
|
|
60
|
-
}
|
|
61
|
-
for (const line of lines) {
|
|
62
|
-
if (isCinematic) {
|
|
63
|
-
process.stdout.write(chalk.dim(` ${line}\n`));
|
|
64
|
-
await sleep(lineDelay);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
if (!isCinematic) {
|
|
68
|
-
const preview = lines.slice(0, 3).join('\n ');
|
|
69
|
-
console.log(chalk.dim(` ${preview}`));
|
|
70
|
-
if (lines.length > 3) {
|
|
71
|
-
console.log(chalk.dim(` ... (${lines.length} lines)`));
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
// ── Hook simulation ─────────────────────────────────────────────────
|
|
76
|
-
async function simulateHookCatch(gate, file, message, severity, options) {
|
|
77
|
-
if (options.cinematic) {
|
|
78
|
-
await pause(300, options);
|
|
79
|
-
}
|
|
80
|
-
const sevColor = severity === 'critical' ? chalk.red.bold
|
|
81
|
-
: severity === 'high' ? chalk.red
|
|
82
|
-
: chalk.yellow;
|
|
83
|
-
const hookPrefix = chalk.magenta.bold('[rigour/hook]');
|
|
84
|
-
const sevLabel = sevColor(severity.toUpperCase());
|
|
85
|
-
const gateLabel = chalk.red(`[${gate}]`);
|
|
86
|
-
console.log(` ${hookPrefix} ${sevLabel} ${gateLabel} ${chalk.white(file)}`);
|
|
87
|
-
console.log(` ${chalk.dim('→')} ${message}`);
|
|
88
|
-
if (options.cinematic) {
|
|
89
|
-
await pause(400, options);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
// ── ASCII score bar ─────────────────────────────────────────────────
|
|
93
|
-
function renderScoreBar(score, label, width = 30) {
|
|
94
|
-
const filled = Math.round((score / 100) * width);
|
|
95
|
-
const empty = width - filled;
|
|
96
|
-
const color = score >= 80 ? chalk.green : score >= 50 ? chalk.yellow : chalk.red;
|
|
97
|
-
const bar = color('█'.repeat(filled)) + chalk.dim('░'.repeat(empty));
|
|
98
|
-
return ` ${label.padEnd(14)} ${bar} ${color.bold(`${score}/100`)}`;
|
|
99
|
-
}
|
|
100
|
-
// ── ASCII trend chart ───────────────────────────────────────────────
|
|
101
|
-
function renderTrendChart(scores) {
|
|
102
|
-
const height = 8;
|
|
103
|
-
const lines = [];
|
|
104
|
-
const maxScore = 100;
|
|
105
|
-
lines.push(chalk.dim(' Score Trend:'));
|
|
106
|
-
for (let row = height; row >= 0; row--) {
|
|
107
|
-
const threshold = (row / height) * maxScore;
|
|
108
|
-
let line = chalk.dim(String(Math.round(threshold)).padStart(3) + ' │');
|
|
109
|
-
for (const score of scores) {
|
|
110
|
-
if (score >= threshold) {
|
|
111
|
-
const color = score >= 80 ? chalk.green : score >= 50 ? chalk.yellow : chalk.red;
|
|
112
|
-
line += color(' ██');
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
line += ' ';
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
lines.push(line);
|
|
119
|
-
}
|
|
120
|
-
lines.push(chalk.dim(' └' + '───'.repeat(scores.length)));
|
|
121
|
-
const labels = scores.map((_, i) => ` R${i + 1}`);
|
|
122
|
-
lines.push(chalk.dim(' ' + labels.join('')));
|
|
123
|
-
return lines.join('\n');
|
|
124
|
-
}
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { pause, typewrite } from './demo-helpers.js';
|
|
13
|
+
import { printBanner, printPlantedIssues, printClosing } from './demo-display.js';
|
|
14
|
+
import { runHooksDemo, runFullGates, runBeforeAfterDemo, scaffoldDemoProject } from './demo-scenarios.js';
|
|
125
15
|
// ── Main demo command ───────────────────────────────────────────────
|
|
126
16
|
export async function demoCommand(options = {}) {
|
|
127
17
|
const isCinematic = !!options.cinematic;
|
|
@@ -162,457 +52,3 @@ export async function demoCommand(options = {}) {
|
|
|
162
52
|
// 5. Closing
|
|
163
53
|
printClosing(isCinematic);
|
|
164
54
|
}
|
|
165
|
-
// ── Banner ───────────────────────────────────────────────────────────
|
|
166
|
-
function printBanner(cinematic) {
|
|
167
|
-
const banner = chalk.bold.cyan(`
|
|
168
|
-
____ _
|
|
169
|
-
/ __ \\(_)____ ___ __ __ _____
|
|
170
|
-
/ /_/ // // __ \`/ / / / / // ___/
|
|
171
|
-
/ _, _// // /_/ // /_/ / / // /
|
|
172
|
-
/_/ |_|/_/ \\__, / \\__,_/_/ /_/
|
|
173
|
-
/____/
|
|
174
|
-
`);
|
|
175
|
-
console.log(banner);
|
|
176
|
-
}
|
|
177
|
-
// ── Planted issues (non-cinematic) ──────────────────────────────────
|
|
178
|
-
function printPlantedIssues() {
|
|
179
|
-
console.log(chalk.bold.yellow('Planted issues:'));
|
|
180
|
-
console.log(chalk.dim(' 1. src/auth.ts — Hardcoded API key (security)'));
|
|
181
|
-
console.log(chalk.dim(' 2. src/api-handler.ts — Unhandled promise (AI drift)'));
|
|
182
|
-
console.log(chalk.dim(' 3. src/data-loader.ts — Hallucinated import (AI drift)'));
|
|
183
|
-
console.log(chalk.dim(' 4. src/utils.ts — TODO marker left by AI'));
|
|
184
|
-
console.log(chalk.dim(' 5. src/god-file.ts — 350+ lines (structural)'));
|
|
185
|
-
console.log('');
|
|
186
|
-
}
|
|
187
|
-
// ── Hooks demo: simulate AI agent → hook catches ────────────────────
|
|
188
|
-
async function runHooksDemo(demoDir, options) {
|
|
189
|
-
const divider = chalk.cyan('━'.repeat(50));
|
|
190
|
-
console.log(divider);
|
|
191
|
-
console.log(chalk.bold.magenta(' Simulating AI agent writing code with hooks active...\n'));
|
|
192
|
-
if (options.cinematic) {
|
|
193
|
-
await pause(600, options);
|
|
194
|
-
}
|
|
195
|
-
// Step 1: AI writes auth.ts with hardcoded key
|
|
196
|
-
await simulateAgentWrite('src/auth.ts', [
|
|
197
|
-
'import express from \'express\';',
|
|
198
|
-
'',
|
|
199
|
-
'const API_KEY = "sk-live-4f3c2b1a0987654321abcdef";',
|
|
200
|
-
'',
|
|
201
|
-
'export function authenticate(req: express.Request) {',
|
|
202
|
-
' return req.headers.authorization === API_KEY;',
|
|
203
|
-
'}',
|
|
204
|
-
], 'security-patterns', 'src/auth.ts:3', 'Possible hardcoded secret or API key', 'critical', options);
|
|
205
|
-
// Step 2: AI writes data-loader.ts with hallucinated import
|
|
206
|
-
await simulateAgentWrite('src/data-loader.ts', [
|
|
207
|
-
'import { z } from \'zod\';',
|
|
208
|
-
'import { magicParser } from \'ai-data-magic\';',
|
|
209
|
-
'',
|
|
210
|
-
'export function loadData(raw: unknown) {',
|
|
211
|
-
' return z.object({ name: z.string() }).parse(raw);',
|
|
212
|
-
'}',
|
|
213
|
-
], 'hallucinated-imports', 'src/data-loader.ts:2', 'Import \'ai-data-magic\' does not resolve to an existing package', 'high', options);
|
|
214
|
-
// Step 3: AI writes api-handler.ts with unhandled promise
|
|
215
|
-
await simulateAgentWrite('src/api-handler.ts', [
|
|
216
|
-
'export async function fetchUser(id: string) {',
|
|
217
|
-
' const res = await fetch(`/api/users/${id}`);',
|
|
218
|
-
' return res.json();',
|
|
219
|
-
'}',
|
|
220
|
-
'',
|
|
221
|
-
'export function handleRequest(req: any, res: any) {',
|
|
222
|
-
' fetchUser(req.params.id); // floating promise',
|
|
223
|
-
' res.send(\'Processing...\');',
|
|
224
|
-
'}',
|
|
225
|
-
], 'promise-safety', 'src/api-handler.ts:7', 'Unhandled promise — fetchUser() called without await or .catch()', 'medium', options);
|
|
226
|
-
console.log('');
|
|
227
|
-
console.log(chalk.magenta.bold(` Hooks caught 3 issues in real time — before the agent finished.`));
|
|
228
|
-
console.log(divider);
|
|
229
|
-
console.log('');
|
|
230
|
-
if (options.cinematic) {
|
|
231
|
-
await pause(1000, options);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
async function simulateAgentWrite(filename, codeLines, gate, file, message, severity, options) {
|
|
235
|
-
console.log(chalk.blue.bold(` Agent: Write → ${filename}`));
|
|
236
|
-
await simulateCodeWrite(filename, codeLines, options);
|
|
237
|
-
await simulateHookCatch(gate, file, message, severity, options);
|
|
238
|
-
console.log('');
|
|
239
|
-
}
|
|
240
|
-
// ── Full gate run ───────────────────────────────────────────────────
|
|
241
|
-
async function runFullGates(demoDir, options) {
|
|
242
|
-
const isCinematic = !!options.cinematic;
|
|
243
|
-
console.log(chalk.bold.blue('Running full Rigour quality gates...\n'));
|
|
244
|
-
if (isCinematic) {
|
|
245
|
-
await pause(500, options);
|
|
246
|
-
}
|
|
247
|
-
try {
|
|
248
|
-
const configContent = await fs.readFile(path.join(demoDir, 'rigour.yml'), 'utf-8');
|
|
249
|
-
const rawConfig = yaml.parse(configContent);
|
|
250
|
-
const config = ConfigSchema.parse(rawConfig);
|
|
251
|
-
const runner = new GateRunner(config);
|
|
252
|
-
const report = await runner.run(demoDir);
|
|
253
|
-
recordScore(demoDir, report);
|
|
254
|
-
const reportPath = path.join(demoDir, config.output.report_path);
|
|
255
|
-
await fs.writeJson(reportPath, report, { spaces: 2 });
|
|
256
|
-
displayGateResults(report, isCinematic);
|
|
257
|
-
await generateArtifacts(demoDir, report, config);
|
|
258
|
-
}
|
|
259
|
-
catch (error) {
|
|
260
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
261
|
-
console.error(chalk.red(`Demo error: ${msg}`));
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
function displayGateResults(report, cinematic) {
|
|
265
|
-
const stats = report.stats;
|
|
266
|
-
if (report.status === 'FAIL') {
|
|
267
|
-
console.log(chalk.red.bold('✘ FAIL — Quality gate violations found.\n'));
|
|
268
|
-
// Score bars
|
|
269
|
-
if (stats.score !== undefined) {
|
|
270
|
-
console.log(renderScoreBar(stats.score, 'Overall'));
|
|
271
|
-
}
|
|
272
|
-
if (stats.ai_health_score !== undefined) {
|
|
273
|
-
console.log(renderScoreBar(stats.ai_health_score, 'AI Health'));
|
|
274
|
-
}
|
|
275
|
-
if (stats.structural_score !== undefined) {
|
|
276
|
-
console.log(renderScoreBar(stats.structural_score, 'Structural'));
|
|
277
|
-
}
|
|
278
|
-
console.log('');
|
|
279
|
-
// Severity breakdown
|
|
280
|
-
printSeverityBreakdown(stats);
|
|
281
|
-
// Violations list
|
|
282
|
-
for (const failure of report.failures) {
|
|
283
|
-
printFailure(failure);
|
|
284
|
-
}
|
|
285
|
-
console.log('');
|
|
286
|
-
}
|
|
287
|
-
else {
|
|
288
|
-
console.log(chalk.green.bold('✔ PASS — All quality gates satisfied.\n'));
|
|
289
|
-
}
|
|
290
|
-
console.log(chalk.dim(`Finished in ${stats.duration_ms}ms\n`));
|
|
291
|
-
}
|
|
292
|
-
function printSeverityBreakdown(stats) {
|
|
293
|
-
if (!stats.severity_breakdown) {
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
const parts = Object.entries(stats.severity_breakdown)
|
|
297
|
-
.filter(([, count]) => count > 0)
|
|
298
|
-
.map(([sev, count]) => {
|
|
299
|
-
const color = sev === 'critical' ? chalk.red.bold
|
|
300
|
-
: sev === 'high' ? chalk.red
|
|
301
|
-
: sev === 'medium' ? chalk.yellow
|
|
302
|
-
: chalk.dim;
|
|
303
|
-
return color(`${sev}: ${count}`);
|
|
304
|
-
});
|
|
305
|
-
if (parts.length > 0) {
|
|
306
|
-
console.log('Severity: ' + parts.join(', ') + '\n');
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
function printFailure(failure) {
|
|
310
|
-
const sevLabel = failure.severity === 'critical' ? chalk.red.bold('CRIT')
|
|
311
|
-
: failure.severity === 'high' ? chalk.red('HIGH')
|
|
312
|
-
: failure.severity === 'medium' ? chalk.yellow('MED ')
|
|
313
|
-
: chalk.dim('LOW ');
|
|
314
|
-
const prov = failure.provenance ? chalk.dim(`[${failure.provenance}]`) : '';
|
|
315
|
-
console.log(` ${sevLabel} ${prov} ${chalk.red(`[${failure.id}]`)} ${failure.title}`);
|
|
316
|
-
if (failure.hint) {
|
|
317
|
-
console.log(chalk.cyan(` ${failure.hint}`));
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
async function generateArtifacts(demoDir, report, config) {
|
|
321
|
-
if (report.status === 'FAIL') {
|
|
322
|
-
const { FixPacketService } = await import('@rigour-labs/core');
|
|
323
|
-
const fixPacketService = new FixPacketService();
|
|
324
|
-
const fixPacket = fixPacketService.generate(report, config);
|
|
325
|
-
const fixPacketPath = path.join(demoDir, 'rigour-fix-packet.json');
|
|
326
|
-
await fs.writeJson(fixPacketPath, fixPacket, { spaces: 2 });
|
|
327
|
-
console.log(chalk.green('✓ Fix packet generated: rigour-fix-packet.json'));
|
|
328
|
-
}
|
|
329
|
-
const auditPath = path.join(demoDir, 'rigour-audit-report.md');
|
|
330
|
-
await generateDemoAudit(demoDir, report, auditPath);
|
|
331
|
-
console.log(chalk.green('✓ Audit report exported: rigour-audit-report.md'));
|
|
332
|
-
console.log('');
|
|
333
|
-
}
|
|
334
|
-
// ── Before/After improvement demo ───────────────────────────────────
|
|
335
|
-
async function runBeforeAfterDemo(demoDir, options) {
|
|
336
|
-
console.log(chalk.bold.green('Simulating agent fixing issues...\n'));
|
|
337
|
-
await pause(600, options);
|
|
338
|
-
// Fix the auth.ts — remove hardcoded key
|
|
339
|
-
await typewrite(chalk.dim(' Agent: Removing hardcoded API key from src/auth.ts...'), options);
|
|
340
|
-
await fs.writeFile(path.join(demoDir, 'src', 'auth.ts'), `
|
|
341
|
-
import express from 'express';
|
|
342
|
-
|
|
343
|
-
export function authenticate(req: express.Request) {
|
|
344
|
-
const token = req.headers.authorization;
|
|
345
|
-
if (!token) {
|
|
346
|
-
return { authenticated: false };
|
|
347
|
-
}
|
|
348
|
-
// Validate against secure key store
|
|
349
|
-
return { authenticated: validateToken(token) };
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function validateToken(token: string): boolean {
|
|
353
|
-
return token.startsWith('Bearer ') && token.length > 20;
|
|
354
|
-
}
|
|
355
|
-
`.trim());
|
|
356
|
-
console.log(chalk.green(' ✓ Fixed: API key moved to environment variable'));
|
|
357
|
-
await pause(300, options);
|
|
358
|
-
// Fix data-loader.ts — remove hallucinated import
|
|
359
|
-
await typewrite(chalk.dim(' Agent: Removing hallucinated import from src/data-loader.ts...'), options);
|
|
360
|
-
await fs.writeFile(path.join(demoDir, 'src', 'data-loader.ts'), `
|
|
361
|
-
import { z } from 'zod';
|
|
362
|
-
|
|
363
|
-
const schema = z.object({
|
|
364
|
-
name: z.string(),
|
|
365
|
-
email: z.string().email(),
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
export function loadData(raw: unknown) {
|
|
369
|
-
return schema.parse(raw);
|
|
370
|
-
}
|
|
371
|
-
`.trim());
|
|
372
|
-
console.log(chalk.green(' ✓ Fixed: Removed non-existent package imports'));
|
|
373
|
-
await pause(300, options);
|
|
374
|
-
// Fix api-handler.ts — add error handling
|
|
375
|
-
await typewrite(chalk.dim(' Agent: Adding error handling to src/api-handler.ts...'), options);
|
|
376
|
-
await fs.writeFile(path.join(demoDir, 'src', 'api-handler.ts'), `
|
|
377
|
-
import express from 'express';
|
|
378
|
-
|
|
379
|
-
export async function fetchUserData(userId: string) {
|
|
380
|
-
const response = await fetch(\`https://api.example.com/users/\${userId}\`);
|
|
381
|
-
if (!response.ok) {
|
|
382
|
-
throw new Error(\`Failed to fetch user: \${response.status}\`);
|
|
383
|
-
}
|
|
384
|
-
return response.json();
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
export async function handleRequest(req: express.Request, res: express.Response) {
|
|
388
|
-
try {
|
|
389
|
-
const data = await fetchUserData(req.params.id);
|
|
390
|
-
res.json(data);
|
|
391
|
-
} catch (error) {
|
|
392
|
-
res.status(500).json({ error: 'Failed to fetch user data' });
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
`.trim());
|
|
396
|
-
console.log(chalk.green(' ✓ Fixed: Added proper await and error handling'));
|
|
397
|
-
console.log('');
|
|
398
|
-
await pause(500, options);
|
|
399
|
-
// Re-run gates to show improvement
|
|
400
|
-
console.log(chalk.bold.blue('Re-running quality gates after fixes...\n'));
|
|
401
|
-
await pause(400, options);
|
|
402
|
-
try {
|
|
403
|
-
const configContent = await fs.readFile(path.join(demoDir, 'rigour.yml'), 'utf-8');
|
|
404
|
-
const rawConfig = yaml.parse(configContent);
|
|
405
|
-
const config = ConfigSchema.parse(rawConfig);
|
|
406
|
-
const runner = new GateRunner(config);
|
|
407
|
-
const report2 = await runner.run(demoDir);
|
|
408
|
-
recordScore(demoDir, report2);
|
|
409
|
-
const score1 = 35; // approximate first-run score
|
|
410
|
-
const score2 = report2.stats.score ?? 75;
|
|
411
|
-
const remaining = report2.failures.length;
|
|
412
|
-
console.log(chalk.bold('Score improvement:\n'));
|
|
413
|
-
console.log(renderScoreBar(score1, 'Before'));
|
|
414
|
-
console.log(renderScoreBar(score2, 'After'));
|
|
415
|
-
console.log('');
|
|
416
|
-
// Trend chart
|
|
417
|
-
console.log(renderTrendChart([score1, score2]));
|
|
418
|
-
console.log('');
|
|
419
|
-
if (remaining > 0) {
|
|
420
|
-
console.log(chalk.yellow(` ${remaining} issue(s) remaining (structural, TODOs)`));
|
|
421
|
-
}
|
|
422
|
-
else {
|
|
423
|
-
console.log(chalk.green.bold(' All issues resolved!'));
|
|
424
|
-
}
|
|
425
|
-
console.log('');
|
|
426
|
-
}
|
|
427
|
-
catch (error) {
|
|
428
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
429
|
-
console.error(chalk.red(`Re-check error: ${msg}`));
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
// ── Closing section ─────────────────────────────────────────────────
|
|
433
|
-
function printClosing(cinematic) {
|
|
434
|
-
const divider = chalk.bold.cyan('━'.repeat(50));
|
|
435
|
-
console.log(divider);
|
|
436
|
-
console.log(chalk.bold('What Rigour does:'));
|
|
437
|
-
console.log(chalk.dim(' Catches AI drift (hallucinated imports, unhandled promises)'));
|
|
438
|
-
console.log(chalk.dim(' Blocks security issues (hardcoded keys, injection patterns)'));
|
|
439
|
-
console.log(chalk.dim(' Enforces structure (file size, complexity, documentation)'));
|
|
440
|
-
console.log(chalk.dim(' Generates audit-ready evidence (scores, trends, reports)'));
|
|
441
|
-
console.log(chalk.dim(' Real-time hooks for Claude, Cursor, Cline, Windsurf'));
|
|
442
|
-
console.log(divider);
|
|
443
|
-
console.log('');
|
|
444
|
-
if (cinematic) {
|
|
445
|
-
console.log(chalk.bold('Peer-reviewed research:'));
|
|
446
|
-
console.log(chalk.white(' Deterministic Quality Gates for AI-Generated Code'));
|
|
447
|
-
console.log(chalk.dim(' https://zenodo.org/records/18673564'));
|
|
448
|
-
console.log('');
|
|
449
|
-
}
|
|
450
|
-
console.log(chalk.bold('Get started:'));
|
|
451
|
-
console.log(chalk.white(' $ npx @rigour-labs/cli init'));
|
|
452
|
-
console.log(chalk.white(' $ npx @rigour-labs/cli check'));
|
|
453
|
-
console.log(chalk.white(' $ npx @rigour-labs/cli hooks init'));
|
|
454
|
-
console.log('');
|
|
455
|
-
console.log(chalk.dim('GitHub: https://github.com/rigour-labs/rigour'));
|
|
456
|
-
console.log(chalk.dim('Docs: https://docs.rigour.run'));
|
|
457
|
-
console.log(chalk.dim('Paper: https://zenodo.org/records/18673564\n'));
|
|
458
|
-
console.log(chalk.dim.italic('If this saved you from a bad commit, star the repo ⭐'));
|
|
459
|
-
}
|
|
460
|
-
// ── Scaffold demo project ───────────────────────────────────────────
|
|
461
|
-
async function scaffoldDemoProject(dir) {
|
|
462
|
-
const config = buildDemoConfig();
|
|
463
|
-
await fs.writeFile(path.join(dir, 'rigour.yml'), yaml.stringify(config));
|
|
464
|
-
await fs.writeJson(path.join(dir, 'package.json'), buildDemoPackageJson(), { spaces: 2 });
|
|
465
|
-
await fs.ensureDir(path.join(dir, 'src'));
|
|
466
|
-
await fs.ensureDir(path.join(dir, 'docs'));
|
|
467
|
-
await writeIssueFiles(dir);
|
|
468
|
-
await writeGodFile(dir);
|
|
469
|
-
await fs.writeFile(path.join(dir, 'README.md'), '# Demo Project\n\nThis is a demo project for Rigour.\n');
|
|
470
|
-
}
|
|
471
|
-
function buildDemoConfig() {
|
|
472
|
-
return {
|
|
473
|
-
version: 1,
|
|
474
|
-
preset: 'api',
|
|
475
|
-
gates: {
|
|
476
|
-
max_file_lines: 300,
|
|
477
|
-
forbid_todos: true,
|
|
478
|
-
forbid_fixme: true,
|
|
479
|
-
ast: { complexity: 10, max_params: 5 },
|
|
480
|
-
security: { enabled: true, block_on_severity: 'high' },
|
|
481
|
-
hallucinated_imports: { enabled: true, severity: 'critical' },
|
|
482
|
-
promise_safety: { enabled: true, severity: 'high' },
|
|
483
|
-
},
|
|
484
|
-
hooks: { enabled: true, tools: ['claude'] },
|
|
485
|
-
ignore: ['.git/**', 'node_modules/**'],
|
|
486
|
-
output: { report_path: 'rigour-report.json' },
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
function buildDemoPackageJson() {
|
|
490
|
-
return {
|
|
491
|
-
name: 'rigour-demo',
|
|
492
|
-
version: '1.0.0',
|
|
493
|
-
dependencies: { express: '^4.18.0', zod: '^3.22.0' },
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
async function writeIssueFiles(dir) {
|
|
497
|
-
// Issue 1: Hardcoded API key
|
|
498
|
-
await fs.writeFile(path.join(dir, 'src', 'auth.ts'), `
|
|
499
|
-
import express from 'express';
|
|
500
|
-
|
|
501
|
-
const API_KEY = "sk-live-4f3c2b1a0987654321abcdef";
|
|
502
|
-
const DB_PASSWORD = "super_secret_p@ssw0rd!";
|
|
503
|
-
|
|
504
|
-
export function authenticate(req: express.Request) {
|
|
505
|
-
const token = req.headers.authorization;
|
|
506
|
-
if (token === API_KEY) {
|
|
507
|
-
return { authenticated: true };
|
|
508
|
-
}
|
|
509
|
-
return { authenticated: false };
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
export function connectDatabase() {
|
|
513
|
-
return { host: 'prod-db.internal', password: DB_PASSWORD };
|
|
514
|
-
}
|
|
515
|
-
`.trim());
|
|
516
|
-
// Issue 2: Unhandled promise
|
|
517
|
-
await fs.writeFile(path.join(dir, 'src', 'api-handler.ts'), `
|
|
518
|
-
import express from 'express';
|
|
519
|
-
|
|
520
|
-
export async function fetchUserData(userId: string) {
|
|
521
|
-
const response = await fetch(\`https://api.example.com/users/\${userId}\`);
|
|
522
|
-
return response.json();
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
export function handleRequest(req: express.Request, res: express.Response) {
|
|
526
|
-
fetchUserData(req.params.id);
|
|
527
|
-
res.send('Processing...');
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
export function batchProcess(ids: string[]) {
|
|
531
|
-
ids.forEach(id => fetchUserData(id));
|
|
532
|
-
}
|
|
533
|
-
`.trim());
|
|
534
|
-
// Issue 3: Hallucinated import
|
|
535
|
-
await fs.writeFile(path.join(dir, 'src', 'data-loader.ts'), `
|
|
536
|
-
import { z } from 'zod';
|
|
537
|
-
import { magicParser } from 'ai-data-magic';
|
|
538
|
-
import { ultraCache } from 'quantum-cache-pro';
|
|
539
|
-
|
|
540
|
-
const schema = z.object({
|
|
541
|
-
name: z.string(),
|
|
542
|
-
email: z.string().email(),
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
export function loadData(raw: unknown) {
|
|
546
|
-
const parsed = schema.parse(raw);
|
|
547
|
-
return parsed;
|
|
548
|
-
}
|
|
549
|
-
`.trim());
|
|
550
|
-
// Issue 4: TODO markers
|
|
551
|
-
await fs.writeFile(path.join(dir, 'src', 'utils.ts'), `
|
|
552
|
-
// TODO: Claude suggested this but I need to review
|
|
553
|
-
// FIXME: This function has edge cases
|
|
554
|
-
export function formatDate(date: Date): string {
|
|
555
|
-
return date.toISOString().split('T')[0];
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
export function sanitizeInput(input: string): string {
|
|
559
|
-
// TODO: Add proper sanitization
|
|
560
|
-
return input.trim();
|
|
561
|
-
}
|
|
562
|
-
`.trim());
|
|
563
|
-
}
|
|
564
|
-
async function writeGodFile(dir) {
|
|
565
|
-
const lines = [
|
|
566
|
-
'// Auto-generated data processing module',
|
|
567
|
-
'export class DataProcessor {',
|
|
568
|
-
];
|
|
569
|
-
for (let i = 0; i < 60; i++) {
|
|
570
|
-
lines.push(` process${i}(data: any) {`);
|
|
571
|
-
lines.push(` const result = data.map((x: any) => x * ${i + 1});`);
|
|
572
|
-
lines.push(` if (result.length > ${i * 10}) {`);
|
|
573
|
-
lines.push(` return result.slice(0, ${i * 10});`);
|
|
574
|
-
lines.push(` }`);
|
|
575
|
-
lines.push(` return result;`);
|
|
576
|
-
lines.push(` }`);
|
|
577
|
-
}
|
|
578
|
-
lines.push('}');
|
|
579
|
-
await fs.writeFile(path.join(dir, 'src', 'god-file.ts'), lines.join('\n'));
|
|
580
|
-
}
|
|
581
|
-
// ── Audit report generator ──────────────────────────────────────────
|
|
582
|
-
async function generateDemoAudit(dir, report, outputPath) {
|
|
583
|
-
const stats = report.stats || {};
|
|
584
|
-
const failures = report.failures || [];
|
|
585
|
-
const lines = [];
|
|
586
|
-
lines.push('# Rigour Audit Report — Demo');
|
|
587
|
-
lines.push('');
|
|
588
|
-
lines.push(`**Generated:** ${new Date().toISOString()}`);
|
|
589
|
-
lines.push(`**Status:** ${report.status}`);
|
|
590
|
-
lines.push(`**Score:** ${stats.score ?? 100}/100`);
|
|
591
|
-
if (stats.ai_health_score !== undefined) {
|
|
592
|
-
lines.push(`**AI Health:** ${stats.ai_health_score}/100`);
|
|
593
|
-
}
|
|
594
|
-
if (stats.structural_score !== undefined) {
|
|
595
|
-
lines.push(`**Structural:** ${stats.structural_score}/100`);
|
|
596
|
-
}
|
|
597
|
-
lines.push('');
|
|
598
|
-
lines.push('## Violations');
|
|
599
|
-
lines.push('');
|
|
600
|
-
for (let i = 0; i < failures.length; i++) {
|
|
601
|
-
const f = failures[i];
|
|
602
|
-
lines.push(`### ${i + 1}. [${(f.severity || 'medium').toUpperCase()}] ${f.title}`);
|
|
603
|
-
lines.push(`- **ID:** \`${f.id}\``);
|
|
604
|
-
lines.push(`- **Provenance:** ${f.provenance || 'traditional'}`);
|
|
605
|
-
lines.push(`- **Details:** ${f.details}`);
|
|
606
|
-
if (f.files?.length) {
|
|
607
|
-
lines.push(`- **Files:** ${f.files.join(', ')}`);
|
|
608
|
-
}
|
|
609
|
-
if (f.hint) {
|
|
610
|
-
lines.push(`- **Hint:** ${f.hint}`);
|
|
611
|
-
}
|
|
612
|
-
lines.push('');
|
|
613
|
-
}
|
|
614
|
-
lines.push('---');
|
|
615
|
-
lines.push('*Generated by Rigour — https://rigour.run*');
|
|
616
|
-
lines.push('*Research: https://zenodo.org/records/18673564*');
|
|
617
|
-
await fs.writeFile(outputPath, lines.join('\n'));
|
|
618
|
-
}
|
package/dist/commands/scan.js
CHANGED
|
@@ -157,12 +157,34 @@ function renderScanHeader(scanCtx, stackSignals) {
|
|
|
157
157
|
}
|
|
158
158
|
function renderScanResults(report, stackSignals, reportPath, cwd) {
|
|
159
159
|
const fakePackages = extractHallucinatedImports(report.failures);
|
|
160
|
+
const criticalSecrets = report.failures.filter(f => f.id === 'security-patterns' && f.severity === 'critical');
|
|
161
|
+
const phantomApis = report.failures.filter(f => f.id === 'phantom-apis');
|
|
162
|
+
const ignoredErrors = report.failures.filter(f => f.id === 'promise-safety' && (f.severity === 'high' || f.severity === 'critical'));
|
|
163
|
+
// --- Scary headlines for the worst findings ---
|
|
164
|
+
let scaryHeadlines = 0;
|
|
165
|
+
if (criticalSecrets.length > 0) {
|
|
166
|
+
console.log(chalk.red.bold(`🔑 HARDCODED SECRETS: ${criticalSecrets.length} credential(s) exposed in plain text`));
|
|
167
|
+
const firstFile = criticalSecrets[0].files?.[0];
|
|
168
|
+
if (firstFile)
|
|
169
|
+
console.log(chalk.dim(` First hit: ${firstFile}`));
|
|
170
|
+
scaryHeadlines++;
|
|
171
|
+
}
|
|
160
172
|
if (fakePackages.length > 0) {
|
|
161
173
|
const unique = [...new Set(fakePackages)];
|
|
162
|
-
console.log(chalk.red.bold(
|
|
163
|
-
console.log(chalk.dim(`Examples: ${unique.slice(0,
|
|
164
|
-
|
|
174
|
+
console.log(chalk.red.bold(`📦 HALLUCINATED PACKAGES: ${unique.length} import(s) don't exist — will crash at runtime`));
|
|
175
|
+
console.log(chalk.dim(` Examples: ${unique.slice(0, 4).join(', ')}${unique.length > 4 ? `, +${unique.length - 4} more` : ''}`));
|
|
176
|
+
scaryHeadlines++;
|
|
177
|
+
}
|
|
178
|
+
if (phantomApis.length > 0) {
|
|
179
|
+
console.log(chalk.red.bold(`👻 PHANTOM APIs: ${phantomApis.length} call(s) to methods that don't exist in stdlib`));
|
|
180
|
+
scaryHeadlines++;
|
|
165
181
|
}
|
|
182
|
+
if (ignoredErrors.length > 0) {
|
|
183
|
+
console.log(chalk.yellow.bold(`🔇 SILENT FAILURES: ${ignoredErrors.length} async error(s) swallowed — failures will vanish without a trace`));
|
|
184
|
+
scaryHeadlines++;
|
|
185
|
+
}
|
|
186
|
+
if (scaryHeadlines > 0)
|
|
187
|
+
console.log('');
|
|
166
188
|
const statusColor = report.status === 'PASS' ? chalk.green.bold : chalk.red.bold;
|
|
167
189
|
const statusLabel = report.status === 'PASS' ? 'PASS' : 'FAIL';
|
|
168
190
|
const score = report.stats.score ?? 0;
|
|
@@ -179,27 +201,48 @@ function renderScanResults(report, stackSignals, reportPath, cwd) {
|
|
|
179
201
|
renderCoverageWarnings(stackSignals);
|
|
180
202
|
console.log('');
|
|
181
203
|
if (report.status === 'FAIL') {
|
|
182
|
-
|
|
204
|
+
// Sort by severity so critical findings appear first
|
|
205
|
+
const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
206
|
+
const sorted = [...report.failures].sort((a, b) => (SEVERITY_ORDER[a.severity ?? 'medium'] ?? 2) - (SEVERITY_ORDER[b.severity ?? 'medium'] ?? 2));
|
|
207
|
+
const topFindings = sorted.slice(0, 8);
|
|
183
208
|
for (const failure of topFindings) {
|
|
184
|
-
const sev =
|
|
185
|
-
|
|
209
|
+
const sev = failure.severity ?? 'medium';
|
|
210
|
+
const sevColor = sev === 'critical' ? chalk.red.bold
|
|
211
|
+
: sev === 'high' ? chalk.yellow.bold
|
|
212
|
+
: sev === 'medium' ? chalk.white
|
|
213
|
+
: chalk.dim;
|
|
214
|
+
console.log(sevColor(`${sev.toUpperCase().padEnd(8)} [${failure.id}] ${failure.title}`));
|
|
186
215
|
if (failure.files && failure.files.length > 0) {
|
|
187
|
-
console.log(chalk.dim(`
|
|
216
|
+
console.log(chalk.dim(` ${failure.files.slice(0, 2).join(', ')}`));
|
|
188
217
|
}
|
|
189
218
|
}
|
|
190
219
|
if (report.failures.length > topFindings.length) {
|
|
191
|
-
console.log(chalk.dim(
|
|
220
|
+
console.log(chalk.dim(`\n...and ${report.failures.length - topFindings.length} more. See full report.`));
|
|
192
221
|
}
|
|
193
222
|
}
|
|
194
223
|
const trend = getScoreTrend(cwd);
|
|
195
224
|
if (trend && trend.recentScores.length >= 3) {
|
|
196
|
-
|
|
225
|
+
const arrow = trend.direction === 'improving' ? '↑' : trend.direction === 'degrading' ? '↓' : '→';
|
|
226
|
+
const color = trend.direction === 'improving' ? chalk.green : trend.direction === 'degrading' ? chalk.red : chalk.dim;
|
|
227
|
+
console.log(color(`\nTrend: ${trend.recentScores.join(' → ')} ${arrow}`));
|
|
197
228
|
}
|
|
198
229
|
console.log(chalk.yellow(`\nFull report: ${reportPath}`));
|
|
199
230
|
if (report.status === 'FAIL') {
|
|
200
|
-
console.log(chalk.yellow('Fix packet:
|
|
231
|
+
console.log(chalk.yellow('Fix packet: rigour-fix-packet.json'));
|
|
201
232
|
}
|
|
202
233
|
console.log(chalk.dim(`Finished in ${report.stats.duration_ms}ms`));
|
|
234
|
+
// --- Next steps ---
|
|
235
|
+
console.log('');
|
|
236
|
+
if (report.status === 'FAIL') {
|
|
237
|
+
console.log(chalk.bold('Next steps:'));
|
|
238
|
+
console.log(` ${chalk.cyan('rigour explain')} — get plain-English fix suggestions`);
|
|
239
|
+
console.log(` ${chalk.cyan('rigour init')} — add quality gates to your project (blocks AI from repeating this)`);
|
|
240
|
+
console.log(` ${chalk.cyan('rigour check --ci')} — enforce in CI/CD pipeline`);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
console.log(chalk.green.bold('✓ This repo is clean. Add it to CI to keep it that way:'));
|
|
244
|
+
console.log(` ${chalk.cyan('rigour init')} — write quality gates to rigour.yml + CI config`);
|
|
245
|
+
}
|
|
203
246
|
}
|
|
204
247
|
function renderCoverageWarnings(stackSignals) {
|
|
205
248
|
const gaps = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/cli",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.6",
|
|
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": "3.0.
|
|
47
|
+
"@rigour-labs/core": "3.0.6"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/fs-extra": "^11.0.4",
|