@rigour-labs/cli 3.0.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +34 -2
- package/dist/commands/demo.d.ts +15 -8
- package/dist/commands/demo.js +470 -158
- package/dist/commands/demo.test.d.ts +1 -0
- package/dist/commands/demo.test.js +59 -0
- package/dist/commands/hooks.d.ts +22 -0
- package/dist/commands/hooks.js +274 -0
- package/dist/commands/hooks.test.d.ts +1 -0
- package/dist/commands/hooks.test.js +77 -0
- package/dist/commands/init.js +25 -1
- package/dist/commands/init.test.d.ts +1 -0
- package/dist/commands/init.test.js +97 -0
- package/package.json +2 -2
package/dist/commands/demo.js
CHANGED
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
* rigour demo
|
|
3
3
|
*
|
|
4
4
|
* Creates a temp project with intentional AI-generated code issues,
|
|
5
|
-
* runs Rigour against it, and shows the full experience
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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)
|
|
11
13
|
*
|
|
12
14
|
* The "flagship demo" — one command to understand Rigour.
|
|
13
15
|
*
|
|
14
|
-
* @since v2.17.0
|
|
16
|
+
* @since v2.17.0 (extended v3.0.0)
|
|
15
17
|
*/
|
|
16
18
|
import fs from 'fs-extra';
|
|
17
19
|
import path from 'path';
|
|
@@ -20,168 +22,479 @@ import yaml from 'yaml';
|
|
|
20
22
|
import os from 'os';
|
|
21
23
|
import { GateRunner, ConfigSchema } from '@rigour-labs/core';
|
|
22
24
|
import { recordScore } from '@rigour-labs/core';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
const SPEED_MULTIPLIERS = {
|
|
26
|
+
fast: 0.3,
|
|
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
|
+
}
|
|
125
|
+
// ── Main demo command ───────────────────────────────────────────────
|
|
126
|
+
export async function demoCommand(options = {}) {
|
|
127
|
+
const isCinematic = !!options.cinematic;
|
|
128
|
+
const showHooks = !!options.hooks || isCinematic;
|
|
129
|
+
printBanner(isCinematic);
|
|
130
|
+
if (isCinematic) {
|
|
131
|
+
await typewrite(chalk.bold('Rigour Demo — Watch AI code governance in real time.\n'), options);
|
|
132
|
+
await pause(800, options);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
console.log(chalk.bold('Rigour Demo — See AI code governance in action.\n'));
|
|
136
|
+
}
|
|
137
|
+
// 1. Create temp project
|
|
138
|
+
const demoDir = path.join(os.tmpdir(), `rigour-demo-${Date.now()}`);
|
|
139
|
+
await fs.ensureDir(demoDir);
|
|
140
|
+
if (isCinematic) {
|
|
141
|
+
await typewrite(chalk.dim(`Setting up demo project...`), options);
|
|
142
|
+
await pause(400, options);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
console.log(chalk.dim(`Creating demo project at ${demoDir}...\n`));
|
|
146
|
+
}
|
|
147
|
+
await scaffoldDemoProject(demoDir);
|
|
148
|
+
console.log(chalk.green('✓ Demo project scaffolded.\n'));
|
|
149
|
+
// 2. Simulate AI agent writing flawed code (cinematic/hooks mode)
|
|
150
|
+
if (showHooks) {
|
|
151
|
+
await runHooksDemo(demoDir, options);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
printPlantedIssues();
|
|
155
|
+
}
|
|
156
|
+
// 3. Run full quality gates
|
|
157
|
+
await runFullGates(demoDir, options);
|
|
158
|
+
// 4. Show "after fix" improvement (cinematic only)
|
|
159
|
+
if (isCinematic) {
|
|
160
|
+
await runBeforeAfterDemo(demoDir, options);
|
|
161
|
+
}
|
|
162
|
+
// 5. Closing
|
|
163
|
+
printClosing(isCinematic);
|
|
164
|
+
}
|
|
165
|
+
// ── Banner ───────────────────────────────────────────────────────────
|
|
166
|
+
function printBanner(cinematic) {
|
|
167
|
+
const banner = chalk.bold.cyan(`
|
|
25
168
|
____ _
|
|
26
169
|
/ __ \\(_)____ ___ __ __ _____
|
|
27
170
|
/ /_/ // // __ \`/ / / / / // ___/
|
|
28
171
|
/ _, _// // /_/ // /_/ / / // /
|
|
29
172
|
/_/ |_|/_/ \\__, / \\__,_/_/ /_/
|
|
30
173
|
/____/
|
|
31
|
-
`)
|
|
32
|
-
console.log(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
console.log(chalk.green('✓ Demo project scaffolded with intentional issues.\n'));
|
|
39
|
-
// Show the issues planted
|
|
40
|
-
console.log(chalk.bold.yellow('📋 Planted issues:'));
|
|
41
|
-
console.log(chalk.dim(' 1. src/auth.ts — Hardcoded API key (security)'));
|
|
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)'));
|
|
42
181
|
console.log(chalk.dim(' 2. src/api-handler.ts — Unhandled promise (AI drift)'));
|
|
43
|
-
console.log(chalk.dim(' 3. src/data-loader.ts
|
|
44
|
-
console.log(chalk.dim(' 4. src/utils.ts
|
|
45
|
-
console.log(chalk.dim(' 5. src/god-file.ts
|
|
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);
|
|
46
226
|
console.log('');
|
|
47
|
-
|
|
48
|
-
console.log(
|
|
49
|
-
|
|
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
|
+
}
|
|
50
247
|
try {
|
|
51
248
|
const configContent = await fs.readFile(path.join(demoDir, 'rigour.yml'), 'utf-8');
|
|
52
249
|
const rawConfig = yaml.parse(configContent);
|
|
53
250
|
const config = ConfigSchema.parse(rawConfig);
|
|
54
251
|
const runner = new GateRunner(config);
|
|
55
252
|
const report = await runner.run(demoDir);
|
|
56
|
-
// Record score
|
|
57
253
|
recordScore(demoDir, report);
|
|
58
|
-
// Write report
|
|
59
254
|
const reportPath = path.join(demoDir, config.output.report_path);
|
|
60
255
|
await fs.writeJson(reportPath, report, { spaces: 2 });
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// Severity breakdown
|
|
77
|
-
if (stats.severity_breakdown) {
|
|
78
|
-
const parts = Object.entries(stats.severity_breakdown)
|
|
79
|
-
.filter(([, count]) => count > 0)
|
|
80
|
-
.map(([sev, count]) => {
|
|
81
|
-
const color = sev === 'critical' ? chalk.red.bold : sev === 'high' ? chalk.red : sev === 'medium' ? chalk.yellow : chalk.dim;
|
|
82
|
-
return color(`${sev}: ${count}`);
|
|
83
|
-
});
|
|
84
|
-
if (parts.length > 0) {
|
|
85
|
-
console.log('Severity: ' + parts.join(', ') + '\n');
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
// Show violations
|
|
89
|
-
for (const failure of report.failures) {
|
|
90
|
-
const sevLabel = failure.severity === 'critical' ? chalk.red.bold('CRIT') :
|
|
91
|
-
failure.severity === 'high' ? chalk.red('HIGH') :
|
|
92
|
-
failure.severity === 'medium' ? chalk.yellow('MED ') : chalk.dim('LOW ');
|
|
93
|
-
const prov = failure.provenance ? chalk.dim(`[${failure.provenance}]`) : '';
|
|
94
|
-
console.log(` ${sevLabel} ${prov} ${chalk.red(`[${failure.id}]`)} ${failure.title}`);
|
|
95
|
-
if (failure.hint) {
|
|
96
|
-
console.log(chalk.cyan(` 💡 ${failure.hint}`));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
console.log('');
|
|
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'));
|
|
100
271
|
}
|
|
101
|
-
|
|
102
|
-
console.log(
|
|
272
|
+
if (stats.ai_health_score !== undefined) {
|
|
273
|
+
console.log(renderScoreBar(stats.ai_health_score, 'AI Health'));
|
|
103
274
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (report.status === 'FAIL') {
|
|
107
|
-
const { FixPacketService } = await import('@rigour-labs/core');
|
|
108
|
-
const fixPacketService = new FixPacketService();
|
|
109
|
-
const fixPacket = fixPacketService.generate(report, config);
|
|
110
|
-
const fixPacketPath = path.join(demoDir, 'rigour-fix-packet.json');
|
|
111
|
-
await fs.writeJson(fixPacketPath, fixPacket, { spaces: 2 });
|
|
112
|
-
console.log(chalk.green(`✓ Fix packet generated: rigour-fix-packet.json`));
|
|
275
|
+
if (stats.structural_score !== undefined) {
|
|
276
|
+
console.log(renderScoreBar(stats.structural_score, 'Structural'));
|
|
113
277
|
}
|
|
114
|
-
// 4. Generate audit report
|
|
115
|
-
const { exportAuditCommand } = await import('./export-audit.js');
|
|
116
|
-
// We manually build a mini audit export
|
|
117
|
-
const auditPath = path.join(demoDir, 'rigour-audit-report.md');
|
|
118
|
-
await generateDemoAudit(demoDir, report, auditPath);
|
|
119
|
-
console.log(chalk.green(`✓ Audit report exported: rigour-audit-report.md`));
|
|
120
278
|
console.log('');
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
console.log(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
console.log(chalk.
|
|
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
|
+
}
|
|
131
425
|
console.log('');
|
|
132
|
-
console.log(chalk.dim(`Demo files: ${demoDir}`));
|
|
133
|
-
console.log(chalk.dim('GitHub: https://github.com/rigour-labs/rigour'));
|
|
134
|
-
console.log(chalk.dim('Docs: https://docs.rigour.run\n'));
|
|
135
|
-
console.log(chalk.dim.italic('If this saved you from a bad commit, star the repo ⭐'));
|
|
136
426
|
}
|
|
137
427
|
catch (error) {
|
|
138
|
-
|
|
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('');
|
|
139
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 ⭐'));
|
|
140
459
|
}
|
|
460
|
+
// ── Scaffold demo project ───────────────────────────────────────────
|
|
141
461
|
async function scaffoldDemoProject(dir) {
|
|
142
|
-
|
|
143
|
-
|
|
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 {
|
|
144
473
|
version: 1,
|
|
145
474
|
preset: 'api',
|
|
146
475
|
gates: {
|
|
147
476
|
max_file_lines: 300,
|
|
148
477
|
forbid_todos: true,
|
|
149
478
|
forbid_fixme: true,
|
|
150
|
-
ast: {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
},
|
|
154
|
-
security: {
|
|
155
|
-
enabled: true,
|
|
156
|
-
block_on_severity: 'high',
|
|
157
|
-
},
|
|
158
|
-
hallucinated_imports: {
|
|
159
|
-
enabled: true,
|
|
160
|
-
severity: 'critical',
|
|
161
|
-
},
|
|
162
|
-
promise_safety: {
|
|
163
|
-
enabled: true,
|
|
164
|
-
severity: 'high',
|
|
165
|
-
},
|
|
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' },
|
|
166
483
|
},
|
|
484
|
+
hooks: { enabled: true, tools: ['claude'] },
|
|
167
485
|
ignore: ['.git/**', 'node_modules/**'],
|
|
168
|
-
output: {
|
|
169
|
-
report_path: 'rigour-report.json',
|
|
170
|
-
},
|
|
486
|
+
output: { report_path: 'rigour-report.json' },
|
|
171
487
|
};
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
488
|
+
}
|
|
489
|
+
function buildDemoPackageJson() {
|
|
490
|
+
return {
|
|
175
491
|
name: 'rigour-demo',
|
|
176
492
|
version: '1.0.0',
|
|
177
|
-
dependencies: {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// src directory
|
|
183
|
-
await fs.ensureDir(path.join(dir, 'src'));
|
|
184
|
-
// Issue 1: Hardcoded API key (security)
|
|
493
|
+
dependencies: { express: '^4.18.0', zod: '^3.22.0' },
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
async function writeIssueFiles(dir) {
|
|
497
|
+
// Issue 1: Hardcoded API key
|
|
185
498
|
await fs.writeFile(path.join(dir, 'src', 'auth.ts'), `
|
|
186
499
|
import express from 'express';
|
|
187
500
|
|
|
@@ -200,7 +513,7 @@ export function connectDatabase() {
|
|
|
200
513
|
return { host: 'prod-db.internal', password: DB_PASSWORD };
|
|
201
514
|
}
|
|
202
515
|
`.trim());
|
|
203
|
-
// Issue 2: Unhandled promise
|
|
516
|
+
// Issue 2: Unhandled promise
|
|
204
517
|
await fs.writeFile(path.join(dir, 'src', 'api-handler.ts'), `
|
|
205
518
|
import express from 'express';
|
|
206
519
|
|
|
@@ -210,17 +523,15 @@ export async function fetchUserData(userId: string) {
|
|
|
210
523
|
}
|
|
211
524
|
|
|
212
525
|
export function handleRequest(req: express.Request, res: express.Response) {
|
|
213
|
-
// AI generated this without .catch() — floating promise
|
|
214
526
|
fetchUserData(req.params.id);
|
|
215
527
|
res.send('Processing...');
|
|
216
528
|
}
|
|
217
529
|
|
|
218
530
|
export function batchProcess(ids: string[]) {
|
|
219
|
-
// Multiple unhandled promises
|
|
220
531
|
ids.forEach(id => fetchUserData(id));
|
|
221
532
|
}
|
|
222
533
|
`.trim());
|
|
223
|
-
// Issue 3: Hallucinated import
|
|
534
|
+
// Issue 3: Hallucinated import
|
|
224
535
|
await fs.writeFile(path.join(dir, 'src', 'data-loader.ts'), `
|
|
225
536
|
import { z } from 'zod';
|
|
226
537
|
import { magicParser } from 'ai-data-magic';
|
|
@@ -236,7 +547,7 @@ export function loadData(raw: unknown) {
|
|
|
236
547
|
return parsed;
|
|
237
548
|
}
|
|
238
549
|
`.trim());
|
|
239
|
-
// Issue 4: TODO
|
|
550
|
+
// Issue 4: TODO markers
|
|
240
551
|
await fs.writeFile(path.join(dir, 'src', 'utils.ts'), `
|
|
241
552
|
// TODO: Claude suggested this but I need to review
|
|
242
553
|
// FIXME: This function has edge cases
|
|
@@ -249,26 +560,25 @@ export function sanitizeInput(input: string): string {
|
|
|
249
560
|
return input.trim();
|
|
250
561
|
}
|
|
251
562
|
`.trim());
|
|
252
|
-
|
|
253
|
-
|
|
563
|
+
}
|
|
564
|
+
async function writeGodFile(dir) {
|
|
565
|
+
const lines = [
|
|
254
566
|
'// Auto-generated data processing module',
|
|
255
567
|
'export class DataProcessor {',
|
|
256
568
|
];
|
|
257
569
|
for (let i = 0; i < 60; i++) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
await fs.writeFile(path.join(dir, 'src', 'god-file.ts'),
|
|
268
|
-
// docs directory (missing required docs)
|
|
269
|
-
await fs.ensureDir(path.join(dir, 'docs'));
|
|
270
|
-
await fs.writeFile(path.join(dir, 'README.md'), '# Demo Project\n\nThis is a demo project for Rigour.\n');
|
|
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'));
|
|
271
580
|
}
|
|
581
|
+
// ── Audit report generator ──────────────────────────────────────────
|
|
272
582
|
async function generateDemoAudit(dir, report, outputPath) {
|
|
273
583
|
const stats = report.stats || {};
|
|
274
584
|
const failures = report.failures || [];
|
|
@@ -278,10 +588,12 @@ async function generateDemoAudit(dir, report, outputPath) {
|
|
|
278
588
|
lines.push(`**Generated:** ${new Date().toISOString()}`);
|
|
279
589
|
lines.push(`**Status:** ${report.status}`);
|
|
280
590
|
lines.push(`**Score:** ${stats.score ?? 100}/100`);
|
|
281
|
-
if (stats.ai_health_score !== undefined)
|
|
591
|
+
if (stats.ai_health_score !== undefined) {
|
|
282
592
|
lines.push(`**AI Health:** ${stats.ai_health_score}/100`);
|
|
283
|
-
|
|
593
|
+
}
|
|
594
|
+
if (stats.structural_score !== undefined) {
|
|
284
595
|
lines.push(`**Structural:** ${stats.structural_score}/100`);
|
|
596
|
+
}
|
|
285
597
|
lines.push('');
|
|
286
598
|
lines.push('## Violations');
|
|
287
599
|
lines.push('');
|
|
@@ -291,16 +603,16 @@ async function generateDemoAudit(dir, report, outputPath) {
|
|
|
291
603
|
lines.push(`- **ID:** \`${f.id}\``);
|
|
292
604
|
lines.push(`- **Provenance:** ${f.provenance || 'traditional'}`);
|
|
293
605
|
lines.push(`- **Details:** ${f.details}`);
|
|
294
|
-
if (f.files?.length)
|
|
606
|
+
if (f.files?.length) {
|
|
295
607
|
lines.push(`- **Files:** ${f.files.join(', ')}`);
|
|
296
|
-
|
|
608
|
+
}
|
|
609
|
+
if (f.hint) {
|
|
297
610
|
lines.push(`- **Hint:** ${f.hint}`);
|
|
611
|
+
}
|
|
298
612
|
lines.push('');
|
|
299
613
|
}
|
|
300
614
|
lines.push('---');
|
|
301
615
|
lines.push('*Generated by Rigour — https://rigour.run*');
|
|
616
|
+
lines.push('*Research: https://zenodo.org/records/18673564*');
|
|
302
617
|
await fs.writeFile(outputPath, lines.join('\n'));
|
|
303
618
|
}
|
|
304
|
-
function sleep(ms) {
|
|
305
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
306
|
-
}
|