@rigour-labs/cli 2.21.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/dist/cli.js +36 -5
- package/dist/commands/check.js +68 -11
- package/dist/commands/demo.d.ts +16 -0
- package/dist/commands/demo.js +306 -0
- package/dist/commands/explain.js +46 -1
- package/dist/commands/export-audit.d.ts +16 -0
- package/dist/commands/export-audit.js +245 -0
- package/dist/init-rules.test.js +2 -1
- package/dist/smoke.test.js +2 -1
- package/package.json +18 -2
- package/studio-dist/assets/index-C0TtM2OR.js +291 -0
- package/studio-dist/index.html +1 -1
- package/studio-dist/assets/index-Ch-q_mnO.js +0 -291
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ Agent writes code → Rigour checks → FAIL? → Fix Packet → Agent retries
|
|
|
35
35
|
|
|
36
36
|
## ⚙️ Quality Gates
|
|
37
37
|
|
|
38
|
+
### Structural & Security Gates
|
|
38
39
|
| Gate | Description |
|
|
39
40
|
|:---|:---|
|
|
40
41
|
| **File Size** | Max lines per file (default: 300-500) |
|
|
@@ -42,9 +43,21 @@ Agent writes code → Rigour checks → FAIL? → Fix Packet → Agent retries
|
|
|
42
43
|
| **Complexity** | Cyclomatic complexity limits (AST-based) |
|
|
43
44
|
| **Required Docs** | SPEC.md, ARCH.md, README must exist |
|
|
44
45
|
| **File Guard** | Protected paths, max files changed |
|
|
45
|
-
| **Security Patterns** | XSS, SQL injection, hardcoded secrets, command injection
|
|
46
|
+
| **Security Patterns** | XSS, SQL injection, hardcoded secrets, command injection |
|
|
46
47
|
| **Context Alignment** | Prevents drift by anchoring on project patterns |
|
|
47
48
|
|
|
49
|
+
### AI-Native Drift Detection (v2.16+)
|
|
50
|
+
| Gate | Description |
|
|
51
|
+
|:---|:---|
|
|
52
|
+
| **Duplication Drift** | Near-identical functions across files — AI re-invents what it forgot |
|
|
53
|
+
| **Hallucinated Imports** | Imports referencing modules that don't exist (JS/TS, Python, Go, Ruby, C#) |
|
|
54
|
+
| **Inconsistent Error Handling** | Same error type handled differently across agent sessions |
|
|
55
|
+
| **Context Window Artifacts** | Quality degradation within a file — clean top, messy bottom |
|
|
56
|
+
| **Async & Error Safety** | Unsafe async/promise patterns, unhandled errors across 6 languages |
|
|
57
|
+
|
|
58
|
+
### Multi-Language Support
|
|
59
|
+
All gates support **TypeScript, JavaScript, Python, Go, Ruby, and C#/.NET**.
|
|
60
|
+
|
|
48
61
|
## 🛠️ Commands
|
|
49
62
|
|
|
50
63
|
| Command | Purpose |
|
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,8 @@ import { guideCommand } from './commands/guide.js';
|
|
|
8
8
|
import { setupCommand } from './commands/setup.js';
|
|
9
9
|
import { indexCommand } from './commands/index.js';
|
|
10
10
|
import { studioCommand } from './commands/studio.js';
|
|
11
|
+
import { exportAuditCommand } from './commands/export-audit.js';
|
|
12
|
+
import { demoCommand } from './commands/demo.js';
|
|
11
13
|
import { checkForUpdates } from './utils/version.js';
|
|
12
14
|
import chalk from 'chalk';
|
|
13
15
|
const CLI_VERSION = '2.0.0';
|
|
@@ -29,7 +31,7 @@ program
|
|
|
29
31
|
program
|
|
30
32
|
.command('init')
|
|
31
33
|
.description('Initialize Rigour in the current directory')
|
|
32
|
-
.option('-p, --preset <name>', 'Project preset (ui, api, infra, data)')
|
|
34
|
+
.option('-p, --preset <name>', 'Project preset (ui, api, infra, data, healthcare, fintech, government)')
|
|
33
35
|
.option('--paradigm <name>', 'Coding paradigm (oop, functional, minimal)')
|
|
34
36
|
.option('--ide <name>', 'Target IDE (cursor, vscode, all). Auto-detects if not specified.')
|
|
35
37
|
.option('--dry-run', 'Show detected configuration without writing files')
|
|
@@ -37,10 +39,12 @@ program
|
|
|
37
39
|
.option('-f, --force', 'Force re-initialization, overwriting existing rigour.yml')
|
|
38
40
|
.addHelpText('after', `
|
|
39
41
|
Examples:
|
|
40
|
-
$ rigour init
|
|
41
|
-
$ rigour init --preset api --explain
|
|
42
|
-
$ rigour init --
|
|
43
|
-
$ rigour init --
|
|
42
|
+
$ rigour init # Auto-discover role & paradigm
|
|
43
|
+
$ rigour init --preset api --explain # Force API role and show why
|
|
44
|
+
$ rigour init --preset healthcare # HIPAA-compliant quality gates
|
|
45
|
+
$ rigour init --preset fintech # SOC2/PCI-DSS quality gates
|
|
46
|
+
$ rigour init --preset government # FedRAMP/NIST quality gates
|
|
47
|
+
$ rigour init --ide all # Create files for all IDEs
|
|
44
48
|
`)
|
|
45
49
|
.action(async (options) => {
|
|
46
50
|
await initCommand(process.cwd(), options);
|
|
@@ -91,6 +95,33 @@ Examples:
|
|
|
91
95
|
failFast: !!options.failFast
|
|
92
96
|
});
|
|
93
97
|
});
|
|
98
|
+
program
|
|
99
|
+
.command('export-audit')
|
|
100
|
+
.description('Generate a compliance audit package from the last check')
|
|
101
|
+
.option('-f, --format <type>', 'Output format: json or md', 'json')
|
|
102
|
+
.option('-o, --output <path>', 'Custom output file path')
|
|
103
|
+
.option('--run', 'Run a fresh rigour check before exporting')
|
|
104
|
+
.addHelpText('after', `
|
|
105
|
+
Examples:
|
|
106
|
+
$ rigour export-audit # Export JSON audit package
|
|
107
|
+
$ rigour export-audit --format md # Export Markdown report
|
|
108
|
+
$ rigour export-audit --run # Run check first, then export
|
|
109
|
+
$ rigour export-audit -o audit.json # Custom output path
|
|
110
|
+
`)
|
|
111
|
+
.action(async (options) => {
|
|
112
|
+
await exportAuditCommand(process.cwd(), options);
|
|
113
|
+
});
|
|
114
|
+
program
|
|
115
|
+
.command('demo')
|
|
116
|
+
.description('Run a live demo — see Rigour catch AI drift, security issues, and structural violations')
|
|
117
|
+
.addHelpText('after', `
|
|
118
|
+
Examples:
|
|
119
|
+
$ rigour demo # Run the flagship demo
|
|
120
|
+
$ npx @rigour-labs/cli demo # Try without installing
|
|
121
|
+
`)
|
|
122
|
+
.action(async () => {
|
|
123
|
+
await demoCommand();
|
|
124
|
+
});
|
|
94
125
|
program
|
|
95
126
|
.command('guide')
|
|
96
127
|
.description('Show the interactive engineering guide')
|
package/dist/commands/check.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import yaml from 'yaml';
|
|
5
|
-
import { GateRunner, ConfigSchema } from '@rigour-labs/core';
|
|
5
|
+
import { GateRunner, ConfigSchema, recordScore, getScoreTrend } from '@rigour-labs/core';
|
|
6
6
|
import inquirer from 'inquirer';
|
|
7
7
|
import { randomUUID } from 'crypto';
|
|
8
8
|
// Exit codes per spec
|
|
@@ -57,6 +57,8 @@ export async function checkCommand(cwd, files = [], options = {}) {
|
|
|
57
57
|
// Write machine report
|
|
58
58
|
const reportPath = path.join(cwd, config.output.report_path);
|
|
59
59
|
await fs.writeJson(reportPath, report, { spaces: 2 });
|
|
60
|
+
// Record score for trend tracking
|
|
61
|
+
recordScore(cwd, report);
|
|
60
62
|
await logStudioEvent(cwd, {
|
|
61
63
|
type: "tool_response",
|
|
62
64
|
requestId,
|
|
@@ -81,15 +83,18 @@ export async function checkCommand(cwd, files = [], options = {}) {
|
|
|
81
83
|
});
|
|
82
84
|
return; // Wait for write callback
|
|
83
85
|
}
|
|
84
|
-
// CI mode: minimal output
|
|
86
|
+
// CI mode: minimal output with score
|
|
85
87
|
if (options.ci) {
|
|
86
88
|
if (report.status === 'PASS') {
|
|
87
|
-
|
|
89
|
+
const scoreStr = report.stats.score !== undefined ? ` (${report.stats.score}/100)` : '';
|
|
90
|
+
console.log(`PASS${scoreStr}`);
|
|
88
91
|
}
|
|
89
92
|
else {
|
|
90
|
-
|
|
93
|
+
const scoreStr = report.stats.score !== undefined ? ` Score: ${report.stats.score}/100` : '';
|
|
94
|
+
console.log(`FAIL: ${report.failures.length} violation(s)${scoreStr}`);
|
|
91
95
|
report.failures.forEach((f) => {
|
|
92
|
-
|
|
96
|
+
const sev = (f.severity || 'medium').toUpperCase();
|
|
97
|
+
console.log(` - [${sev}] [${f.id}] ${f.title}`);
|
|
93
98
|
});
|
|
94
99
|
}
|
|
95
100
|
process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
|
|
@@ -104,21 +109,73 @@ export async function checkCommand(cwd, files = [], options = {}) {
|
|
|
104
109
|
}
|
|
105
110
|
else {
|
|
106
111
|
console.log(chalk.red.bold('✘ FAIL - Quality gate violations found.\n'));
|
|
112
|
+
// Score summary line
|
|
113
|
+
const stats = report.stats;
|
|
114
|
+
const scoreParts = [];
|
|
115
|
+
if (stats.score !== undefined)
|
|
116
|
+
scoreParts.push(`Score: ${stats.score}/100`);
|
|
117
|
+
if (stats.ai_health_score !== undefined)
|
|
118
|
+
scoreParts.push(`AI Health: ${stats.ai_health_score}/100`);
|
|
119
|
+
if (stats.structural_score !== undefined)
|
|
120
|
+
scoreParts.push(`Structural: ${stats.structural_score}/100`);
|
|
121
|
+
if (scoreParts.length > 0) {
|
|
122
|
+
console.log(chalk.bold(scoreParts.join(' | ')) + '\n');
|
|
123
|
+
}
|
|
124
|
+
// Severity breakdown
|
|
125
|
+
if (stats.severity_breakdown) {
|
|
126
|
+
const parts = Object.entries(stats.severity_breakdown)
|
|
127
|
+
.filter(([, count]) => count > 0)
|
|
128
|
+
.map(([sev, count]) => {
|
|
129
|
+
const color = sev === 'critical' ? chalk.red.bold : sev === 'high' ? chalk.red : sev === 'medium' ? chalk.yellow : chalk.dim;
|
|
130
|
+
return color(`${sev}: ${count}`);
|
|
131
|
+
});
|
|
132
|
+
if (parts.length > 0) {
|
|
133
|
+
console.log('Severity: ' + parts.join(', ') + '\n');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Group failures by provenance
|
|
137
|
+
const severityIcon = (s) => {
|
|
138
|
+
switch (s) {
|
|
139
|
+
case 'critical': return chalk.red.bold('CRIT');
|
|
140
|
+
case 'high': return chalk.red('HIGH');
|
|
141
|
+
case 'medium': return chalk.yellow('MED ');
|
|
142
|
+
case 'low': return chalk.dim('LOW ');
|
|
143
|
+
case 'info': return chalk.dim('INFO');
|
|
144
|
+
default: return chalk.yellow('MED ');
|
|
145
|
+
}
|
|
146
|
+
};
|
|
107
147
|
for (const failure of report.failures) {
|
|
108
|
-
|
|
109
|
-
|
|
148
|
+
const sev = severityIcon(failure.severity);
|
|
149
|
+
const prov = failure.provenance ? chalk.dim(`[${failure.provenance}]`) : '';
|
|
150
|
+
console.log(`${sev} ${prov} ${chalk.red(`[${failure.id}]`)} ${failure.title}`);
|
|
151
|
+
console.log(chalk.dim(` Details: ${failure.details}`));
|
|
110
152
|
if (failure.files && failure.files.length > 0) {
|
|
111
|
-
console.log(chalk.dim('
|
|
112
|
-
failure.files.forEach((f) => console.log(chalk.dim(`
|
|
153
|
+
console.log(chalk.dim(' Files:'));
|
|
154
|
+
failure.files.forEach((f) => console.log(chalk.dim(` - ${f}`)));
|
|
113
155
|
}
|
|
114
156
|
if (failure.hint) {
|
|
115
|
-
console.log(chalk.cyan(`
|
|
157
|
+
console.log(chalk.cyan(` Hint: ${failure.hint}`));
|
|
116
158
|
}
|
|
117
159
|
console.log('');
|
|
118
160
|
}
|
|
119
161
|
console.log(chalk.yellow(`See ${config.output.report_path} for full details.`));
|
|
120
162
|
}
|
|
121
|
-
|
|
163
|
+
// Score trend display
|
|
164
|
+
const trend = getScoreTrend(cwd);
|
|
165
|
+
if (trend && trend.recentScores.length >= 3) {
|
|
166
|
+
const arrow = trend.direction === 'improving' ? chalk.green('↑') :
|
|
167
|
+
trend.direction === 'degrading' ? chalk.red('↓') : chalk.dim('→');
|
|
168
|
+
const trendColor = trend.direction === 'improving' ? chalk.green :
|
|
169
|
+
trend.direction === 'degrading' ? chalk.red : chalk.dim;
|
|
170
|
+
const scoresStr = trend.recentScores.map(s => String(s)).join(' → ');
|
|
171
|
+
console.log(trendColor(`\nScore Trend: ${scoresStr} (${trend.direction} ${arrow})`));
|
|
172
|
+
}
|
|
173
|
+
// Stats footer
|
|
174
|
+
const footerParts = [`Finished in ${report.stats.duration_ms}ms`];
|
|
175
|
+
if (report.status === 'PASS' && report.stats.score !== undefined) {
|
|
176
|
+
footerParts.push(`Score: ${report.stats.score}/100`);
|
|
177
|
+
}
|
|
178
|
+
console.log(chalk.dim('\n' + footerParts.join(' | ')));
|
|
122
179
|
process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
|
|
123
180
|
}
|
|
124
181
|
catch (error) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rigour demo
|
|
3
|
+
*
|
|
4
|
+
* Creates a temp project with intentional AI-generated code issues,
|
|
5
|
+
* runs Rigour against it, and shows the full experience:
|
|
6
|
+
* 1. Scaffolds a broken codebase
|
|
7
|
+
* 2. Runs quality gates → FAIL with violations
|
|
8
|
+
* 3. Shows the fix packet
|
|
9
|
+
* 4. Exports an audit report
|
|
10
|
+
* 5. Cleans up
|
|
11
|
+
*
|
|
12
|
+
* The "flagship demo" — one command to understand Rigour.
|
|
13
|
+
*
|
|
14
|
+
* @since v2.17.0
|
|
15
|
+
*/
|
|
16
|
+
export declare function demoCommand(): Promise<void>;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rigour demo
|
|
3
|
+
*
|
|
4
|
+
* Creates a temp project with intentional AI-generated code issues,
|
|
5
|
+
* runs Rigour against it, and shows the full experience:
|
|
6
|
+
* 1. Scaffolds a broken codebase
|
|
7
|
+
* 2. Runs quality gates → FAIL with violations
|
|
8
|
+
* 3. Shows the fix packet
|
|
9
|
+
* 4. Exports an audit report
|
|
10
|
+
* 5. Cleans up
|
|
11
|
+
*
|
|
12
|
+
* The "flagship demo" — one command to understand Rigour.
|
|
13
|
+
*
|
|
14
|
+
* @since v2.17.0
|
|
15
|
+
*/
|
|
16
|
+
import fs from 'fs-extra';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
import yaml from 'yaml';
|
|
20
|
+
import os from 'os';
|
|
21
|
+
import { GateRunner, ConfigSchema } from '@rigour-labs/core';
|
|
22
|
+
import { recordScore } from '@rigour-labs/core';
|
|
23
|
+
export async function demoCommand() {
|
|
24
|
+
console.log(chalk.bold.cyan(`
|
|
25
|
+
____ _
|
|
26
|
+
/ __ \\(_)____ ___ __ __ _____
|
|
27
|
+
/ /_/ // // __ \`/ / / / / // ___/
|
|
28
|
+
/ _, _// // /_/ // /_/ / / // /
|
|
29
|
+
/_/ |_|/_/ \\__, / \\__,_/_/ /_/
|
|
30
|
+
/____/
|
|
31
|
+
`));
|
|
32
|
+
console.log(chalk.bold('Rigour Demo — See AI code governance in action.\n'));
|
|
33
|
+
// 1. Create temp project
|
|
34
|
+
const demoDir = path.join(os.tmpdir(), `rigour-demo-${Date.now()}`);
|
|
35
|
+
console.log(chalk.dim(`Creating demo project at ${demoDir}...\n`));
|
|
36
|
+
await fs.ensureDir(demoDir);
|
|
37
|
+
await scaffoldDemoProject(demoDir);
|
|
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)'));
|
|
42
|
+
console.log(chalk.dim(' 2. src/api-handler.ts — Unhandled promise (AI drift)'));
|
|
43
|
+
console.log(chalk.dim(' 3. src/data-loader.ts — Hallucinated import (AI drift)'));
|
|
44
|
+
console.log(chalk.dim(' 4. src/utils.ts — TODO marker left by AI'));
|
|
45
|
+
console.log(chalk.dim(' 5. src/god-file.ts — 350+ lines (structural)'));
|
|
46
|
+
console.log('');
|
|
47
|
+
// 2. Run quality gates
|
|
48
|
+
console.log(chalk.bold.blue('🔍 Running Rigour quality gates...\n'));
|
|
49
|
+
await sleep(500);
|
|
50
|
+
try {
|
|
51
|
+
const configContent = await fs.readFile(path.join(demoDir, 'rigour.yml'), 'utf-8');
|
|
52
|
+
const rawConfig = yaml.parse(configContent);
|
|
53
|
+
const config = ConfigSchema.parse(rawConfig);
|
|
54
|
+
const runner = new GateRunner(config);
|
|
55
|
+
const report = await runner.run(demoDir);
|
|
56
|
+
// Record score
|
|
57
|
+
recordScore(demoDir, report);
|
|
58
|
+
// Write report
|
|
59
|
+
const reportPath = path.join(demoDir, config.output.report_path);
|
|
60
|
+
await fs.writeJson(reportPath, report, { spaces: 2 });
|
|
61
|
+
// Display results
|
|
62
|
+
const stats = report.stats;
|
|
63
|
+
if (report.status === 'FAIL') {
|
|
64
|
+
console.log(chalk.red.bold('✘ FAIL — Quality gate violations found.\n'));
|
|
65
|
+
// Score display
|
|
66
|
+
const scoreParts = [];
|
|
67
|
+
if (stats.score !== undefined)
|
|
68
|
+
scoreParts.push(`Score: ${stats.score}/100`);
|
|
69
|
+
if (stats.ai_health_score !== undefined)
|
|
70
|
+
scoreParts.push(`AI Health: ${stats.ai_health_score}/100`);
|
|
71
|
+
if (stats.structural_score !== undefined)
|
|
72
|
+
scoreParts.push(`Structural: ${stats.structural_score}/100`);
|
|
73
|
+
if (scoreParts.length > 0) {
|
|
74
|
+
console.log(chalk.bold(scoreParts.join(' | ')) + '\n');
|
|
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('');
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
console.log(chalk.green.bold('✔ PASS — All quality gates satisfied.\n'));
|
|
103
|
+
}
|
|
104
|
+
console.log(chalk.dim(`Finished in ${stats.duration_ms}ms\n`));
|
|
105
|
+
// 3. Generate fix packet
|
|
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`));
|
|
113
|
+
}
|
|
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
|
+
console.log('');
|
|
121
|
+
console.log(chalk.bold.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
122
|
+
console.log(chalk.bold('This is what Rigour does:'));
|
|
123
|
+
console.log(chalk.dim(' • Catches AI drift (hallucinated imports, unhandled promises)'));
|
|
124
|
+
console.log(chalk.dim(' • Blocks security issues (hardcoded keys, injection patterns)'));
|
|
125
|
+
console.log(chalk.dim(' • Enforces structure (file size, complexity, documentation)'));
|
|
126
|
+
console.log(chalk.dim(' • Generates audit-ready evidence (scores, trends, reports)'));
|
|
127
|
+
console.log(chalk.bold.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
|
|
128
|
+
console.log(chalk.bold('Get started:'));
|
|
129
|
+
console.log(chalk.white(' $ npx @rigour-labs/cli init'));
|
|
130
|
+
console.log(chalk.white(' $ npx @rigour-labs/cli check'));
|
|
131
|
+
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
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
console.error(chalk.red(`Demo error: ${error.message}`));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function scaffoldDemoProject(dir) {
|
|
142
|
+
// rigour.yml
|
|
143
|
+
const config = {
|
|
144
|
+
version: 1,
|
|
145
|
+
preset: 'api',
|
|
146
|
+
gates: {
|
|
147
|
+
max_file_lines: 300,
|
|
148
|
+
forbid_todos: true,
|
|
149
|
+
forbid_fixme: true,
|
|
150
|
+
ast: {
|
|
151
|
+
complexity: 10,
|
|
152
|
+
max_params: 5,
|
|
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
|
+
},
|
|
166
|
+
},
|
|
167
|
+
ignore: ['.git/**', 'node_modules/**'],
|
|
168
|
+
output: {
|
|
169
|
+
report_path: 'rigour-report.json',
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
await fs.writeFile(path.join(dir, 'rigour.yml'), yaml.stringify(config));
|
|
173
|
+
// package.json (for hallucinated import detection)
|
|
174
|
+
await fs.writeJson(path.join(dir, 'package.json'), {
|
|
175
|
+
name: 'rigour-demo',
|
|
176
|
+
version: '1.0.0',
|
|
177
|
+
dependencies: {
|
|
178
|
+
express: '^4.18.0',
|
|
179
|
+
zod: '^3.22.0',
|
|
180
|
+
},
|
|
181
|
+
}, { spaces: 2 });
|
|
182
|
+
// src directory
|
|
183
|
+
await fs.ensureDir(path.join(dir, 'src'));
|
|
184
|
+
// Issue 1: Hardcoded API key (security)
|
|
185
|
+
await fs.writeFile(path.join(dir, 'src', 'auth.ts'), `
|
|
186
|
+
import express from 'express';
|
|
187
|
+
|
|
188
|
+
const API_KEY = "sk-live-4f3c2b1a0987654321abcdef";
|
|
189
|
+
const DB_PASSWORD = "super_secret_p@ssw0rd!";
|
|
190
|
+
|
|
191
|
+
export function authenticate(req: express.Request) {
|
|
192
|
+
const token = req.headers.authorization;
|
|
193
|
+
if (token === API_KEY) {
|
|
194
|
+
return { authenticated: true };
|
|
195
|
+
}
|
|
196
|
+
return { authenticated: false };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function connectDatabase() {
|
|
200
|
+
return { host: 'prod-db.internal', password: DB_PASSWORD };
|
|
201
|
+
}
|
|
202
|
+
`.trim());
|
|
203
|
+
// Issue 2: Unhandled promise (AI drift — promise safety)
|
|
204
|
+
await fs.writeFile(path.join(dir, 'src', 'api-handler.ts'), `
|
|
205
|
+
import express from 'express';
|
|
206
|
+
|
|
207
|
+
export async function fetchUserData(userId: string) {
|
|
208
|
+
const response = await fetch(\`https://api.example.com/users/\${userId}\`);
|
|
209
|
+
return response.json();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function handleRequest(req: express.Request, res: express.Response) {
|
|
213
|
+
// AI generated this without .catch() — floating promise
|
|
214
|
+
fetchUserData(req.params.id);
|
|
215
|
+
res.send('Processing...');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function batchProcess(ids: string[]) {
|
|
219
|
+
// Multiple unhandled promises
|
|
220
|
+
ids.forEach(id => fetchUserData(id));
|
|
221
|
+
}
|
|
222
|
+
`.trim());
|
|
223
|
+
// Issue 3: Hallucinated import (AI drift)
|
|
224
|
+
await fs.writeFile(path.join(dir, 'src', 'data-loader.ts'), `
|
|
225
|
+
import { z } from 'zod';
|
|
226
|
+
import { magicParser } from 'ai-data-magic';
|
|
227
|
+
import { ultraCache } from 'quantum-cache-pro';
|
|
228
|
+
|
|
229
|
+
const schema = z.object({
|
|
230
|
+
name: z.string(),
|
|
231
|
+
email: z.string().email(),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
export function loadData(raw: unknown) {
|
|
235
|
+
const parsed = schema.parse(raw);
|
|
236
|
+
return parsed;
|
|
237
|
+
}
|
|
238
|
+
`.trim());
|
|
239
|
+
// Issue 4: TODO marker
|
|
240
|
+
await fs.writeFile(path.join(dir, 'src', 'utils.ts'), `
|
|
241
|
+
// TODO: Claude suggested this but I need to review
|
|
242
|
+
// FIXME: This function has edge cases
|
|
243
|
+
export function formatDate(date: Date): string {
|
|
244
|
+
return date.toISOString().split('T')[0];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function sanitizeInput(input: string): string {
|
|
248
|
+
// TODO: Add proper sanitization
|
|
249
|
+
return input.trim();
|
|
250
|
+
}
|
|
251
|
+
`.trim());
|
|
252
|
+
// Issue 5: God file (350+ lines)
|
|
253
|
+
const godFileLines = [
|
|
254
|
+
'// Auto-generated data processing module',
|
|
255
|
+
'export class DataProcessor {',
|
|
256
|
+
];
|
|
257
|
+
for (let i = 0; i < 60; i++) {
|
|
258
|
+
godFileLines.push(` process${i}(data: any) {`);
|
|
259
|
+
godFileLines.push(` const result = data.map((x: any) => x * ${i + 1});`);
|
|
260
|
+
godFileLines.push(` if (result.length > ${i * 10}) {`);
|
|
261
|
+
godFileLines.push(` return result.slice(0, ${i * 10});`);
|
|
262
|
+
godFileLines.push(` }`);
|
|
263
|
+
godFileLines.push(` return result;`);
|
|
264
|
+
godFileLines.push(` }`);
|
|
265
|
+
}
|
|
266
|
+
godFileLines.push('}');
|
|
267
|
+
await fs.writeFile(path.join(dir, 'src', 'god-file.ts'), godFileLines.join('\n'));
|
|
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');
|
|
271
|
+
}
|
|
272
|
+
async function generateDemoAudit(dir, report, outputPath) {
|
|
273
|
+
const stats = report.stats || {};
|
|
274
|
+
const failures = report.failures || [];
|
|
275
|
+
const lines = [];
|
|
276
|
+
lines.push('# Rigour Audit Report — Demo');
|
|
277
|
+
lines.push('');
|
|
278
|
+
lines.push(`**Generated:** ${new Date().toISOString()}`);
|
|
279
|
+
lines.push(`**Status:** ${report.status}`);
|
|
280
|
+
lines.push(`**Score:** ${stats.score ?? 100}/100`);
|
|
281
|
+
if (stats.ai_health_score !== undefined)
|
|
282
|
+
lines.push(`**AI Health:** ${stats.ai_health_score}/100`);
|
|
283
|
+
if (stats.structural_score !== undefined)
|
|
284
|
+
lines.push(`**Structural:** ${stats.structural_score}/100`);
|
|
285
|
+
lines.push('');
|
|
286
|
+
lines.push('## Violations');
|
|
287
|
+
lines.push('');
|
|
288
|
+
for (let i = 0; i < failures.length; i++) {
|
|
289
|
+
const f = failures[i];
|
|
290
|
+
lines.push(`### ${i + 1}. [${(f.severity || 'medium').toUpperCase()}] ${f.title}`);
|
|
291
|
+
lines.push(`- **ID:** \`${f.id}\``);
|
|
292
|
+
lines.push(`- **Provenance:** ${f.provenance || 'traditional'}`);
|
|
293
|
+
lines.push(`- **Details:** ${f.details}`);
|
|
294
|
+
if (f.files?.length)
|
|
295
|
+
lines.push(`- **Files:** ${f.files.join(', ')}`);
|
|
296
|
+
if (f.hint)
|
|
297
|
+
lines.push(`- **Hint:** ${f.hint}`);
|
|
298
|
+
lines.push('');
|
|
299
|
+
}
|
|
300
|
+
lines.push('---');
|
|
301
|
+
lines.push('*Generated by Rigour — https://rigour.run*');
|
|
302
|
+
await fs.writeFile(outputPath, lines.join('\n'));
|
|
303
|
+
}
|
|
304
|
+
function sleep(ms) {
|
|
305
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
306
|
+
}
|
package/dist/commands/explain.js
CHANGED
|
@@ -28,6 +28,38 @@ export async function explainCommand(cwd) {
|
|
|
28
28
|
console.log(chalk.bold('Status: ') + (report.status === 'PASS'
|
|
29
29
|
? chalk.green.bold('✅ PASS')
|
|
30
30
|
: chalk.red.bold('🛑 FAIL')));
|
|
31
|
+
// Score summary
|
|
32
|
+
if (report.stats) {
|
|
33
|
+
const stats = report.stats;
|
|
34
|
+
const scoreParts = [];
|
|
35
|
+
if (stats.score !== undefined)
|
|
36
|
+
scoreParts.push(`Score: ${stats.score}/100`);
|
|
37
|
+
if (stats.ai_health_score !== undefined)
|
|
38
|
+
scoreParts.push(`AI Health: ${stats.ai_health_score}/100`);
|
|
39
|
+
if (stats.structural_score !== undefined)
|
|
40
|
+
scoreParts.push(`Structural: ${stats.structural_score}/100`);
|
|
41
|
+
if (scoreParts.length > 0) {
|
|
42
|
+
console.log(chalk.bold('\n' + scoreParts.join(' | ')));
|
|
43
|
+
}
|
|
44
|
+
// Severity breakdown
|
|
45
|
+
if (stats.severity_breakdown) {
|
|
46
|
+
const sevParts = Object.entries(stats.severity_breakdown)
|
|
47
|
+
.filter(([, count]) => count > 0)
|
|
48
|
+
.map(([sev, count]) => `${sev}: ${count}`);
|
|
49
|
+
if (sevParts.length > 0) {
|
|
50
|
+
console.log(chalk.dim('Severity: ' + sevParts.join(', ')));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Provenance breakdown
|
|
54
|
+
if (stats.provenance_breakdown) {
|
|
55
|
+
const provParts = Object.entries(stats.provenance_breakdown)
|
|
56
|
+
.filter(([, count]) => count > 0)
|
|
57
|
+
.map(([prov, count]) => `${prov}: ${count}`);
|
|
58
|
+
if (provParts.length > 0) {
|
|
59
|
+
console.log(chalk.dim('Categories: ' + provParts.join(', ')));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
31
63
|
console.log(chalk.bold('\nGate Summary:'));
|
|
32
64
|
for (const [gate, status] of Object.entries(report.summary || {})) {
|
|
33
65
|
const icon = status === 'PASS' ? '✅' : status === 'FAIL' ? '❌' : '⏭️';
|
|
@@ -35,8 +67,21 @@ export async function explainCommand(cwd) {
|
|
|
35
67
|
}
|
|
36
68
|
if (report.failures && report.failures.length > 0) {
|
|
37
69
|
console.log(chalk.bold.red(`\n🔧 ${report.failures.length} Violation(s) to Fix:\n`));
|
|
70
|
+
// Severity color helper
|
|
71
|
+
const sevColor = (s) => {
|
|
72
|
+
switch (s) {
|
|
73
|
+
case 'critical': return chalk.red.bold;
|
|
74
|
+
case 'high': return chalk.red;
|
|
75
|
+
case 'medium': return chalk.yellow;
|
|
76
|
+
case 'low': return chalk.dim;
|
|
77
|
+
default: return chalk.dim;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
38
80
|
report.failures.forEach((failure, index) => {
|
|
39
|
-
|
|
81
|
+
const sev = failure.severity || 'medium';
|
|
82
|
+
const sevLabel = sevColor(sev)(`[${sev.toUpperCase()}]`);
|
|
83
|
+
const provLabel = failure.provenance ? chalk.dim(`(${failure.provenance})`) : '';
|
|
84
|
+
console.log(chalk.white(`${index + 1}. `) + sevLabel + ' ' + chalk.bold.yellow(`[${failure.id.toUpperCase()}]`) + ' ' + provLabel + chalk.white(` ${failure.title}`));
|
|
40
85
|
console.log(chalk.dim(` └─ ${failure.details}`));
|
|
41
86
|
if (failure.files && failure.files.length > 0) {
|
|
42
87
|
console.log(chalk.cyan(` 📁 Files: ${failure.files.join(', ')}`));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* export-audit command
|
|
3
|
+
*
|
|
4
|
+
* Generates a compliance audit package from the last gate check.
|
|
5
|
+
* The artifact compliance officers hand to auditors.
|
|
6
|
+
*
|
|
7
|
+
* Formats: JSON (structured) or Markdown (human-readable)
|
|
8
|
+
*
|
|
9
|
+
* @since v2.17.0
|
|
10
|
+
*/
|
|
11
|
+
export interface ExportAuditOptions {
|
|
12
|
+
format?: 'json' | 'md';
|
|
13
|
+
output?: string;
|
|
14
|
+
run?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function exportAuditCommand(cwd: string, options?: ExportAuditOptions): Promise<void>;
|