@rigour-labs/cli 3.0.3 → 3.0.5

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 CHANGED
@@ -12,6 +12,7 @@ Rigour forces AI agents to meet strict engineering standards before marking task
12
12
  ## 🚀 Quick Start
13
13
 
14
14
  ```bash
15
+ npx rigour scan # Zero-config scan (auto-detect stack)
15
16
  npx rigour init # Initialize quality gates
16
17
  npx rigour check # Verify code quality
17
18
  npx rigour run -- claude "Build feature X" # Agent loop
@@ -62,6 +63,7 @@ All gates support **TypeScript, JavaScript, Python, Go, Ruby, and C#/.NET**.
62
63
 
63
64
  | Command | Purpose |
64
65
  |:---|:---|
66
+ | `rigour scan` | Zero-config stack-aware scan using existing gates |
65
67
  | `rigour init` | Setup Rigour in your project |
66
68
  | `rigour check` | Validate code against quality gates |
67
69
  | `rigour check --ci` | CI mode with appropriate output |
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { Command } from 'commander';
3
3
  import { initCommand } from './commands/init.js';
4
4
  import { checkCommand } from './commands/check.js';
5
+ import { scanCommand } from './commands/scan.js';
5
6
  import { explainCommand } from './commands/explain.js';
6
7
  import { runLoop } from './commands/run.js';
7
8
  import { guideCommand } from './commands/guide.js';
@@ -69,6 +70,23 @@ Examples:
69
70
  .action(async (files, options) => {
70
71
  await checkCommand(process.cwd(), files, options);
71
72
  });
73
+ program
74
+ .command('scan')
75
+ .description('Run zero-config scan with auto-detected stack and existing gates')
76
+ .argument('[files...]', 'Specific files or directories to scan')
77
+ .option('--ci', 'CI mode (minimal output, non-zero exit on fail)')
78
+ .option('--json', 'Output report in JSON format')
79
+ .option('-c, --config <path>', 'Path to custom rigour.yml configuration (optional)')
80
+ .addHelpText('after', `
81
+ Examples:
82
+ $ rigour scan # Zero-config scan in current repo
83
+ $ rigour scan ./src # Scan only src
84
+ $ rigour scan --json # Machine-readable output
85
+ $ rigour scan --ci # CI-friendly output
86
+ `)
87
+ .action(async (files, options) => {
88
+ await scanCommand(process.cwd(), files, options);
89
+ });
72
90
  program
73
91
  .command('explain')
74
92
  .description('Explain the last quality gate report with actionable bullets')
@@ -0,0 +1,6 @@
1
+ export interface ScanOptions {
2
+ ci?: boolean;
3
+ json?: boolean;
4
+ config?: string;
5
+ }
6
+ export declare function scanCommand(cwd: string, files?: string[], options?: ScanOptions): Promise<void>;
@@ -0,0 +1,236 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import yaml from 'yaml';
5
+ import { globby } from 'globby';
6
+ import { GateRunner, ConfigSchema, DiscoveryService, FixPacketService, recordScore, getScoreTrend, } from '@rigour-labs/core';
7
+ // Exit codes per spec
8
+ const EXIT_PASS = 0;
9
+ const EXIT_FAIL = 1;
10
+ const EXIT_CONFIG_ERROR = 2;
11
+ const EXIT_INTERNAL_ERROR = 3;
12
+ const LANGUAGE_PATTERNS = {
13
+ 'TypeScript': ['**/*.ts', '**/*.tsx'],
14
+ 'JavaScript': ['**/*.js', '**/*.jsx', '**/*.mjs', '**/*.cjs'],
15
+ 'Python': ['**/*.py'],
16
+ 'Go': ['**/*.go'],
17
+ 'Java': ['**/*.java'],
18
+ 'Kotlin': ['**/*.kt'],
19
+ 'C#': ['**/*.cs'],
20
+ 'Ruby': ['**/*.rb', '**/*.rake'],
21
+ 'Rust': ['**/*.rs'],
22
+ };
23
+ const COMMON_IGNORE = [
24
+ '**/node_modules/**',
25
+ '**/.git/**',
26
+ '**/dist/**',
27
+ '**/build/**',
28
+ '**/.next/**',
29
+ '**/coverage/**',
30
+ '**/vendor/**',
31
+ '**/.venv/**',
32
+ '**/venv/**',
33
+ '**/target/**',
34
+ '**/.terraform/**',
35
+ '**/*.min.js',
36
+ ];
37
+ const HEADLINE_GATE_SUPPORT = {
38
+ 'hallucinated-imports': ['TypeScript', 'JavaScript', 'Python', 'Go', 'Ruby', 'C#', 'Rust', 'Java', 'Kotlin'],
39
+ 'phantom-apis': ['TypeScript', 'JavaScript', 'Python', 'Go', 'C#', 'Java', 'Kotlin'],
40
+ 'deprecated-apis': ['TypeScript', 'JavaScript', 'Python', 'Go', 'C#', 'Java', 'Kotlin'],
41
+ 'promise-safety': ['TypeScript', 'JavaScript', 'Python', 'Go', 'Ruby', 'C#'],
42
+ 'security-patterns': ['TypeScript', 'JavaScript', 'Python', 'Go', 'Java', 'Kotlin'],
43
+ 'duplication-drift': ['TypeScript', 'JavaScript', 'Python'],
44
+ 'inconsistent-error-handling': ['TypeScript', 'JavaScript'],
45
+ 'context-window-artifacts': ['TypeScript', 'JavaScript', 'Python'],
46
+ };
47
+ export async function scanCommand(cwd, files = [], options = {}) {
48
+ try {
49
+ const scanCtx = await resolveScanConfig(cwd, options);
50
+ const stackSignals = await detectStackSignals(cwd);
51
+ if (!options.ci && !options.json) {
52
+ renderScanHeader(scanCtx, stackSignals);
53
+ }
54
+ const runner = new GateRunner(scanCtx.config);
55
+ const report = await runner.run(cwd, files.length > 0 ? files : undefined);
56
+ // Write machine report and score history
57
+ const reportPath = path.join(cwd, scanCtx.config.output.report_path);
58
+ await fs.writeJson(reportPath, report, { spaces: 2 });
59
+ recordScore(cwd, report);
60
+ // Generate fix packet on failure
61
+ if (report.status === 'FAIL') {
62
+ const fixPacketService = new FixPacketService();
63
+ const fixPacket = fixPacketService.generate(report, scanCtx.config);
64
+ const fixPacketPath = path.join(cwd, 'rigour-fix-packet.json');
65
+ await fs.writeJson(fixPacketPath, fixPacket, { spaces: 2 });
66
+ }
67
+ if (options.json) {
68
+ process.stdout.write(JSON.stringify({
69
+ mode: scanCtx.mode,
70
+ preset: scanCtx.detectedPreset ?? scanCtx.config.preset,
71
+ paradigm: scanCtx.detectedParadigm ?? scanCtx.config.paradigm,
72
+ stack: stackSignals,
73
+ report,
74
+ }, null, 2) + '\n');
75
+ process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
76
+ }
77
+ if (options.ci) {
78
+ const score = report.stats.score ?? 0;
79
+ if (report.status === 'PASS') {
80
+ console.log(`PASS (${score}/100)`);
81
+ }
82
+ else {
83
+ console.log(`FAIL: ${report.failures.length} violation(s) | Score: ${score}/100`);
84
+ }
85
+ process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
86
+ }
87
+ renderScanResults(report, stackSignals, scanCtx.config.output.report_path, cwd);
88
+ process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
89
+ }
90
+ catch (error) {
91
+ if (error.name === 'ZodError') {
92
+ console.error(chalk.red('\nInvalid configuration for scan mode:'));
93
+ error.issues.forEach((issue) => {
94
+ console.error(chalk.red(` • ${issue.path.join('.')}: ${issue.message}`));
95
+ });
96
+ process.exit(EXIT_CONFIG_ERROR);
97
+ }
98
+ console.error(chalk.red(`Internal error: ${error.message}`));
99
+ process.exit(EXIT_INTERNAL_ERROR);
100
+ }
101
+ }
102
+ async function resolveScanConfig(cwd, options) {
103
+ const explicitConfig = options.config ? path.resolve(cwd, options.config) : undefined;
104
+ const defaultConfig = path.join(cwd, 'rigour.yml');
105
+ const configPath = explicitConfig || defaultConfig;
106
+ if (await fs.pathExists(configPath)) {
107
+ const configContent = await fs.readFile(configPath, 'utf-8');
108
+ const rawConfig = yaml.parse(configContent);
109
+ const config = ConfigSchema.parse(rawConfig);
110
+ return {
111
+ mode: 'existing-config',
112
+ config,
113
+ configPath,
114
+ detectedPreset: config.preset,
115
+ detectedParadigm: config.paradigm,
116
+ };
117
+ }
118
+ const discovery = new DiscoveryService();
119
+ const discovered = await discovery.discover(cwd);
120
+ return {
121
+ mode: 'auto-discovered',
122
+ config: ConfigSchema.parse(discovered.config),
123
+ detectedPreset: discovered.matches.preset?.name,
124
+ detectedParadigm: discovered.matches.paradigm?.name,
125
+ };
126
+ }
127
+ async function detectStackSignals(cwd) {
128
+ const languageChecks = await Promise.all(Object.entries(LANGUAGE_PATTERNS).map(async ([language, patterns]) => {
129
+ const matches = await globby(patterns, { cwd, gitignore: true, ignore: COMMON_IGNORE });
130
+ return { language, found: matches.length > 0 };
131
+ }));
132
+ const languages = languageChecks.filter(item => item.found).map(item => item.language);
133
+ const [dockerMatches, terraformMatches, sqlMatches] = await Promise.all([
134
+ globby(['**/Dockerfile', '**/docker-compose*.yml', '**/*.dockerfile'], { cwd, gitignore: true, ignore: COMMON_IGNORE }),
135
+ globby(['**/*.tf', '**/*.tfvars', '**/*.hcl'], { cwd, gitignore: true, ignore: COMMON_IGNORE }),
136
+ globby(['**/*.sql'], { cwd, gitignore: true, ignore: COMMON_IGNORE }),
137
+ ]);
138
+ return {
139
+ languages,
140
+ hasDocker: dockerMatches.length > 0,
141
+ hasTerraform: terraformMatches.length > 0,
142
+ hasSql: sqlMatches.length > 0,
143
+ };
144
+ }
145
+ function renderScanHeader(scanCtx, stackSignals) {
146
+ console.log(chalk.bold.cyan('\nRigour Scan'));
147
+ console.log(chalk.dim('Zero-config security and AI-drift sweep using existing Rigour gates.\n'));
148
+ const modeLabel = scanCtx.mode === 'existing-config'
149
+ ? `Using existing config: ${path.basename(scanCtx.configPath || 'rigour.yml')}`
150
+ : 'Auto-discovered config (no rigour.yml required)';
151
+ const preset = scanCtx.detectedPreset || scanCtx.config.preset || 'universal';
152
+ const paradigm = scanCtx.detectedParadigm || scanCtx.config.paradigm || 'general';
153
+ console.log(chalk.bold(`Mode:`) + ` ${modeLabel}`);
154
+ console.log(chalk.bold(`Detected profile:`) + ` preset=${preset}, paradigm=${paradigm}`);
155
+ console.log(chalk.bold(`Detected stack:`) + ` ${stackSignals.languages.join(', ') || 'No major language signatures detected'}`);
156
+ console.log('');
157
+ }
158
+ function renderScanResults(report, stackSignals, reportPath, cwd) {
159
+ const fakePackages = extractHallucinatedImports(report.failures);
160
+ if (fakePackages.length > 0) {
161
+ const unique = [...new Set(fakePackages)];
162
+ console.log(chalk.red.bold(`oh shit: ${unique.length} fake package/path import(s) detected`));
163
+ console.log(chalk.dim(`Examples: ${unique.slice(0, 5).join(', ')}${unique.length > 5 ? ', ...' : ''}`));
164
+ console.log('');
165
+ }
166
+ const statusColor = report.status === 'PASS' ? chalk.green.bold : chalk.red.bold;
167
+ const statusLabel = report.status === 'PASS' ? 'PASS' : 'FAIL';
168
+ const score = report.stats.score ?? 0;
169
+ const aiHealth = report.stats.ai_health_score ?? 0;
170
+ const structural = report.stats.structural_score ?? 0;
171
+ console.log(statusColor(`${statusLabel} | Score ${score}/100 | AI Health ${aiHealth}/100 | Structural ${structural}/100`));
172
+ const severity = report.stats.severity_breakdown || {};
173
+ const sevParts = ['critical', 'high', 'medium', 'low', 'info']
174
+ .filter(level => (severity[level] || 0) > 0)
175
+ .map(level => `${level}: ${severity[level]}`);
176
+ if (sevParts.length > 0) {
177
+ console.log(`Severity: ${sevParts.join(', ')}`);
178
+ }
179
+ renderCoverageWarnings(stackSignals);
180
+ console.log('');
181
+ if (report.status === 'FAIL') {
182
+ const topFindings = report.failures.slice(0, 8);
183
+ for (const failure of topFindings) {
184
+ const sev = (failure.severity || 'medium').toUpperCase().padEnd(8, ' ');
185
+ console.log(`${sev} [${failure.id}] ${failure.title}`);
186
+ if (failure.files && failure.files.length > 0) {
187
+ console.log(chalk.dim(` files: ${failure.files.slice(0, 3).join(', ')}`));
188
+ }
189
+ }
190
+ if (report.failures.length > topFindings.length) {
191
+ console.log(chalk.dim(`...and ${report.failures.length - topFindings.length} more findings`));
192
+ }
193
+ }
194
+ const trend = getScoreTrend(cwd);
195
+ if (trend && trend.recentScores.length >= 3) {
196
+ console.log(chalk.dim(`\nTrend: ${trend.recentScores.join(' -> ')} (${trend.direction})`));
197
+ }
198
+ console.log(chalk.yellow(`\nFull report: ${reportPath}`));
199
+ if (report.status === 'FAIL') {
200
+ console.log(chalk.yellow('Fix packet: rigour-fix-packet.json'));
201
+ }
202
+ console.log(chalk.dim(`Finished in ${report.stats.duration_ms}ms`));
203
+ }
204
+ function renderCoverageWarnings(stackSignals) {
205
+ const gaps = [];
206
+ for (const language of stackSignals.languages) {
207
+ const supportedBy = Object.entries(HEADLINE_GATE_SUPPORT)
208
+ .filter(([, languages]) => languages.includes(language))
209
+ .map(([gateId]) => gateId);
210
+ if (supportedBy.length < 3) {
211
+ gaps.push(`${language}: partial support (${supportedBy.join(', ') || 'none'})`);
212
+ }
213
+ }
214
+ if (stackSignals.hasDocker || stackSignals.hasTerraform) {
215
+ gaps.push('Infra files detected (Docker/Terraform) but no dedicated vulnerability/drift gate yet');
216
+ }
217
+ if (stackSignals.hasSql) {
218
+ gaps.push('SQL files detected but no dedicated .sql static gate yet (string-level SQL checks only)');
219
+ }
220
+ if (gaps.length > 0) {
221
+ console.log(chalk.yellow('Coverage gaps to close:'));
222
+ gaps.forEach(gap => console.log(chalk.yellow(` - ${gap}`)));
223
+ }
224
+ }
225
+ function extractHallucinatedImports(failures) {
226
+ const fakeImports = [];
227
+ for (const failure of failures) {
228
+ if (failure.id !== 'hallucinated-imports')
229
+ continue;
230
+ const matches = failure.details.matchAll(/import '([^']+)'/g);
231
+ for (const match of matches) {
232
+ fakeImports.push(match[1]);
233
+ }
234
+ }
235
+ return fakeImports;
236
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { scanCommand } from './scan.js';
6
+ describe('scanCommand', () => {
7
+ let testDir;
8
+ beforeEach(async () => {
9
+ testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-scan-test-'));
10
+ vi.spyOn(process, 'exit').mockImplementation(() => undefined);
11
+ vi.spyOn(console, 'log').mockImplementation(() => { });
12
+ vi.spyOn(console, 'error').mockImplementation(() => { });
13
+ });
14
+ afterEach(async () => {
15
+ await fs.remove(testDir);
16
+ vi.restoreAllMocks();
17
+ });
18
+ it('runs in zero-config mode without rigour.yml', async () => {
19
+ await fs.writeJson(path.join(testDir, 'package.json'), { name: 'scan-zero-config-test' });
20
+ await fs.writeFile(path.join(testDir, 'index.js'), "import fake from 'totally-fake-package';\nconsole.log(fake);\n");
21
+ await expect(scanCommand(testDir, [], {})).resolves.not.toThrow();
22
+ expect(process.exit).toHaveBeenCalled();
23
+ expect(await fs.pathExists(path.join(testDir, 'rigour-report.json'))).toBe(true);
24
+ }, 30_000);
25
+ it('uses provided config path when passed', async () => {
26
+ await fs.writeFile(path.join(testDir, 'app.js'), "export const ok = 42;\n");
27
+ await fs.writeFile(path.join(testDir, 'scan-config.yml'), `
28
+ version: 1
29
+ gates:
30
+ required_files: []
31
+ forbid_todos: false
32
+ forbid_fixme: false
33
+ context:
34
+ enabled: false
35
+ environment:
36
+ enabled: false
37
+ retry_loop_breaker:
38
+ enabled: false
39
+ security:
40
+ enabled: false
41
+ duplication_drift:
42
+ enabled: false
43
+ hallucinated_imports:
44
+ enabled: false
45
+ inconsistent_error_handling:
46
+ enabled: false
47
+ context_window_artifacts:
48
+ enabled: false
49
+ promise_safety:
50
+ enabled: false
51
+ phantom_apis:
52
+ enabled: false
53
+ deprecated_apis:
54
+ enabled: false
55
+ test_quality:
56
+ enabled: false
57
+ output:
58
+ report_path: scan-report.json
59
+ `);
60
+ await expect(scanCommand(testDir, [], { config: 'scan-config.yml' })).resolves.not.toThrow();
61
+ expect(process.exit).toHaveBeenCalledWith(0);
62
+ expect(await fs.pathExists(path.join(testDir, 'scan-report.json'))).toBe(true);
63
+ }, 30_000);
64
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/cli",
3
- "version": "3.0.3",
3
+ "version": "3.0.5",
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.3"
47
+ "@rigour-labs/core": "3.0.5"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/fs-extra": "^11.0.4",