@aiready/cli 0.9.27 ā 0.9.28
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 +423 -377
- package/dist/cli.mjs +442 -374
- package/package.json +3 -3
- package/src/cli.ts +49 -1284
- package/src/commands/consistency.ts +192 -0
- package/src/commands/context.ts +192 -0
- package/src/commands/index.ts +9 -0
- package/src/commands/patterns.ts +179 -0
- package/src/commands/scan.ts +455 -0
- package/src/commands/visualize.ts +253 -0
- package/src/utils/helpers.ts +133 -0
- package/tsconfig.json +5 -2
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan command - Run comprehensive AI-readiness analysis (patterns + context + consistency)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { writeFileSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { resolve as resolvePath } from 'path';
|
|
9
|
+
import {
|
|
10
|
+
loadMergedConfig,
|
|
11
|
+
handleJSONOutput,
|
|
12
|
+
handleCLIError,
|
|
13
|
+
getElapsedTime,
|
|
14
|
+
resolveOutputPath,
|
|
15
|
+
calculateOverallScore,
|
|
16
|
+
formatScore,
|
|
17
|
+
formatToolScore,
|
|
18
|
+
getRating,
|
|
19
|
+
getRatingDisplay,
|
|
20
|
+
parseWeightString,
|
|
21
|
+
type ToolScoringOutput,
|
|
22
|
+
} from '@aiready/core';
|
|
23
|
+
import { analyzeUnified } from '../index';
|
|
24
|
+
import { getReportTimestamp, warnIfGraphCapExceeded, truncateArray } from '../utils/helpers';
|
|
25
|
+
|
|
26
|
+
interface ScanOptions {
|
|
27
|
+
tools?: string;
|
|
28
|
+
include?: string;
|
|
29
|
+
exclude?: string;
|
|
30
|
+
output?: string;
|
|
31
|
+
outputFile?: string;
|
|
32
|
+
score?: boolean;
|
|
33
|
+
noScore?: boolean;
|
|
34
|
+
weights?: string;
|
|
35
|
+
threshold?: string;
|
|
36
|
+
ci?: boolean;
|
|
37
|
+
failOn?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function scanAction(directory: string, options: ScanOptions) {
|
|
41
|
+
console.log(chalk.blue('š Starting AIReady unified analysis...\n'));
|
|
42
|
+
|
|
43
|
+
const startTime = Date.now();
|
|
44
|
+
// Resolve directory to absolute path to ensure .aiready/ is created in the right location
|
|
45
|
+
const resolvedDir = resolvePath(process.cwd(), directory || '.');
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Define defaults
|
|
49
|
+
const defaults = {
|
|
50
|
+
tools: ['patterns', 'context', 'consistency'],
|
|
51
|
+
include: undefined,
|
|
52
|
+
exclude: undefined,
|
|
53
|
+
output: {
|
|
54
|
+
format: 'json',
|
|
55
|
+
file: undefined,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Load and merge config with CLI options
|
|
60
|
+
const baseOptions = await loadMergedConfig(resolvedDir, defaults, {
|
|
61
|
+
tools: options.tools ? options.tools.split(',').map((t: string) => t.trim()) as ('patterns' | 'context' | 'consistency')[] : undefined,
|
|
62
|
+
include: options.include?.split(','),
|
|
63
|
+
exclude: options.exclude?.split(','),
|
|
64
|
+
}) as any;
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
// Apply smart defaults for pattern detection if patterns tool is enabled
|
|
68
|
+
let finalOptions = { ...baseOptions };
|
|
69
|
+
if (baseOptions.tools.includes('patterns')) {
|
|
70
|
+
const { getSmartDefaults } = await import('@aiready/pattern-detect');
|
|
71
|
+
const patternSmartDefaults = await getSmartDefaults(resolvedDir, baseOptions);
|
|
72
|
+
// Merge deeply to preserve nested config
|
|
73
|
+
finalOptions = { ...patternSmartDefaults, ...finalOptions, ...baseOptions };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Print pre-run summary with expanded settings (truncate long arrays)
|
|
77
|
+
console.log(chalk.cyan('\n=== AIReady Run Preview ==='));
|
|
78
|
+
console.log(chalk.white('Tools to run:'), (finalOptions.tools || ['patterns', 'context', 'consistency']).join(', '));
|
|
79
|
+
console.log(chalk.white('Will use settings from config and defaults.'));
|
|
80
|
+
|
|
81
|
+
// Common top-level settings
|
|
82
|
+
console.log(chalk.white('\nGeneral settings:'));
|
|
83
|
+
if (finalOptions.rootDir) console.log(` rootDir: ${chalk.bold(String(finalOptions.rootDir))}`);
|
|
84
|
+
if (finalOptions.include) console.log(` include: ${chalk.bold(truncateArray(finalOptions.include, 6))}`);
|
|
85
|
+
if (finalOptions.exclude) console.log(` exclude: ${chalk.bold(truncateArray(finalOptions.exclude, 6))}`);
|
|
86
|
+
|
|
87
|
+
if (finalOptions['pattern-detect'] || finalOptions.minSimilarity) {
|
|
88
|
+
const patternDetectConfig = finalOptions['pattern-detect'] || {
|
|
89
|
+
minSimilarity: finalOptions.minSimilarity,
|
|
90
|
+
minLines: finalOptions.minLines,
|
|
91
|
+
approx: finalOptions.approx,
|
|
92
|
+
minSharedTokens: finalOptions.minSharedTokens,
|
|
93
|
+
maxCandidatesPerBlock: finalOptions.maxCandidatesPerBlock,
|
|
94
|
+
batchSize: finalOptions.batchSize,
|
|
95
|
+
streamResults: finalOptions.streamResults,
|
|
96
|
+
severity: (finalOptions as any).severity,
|
|
97
|
+
includeTests: (finalOptions as any).includeTests,
|
|
98
|
+
};
|
|
99
|
+
console.log(chalk.white('\nPattern-detect settings:'));
|
|
100
|
+
console.log(` minSimilarity: ${chalk.bold(patternDetectConfig.minSimilarity ?? 'default')}`);
|
|
101
|
+
console.log(` minLines: ${chalk.bold(patternDetectConfig.minLines ?? 'default')}`);
|
|
102
|
+
if (patternDetectConfig.approx !== undefined) console.log(` approx: ${chalk.bold(String(patternDetectConfig.approx))}`);
|
|
103
|
+
if (patternDetectConfig.minSharedTokens !== undefined) console.log(` minSharedTokens: ${chalk.bold(String(patternDetectConfig.minSharedTokens))}`);
|
|
104
|
+
if (patternDetectConfig.maxCandidatesPerBlock !== undefined) console.log(` maxCandidatesPerBlock: ${chalk.bold(String(patternDetectConfig.maxCandidatesPerBlock))}`);
|
|
105
|
+
if (patternDetectConfig.batchSize !== undefined) console.log(` batchSize: ${chalk.bold(String(patternDetectConfig.batchSize))}`);
|
|
106
|
+
if (patternDetectConfig.streamResults !== undefined) console.log(` streamResults: ${chalk.bold(String(patternDetectConfig.streamResults))}`);
|
|
107
|
+
if (patternDetectConfig.severity !== undefined) console.log(` severity: ${chalk.bold(String(patternDetectConfig.severity))}`);
|
|
108
|
+
if (patternDetectConfig.includeTests !== undefined) console.log(` includeTests: ${chalk.bold(String(patternDetectConfig.includeTests))}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (finalOptions['context-analyzer'] || finalOptions.maxDepth) {
|
|
112
|
+
const ca = finalOptions['context-analyzer'] || {
|
|
113
|
+
maxDepth: finalOptions.maxDepth,
|
|
114
|
+
maxContextBudget: finalOptions.maxContextBudget,
|
|
115
|
+
minCohesion: (finalOptions as any).minCohesion,
|
|
116
|
+
maxFragmentation: (finalOptions as any).maxFragmentation,
|
|
117
|
+
includeNodeModules: (finalOptions as any).includeNodeModules,
|
|
118
|
+
};
|
|
119
|
+
console.log(chalk.white('\nContext-analyzer settings:'));
|
|
120
|
+
console.log(` maxDepth: ${chalk.bold(ca.maxDepth ?? 'default')}`);
|
|
121
|
+
console.log(` maxContextBudget: ${chalk.bold(ca.maxContextBudget ?? 'default')}`);
|
|
122
|
+
if (ca.minCohesion !== undefined) console.log(` minCohesion: ${chalk.bold(String(ca.minCohesion))}`);
|
|
123
|
+
if (ca.maxFragmentation !== undefined) console.log(` maxFragmentation: ${chalk.bold(String(ca.maxFragmentation))}`);
|
|
124
|
+
if (ca.includeNodeModules !== undefined) console.log(` includeNodeModules: ${chalk.bold(String(ca.includeNodeModules))}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (finalOptions.consistency) {
|
|
128
|
+
const c = finalOptions.consistency;
|
|
129
|
+
console.log(chalk.white('\nConsistency settings:'));
|
|
130
|
+
console.log(` checkNaming: ${chalk.bold(String(c.checkNaming ?? true))}`);
|
|
131
|
+
console.log(` checkPatterns: ${chalk.bold(String(c.checkPatterns ?? true))}`);
|
|
132
|
+
console.log(` checkArchitecture: ${chalk.bold(String(c.checkArchitecture ?? false))}`);
|
|
133
|
+
if (c.minSeverity) console.log(` minSeverity: ${chalk.bold(c.minSeverity)}`);
|
|
134
|
+
if (c.acceptedAbbreviations) console.log(` acceptedAbbreviations: ${chalk.bold(truncateArray(c.acceptedAbbreviations, 8))}`);
|
|
135
|
+
if (c.shortWords) console.log(` shortWords: ${chalk.bold(truncateArray(c.shortWords, 8))}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(chalk.white('\nStarting analysis...'));
|
|
139
|
+
|
|
140
|
+
// Progress callback to surface per-tool output as each tool finishes
|
|
141
|
+
const progressCallback = (event: { tool: string; data: any }) => {
|
|
142
|
+
console.log(chalk.cyan(`\n--- ${event.tool.toUpperCase()} RESULTS ---`));
|
|
143
|
+
try {
|
|
144
|
+
if (event.tool === 'patterns') {
|
|
145
|
+
const pr = event.data as any;
|
|
146
|
+
console.log(` Duplicate patterns: ${chalk.bold(String(pr.duplicates?.length || 0))}`);
|
|
147
|
+
console.log(` Files with pattern issues: ${chalk.bold(String(pr.results?.length || 0))}`);
|
|
148
|
+
// show top duplicate summaries
|
|
149
|
+
if (pr.duplicates && pr.duplicates.length > 0) {
|
|
150
|
+
pr.duplicates.slice(0, 5).forEach((d: any, i: number) => {
|
|
151
|
+
console.log(` ${i + 1}. ${d.file1.split('/').pop()} ā ${d.file2.split('/').pop()} (sim=${(d.similarity * 100).toFixed(1)}%)`);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// show top files with pattern issues (sorted by issue count desc)
|
|
156
|
+
if (pr.results && pr.results.length > 0) {
|
|
157
|
+
console.log(` Top files with pattern issues:`);
|
|
158
|
+
const sortedByIssues = [...pr.results].sort((a: any, b: any) => (b.issues?.length || 0) - (a.issues?.length || 0));
|
|
159
|
+
sortedByIssues.slice(0, 5).forEach((r: any, i: number) => {
|
|
160
|
+
console.log(` ${i + 1}. ${r.fileName.split('/').pop()} - ${r.issues.length} issue(s)`);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Grouping and clusters summary (if available) ā show after detailed findings
|
|
165
|
+
if (pr.groups && pr.groups.length >= 0) {
|
|
166
|
+
console.log(` ā
Grouped ${chalk.bold(String(pr.duplicates?.length || 0))} duplicates into ${chalk.bold(String(pr.groups.length))} file pairs`);
|
|
167
|
+
}
|
|
168
|
+
if (pr.clusters && pr.clusters.length >= 0) {
|
|
169
|
+
console.log(` ā
Created ${chalk.bold(String(pr.clusters.length))} refactor clusters`);
|
|
170
|
+
// show brief cluster summaries
|
|
171
|
+
pr.clusters.slice(0, 3).forEach((cl: any, idx: number) => {
|
|
172
|
+
const files = (cl.files || []).map((f: any) => f.path.split('/').pop()).join(', ');
|
|
173
|
+
console.log(` ${idx + 1}. ${files} (${cl.tokenCost || 'n/a'} tokens)`);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
} else if (event.tool === 'context') {
|
|
177
|
+
const cr = event.data as any[];
|
|
178
|
+
console.log(` Context issues found: ${chalk.bold(String(cr.length || 0))}`);
|
|
179
|
+
cr.slice(0, 5).forEach((c: any, i: number) => {
|
|
180
|
+
const msg = c.message ? ` - ${c.message}` : '';
|
|
181
|
+
console.log(` ${i + 1}. ${c.file} (${c.severity || 'n/a'})${msg}`);
|
|
182
|
+
});
|
|
183
|
+
} else if (event.tool === 'consistency') {
|
|
184
|
+
const rep = event.data as any;
|
|
185
|
+
console.log(` Consistency totalIssues: ${chalk.bold(String(rep.summary?.totalIssues || 0))}`);
|
|
186
|
+
|
|
187
|
+
if (rep.results && rep.results.length > 0) {
|
|
188
|
+
// Group issues by file
|
|
189
|
+
const fileMap = new Map<string, any[]>();
|
|
190
|
+
rep.results.forEach((r: any) => {
|
|
191
|
+
(r.issues || []).forEach((issue: any) => {
|
|
192
|
+
const file = issue.location?.file || r.file || 'unknown';
|
|
193
|
+
if (!fileMap.has(file)) fileMap.set(file, []);
|
|
194
|
+
fileMap.get(file)!.push(issue);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Sort files by number of issues desc
|
|
199
|
+
const files = Array.from(fileMap.entries()).sort((a, b) => b[1].length - a[1].length);
|
|
200
|
+
const topFiles = files.slice(0, 10);
|
|
201
|
+
|
|
202
|
+
topFiles.forEach(([file, issues], idx) => {
|
|
203
|
+
// Count severities
|
|
204
|
+
const counts = issues.reduce((acc: any, it: any) => {
|
|
205
|
+
const s = (it.severity || 'info').toLowerCase();
|
|
206
|
+
acc[s] = (acc[s] || 0) + 1;
|
|
207
|
+
return acc;
|
|
208
|
+
}, {} as Record<string, number>);
|
|
209
|
+
|
|
210
|
+
const sample = issues.find((it: any) => it.severity === 'critical' || it.severity === 'major') || issues[0];
|
|
211
|
+
const sampleMsg = sample ? ` ā ${sample.message}` : '';
|
|
212
|
+
|
|
213
|
+
console.log(` ${idx + 1}. ${file} ā ${issues.length} issue(s) (critical:${counts.critical||0} major:${counts.major||0} minor:${counts.minor||0} info:${counts.info||0})${sampleMsg}`);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const remaining = files.length - topFiles.length;
|
|
217
|
+
if (remaining > 0) {
|
|
218
|
+
console.log(chalk.dim(` ... and ${remaining} more files with issues (use --output json for full details)`));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} catch (err) {
|
|
223
|
+
// don't crash the run for progress printing errors
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const results = await analyzeUnified({ ...finalOptions, progressCallback, suppressToolConfig: true });
|
|
228
|
+
|
|
229
|
+
// Summarize tools and results to console
|
|
230
|
+
console.log(chalk.cyan('\n=== AIReady Run Summary ==='));
|
|
231
|
+
console.log(chalk.white('Tools run:'), (finalOptions.tools || ['patterns', 'context', 'consistency']).join(', '));
|
|
232
|
+
|
|
233
|
+
// Results summary
|
|
234
|
+
console.log(chalk.cyan('\nResults summary:'));
|
|
235
|
+
console.log(` Total issues (all tools): ${chalk.bold(String(results.summary.totalIssues || 0))}`);
|
|
236
|
+
if (results.duplicates) console.log(` Duplicate patterns found: ${chalk.bold(String(results.duplicates.length || 0))}`);
|
|
237
|
+
if (results.patterns) console.log(` Pattern files with issues: ${chalk.bold(String(results.patterns.length || 0))}`);
|
|
238
|
+
if (results.context) console.log(` Context issues: ${chalk.bold(String(results.context.length || 0))}`);
|
|
239
|
+
if (results.consistency) console.log(` Consistency issues: ${chalk.bold(String(results.consistency.summary.totalIssues || 0))}`);
|
|
240
|
+
console.log(chalk.cyan('===========================\n'));
|
|
241
|
+
|
|
242
|
+
const elapsedTime = getElapsedTime(startTime);
|
|
243
|
+
|
|
244
|
+
// Calculate score if requested: assemble per-tool scoring outputs
|
|
245
|
+
let scoringResult: ReturnType<typeof calculateOverallScore> | undefined;
|
|
246
|
+
if (options.score || finalOptions.scoring?.showBreakdown) {
|
|
247
|
+
const toolScores: Map<string, ToolScoringOutput> = new Map();
|
|
248
|
+
|
|
249
|
+
// Patterns score
|
|
250
|
+
if (results.duplicates) {
|
|
251
|
+
const { calculatePatternScore } = await import('@aiready/pattern-detect');
|
|
252
|
+
try {
|
|
253
|
+
const patternScore = calculatePatternScore(results.duplicates, results.patterns?.length || 0);
|
|
254
|
+
toolScores.set('pattern-detect', patternScore);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
// ignore scoring failures for a single tool
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Context score
|
|
261
|
+
if (results.context) {
|
|
262
|
+
const { generateSummary: genContextSummary, calculateContextScore } = await import('@aiready/context-analyzer');
|
|
263
|
+
try {
|
|
264
|
+
const ctxSummary = genContextSummary(results.context);
|
|
265
|
+
const contextScore = calculateContextScore(ctxSummary);
|
|
266
|
+
toolScores.set('context-analyzer', contextScore);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
// ignore
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Consistency score
|
|
273
|
+
if (results.consistency) {
|
|
274
|
+
const { calculateConsistencyScore } = await import('@aiready/consistency');
|
|
275
|
+
try {
|
|
276
|
+
const issues = results.consistency.results?.flatMap((r: any) => r.issues) || [];
|
|
277
|
+
const totalFiles = results.consistency.summary?.filesAnalyzed || 0;
|
|
278
|
+
const consistencyScore = calculateConsistencyScore(issues, totalFiles);
|
|
279
|
+
toolScores.set('consistency', consistencyScore);
|
|
280
|
+
} catch (err) {
|
|
281
|
+
// ignore
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Parse CLI weight overrides (if any)
|
|
286
|
+
const cliWeights = parseWeightString((options as any).weights);
|
|
287
|
+
|
|
288
|
+
// Only calculate overall score if we have at least one tool score
|
|
289
|
+
if (toolScores.size > 0) {
|
|
290
|
+
scoringResult = calculateOverallScore(toolScores, finalOptions, cliWeights.size ? cliWeights : undefined);
|
|
291
|
+
|
|
292
|
+
console.log(chalk.bold('\nš AI Readiness Overall Score'));
|
|
293
|
+
console.log(` ${formatScore(scoringResult)}`);
|
|
294
|
+
|
|
295
|
+
// Show concise breakdown; detailed breakdown only if config requests it
|
|
296
|
+
if (scoringResult.breakdown && scoringResult.breakdown.length > 0) {
|
|
297
|
+
console.log(chalk.bold('\nTool breakdown:'));
|
|
298
|
+
scoringResult.breakdown.forEach((tool) => {
|
|
299
|
+
const rating = getRating(tool.score);
|
|
300
|
+
const rd = getRatingDisplay(rating);
|
|
301
|
+
console.log(` - ${tool.toolName}: ${tool.score}/100 (${rating}) ${rd.emoji}`);
|
|
302
|
+
});
|
|
303
|
+
console.log();
|
|
304
|
+
|
|
305
|
+
if (finalOptions.scoring?.showBreakdown) {
|
|
306
|
+
console.log(chalk.bold('Detailed tool breakdown:'));
|
|
307
|
+
scoringResult.breakdown.forEach((tool) => {
|
|
308
|
+
console.log(formatToolScore(tool));
|
|
309
|
+
});
|
|
310
|
+
console.log();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Persist JSON summary when output format is json
|
|
317
|
+
const outputFormat = options.output || finalOptions.output?.format || 'console';
|
|
318
|
+
const userOutputFile = options.outputFile || finalOptions.output?.file;
|
|
319
|
+
if (outputFormat === 'json') {
|
|
320
|
+
const timestamp = getReportTimestamp();
|
|
321
|
+
const defaultFilename = `aiready-report-${timestamp}.json`;
|
|
322
|
+
const outputPath = resolveOutputPath(userOutputFile, defaultFilename, resolvedDir);
|
|
323
|
+
const outputData = { ...results, scoring: scoringResult };
|
|
324
|
+
handleJSONOutput(outputData, outputPath, `ā
Report saved to ${outputPath}`);
|
|
325
|
+
|
|
326
|
+
// Warn if graph caps may be exceeded
|
|
327
|
+
warnIfGraphCapExceeded(outputData, resolvedDir);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// CI/CD Gatekeeper Mode
|
|
331
|
+
const isCI = options.ci || process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
|
|
332
|
+
if (isCI && scoringResult) {
|
|
333
|
+
const threshold = options.threshold ? parseInt(options.threshold) : undefined;
|
|
334
|
+
const failOnLevel = options.failOn || 'critical';
|
|
335
|
+
|
|
336
|
+
// Output GitHub Actions annotations
|
|
337
|
+
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
338
|
+
console.log(`\n::group::AI Readiness Score`);
|
|
339
|
+
console.log(`score=${scoringResult.overallScore}`);
|
|
340
|
+
if (scoringResult.breakdown) {
|
|
341
|
+
scoringResult.breakdown.forEach(tool => {
|
|
342
|
+
console.log(`${tool.toolName}=${tool.score}`);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
console.log('::endgroup::');
|
|
346
|
+
|
|
347
|
+
// Output annotation for score
|
|
348
|
+
if (threshold && scoringResult.overallScore < threshold) {
|
|
349
|
+
console.log(`::error::AI Readiness Score ${scoringResult.overallScore} is below threshold ${threshold}`);
|
|
350
|
+
} else if (threshold) {
|
|
351
|
+
console.log(`::notice::AI Readiness Score: ${scoringResult.overallScore}/100 (threshold: ${threshold})`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Output annotations for critical issues
|
|
355
|
+
if (results.patterns) {
|
|
356
|
+
const criticalPatterns = results.patterns.flatMap((p: any) =>
|
|
357
|
+
p.issues.filter((i: any) => i.severity === 'critical')
|
|
358
|
+
);
|
|
359
|
+
criticalPatterns.slice(0, 10).forEach((issue: any) => {
|
|
360
|
+
console.log(`::warning file=${issue.location?.file || 'unknown'},line=${issue.location?.line || 1}::${issue.message}`);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Determine if we should fail
|
|
366
|
+
let shouldFail = false;
|
|
367
|
+
let failReason = '';
|
|
368
|
+
|
|
369
|
+
// Check threshold
|
|
370
|
+
if (threshold && scoringResult.overallScore < threshold) {
|
|
371
|
+
shouldFail = true;
|
|
372
|
+
failReason = `AI Readiness Score ${scoringResult.overallScore} is below threshold ${threshold}`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Check fail-on severity
|
|
376
|
+
if (failOnLevel !== 'none') {
|
|
377
|
+
const severityLevels = { critical: 4, major: 3, minor: 2, any: 1 };
|
|
378
|
+
const minSeverity = severityLevels[failOnLevel as keyof typeof severityLevels] || 4;
|
|
379
|
+
|
|
380
|
+
let criticalCount = 0;
|
|
381
|
+
let majorCount = 0;
|
|
382
|
+
|
|
383
|
+
if (results.patterns) {
|
|
384
|
+
results.patterns.forEach((p: any) => {
|
|
385
|
+
p.issues.forEach((i: any) => {
|
|
386
|
+
if (i.severity === 'critical') criticalCount++;
|
|
387
|
+
if (i.severity === 'major') majorCount++;
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
if (results.context) {
|
|
392
|
+
results.context.forEach((c: any) => {
|
|
393
|
+
if (c.severity === 'critical') criticalCount++;
|
|
394
|
+
if (c.severity === 'major') majorCount++;
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
if (results.consistency?.results) {
|
|
398
|
+
results.consistency.results.forEach((r: any) => {
|
|
399
|
+
r.issues?.forEach((i: any) => {
|
|
400
|
+
if (i.severity === 'critical') criticalCount++;
|
|
401
|
+
if (i.severity === 'major') majorCount++;
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (minSeverity >= 4 && criticalCount > 0) {
|
|
407
|
+
shouldFail = true;
|
|
408
|
+
failReason = `Found ${criticalCount} critical issues`;
|
|
409
|
+
} else if (minSeverity >= 3 && (criticalCount + majorCount) > 0) {
|
|
410
|
+
shouldFail = true;
|
|
411
|
+
failReason = `Found ${criticalCount} critical and ${majorCount} major issues`;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Output result
|
|
416
|
+
if (shouldFail) {
|
|
417
|
+
console.log(chalk.red('\nš« PR BLOCKED: AI Readiness Check Failed'));
|
|
418
|
+
console.log(chalk.red(` Reason: ${failReason}`));
|
|
419
|
+
console.log(chalk.dim('\n Remediation steps:'));
|
|
420
|
+
console.log(chalk.dim(' 1. Run `aiready scan` locally to see detailed issues'));
|
|
421
|
+
console.log(chalk.dim(' 2. Fix the critical issues before merging'));
|
|
422
|
+
console.log(chalk.dim(' 3. Consider upgrading to Team plan for historical tracking: https://getaiready.dev/pricing'));
|
|
423
|
+
process.exit(1);
|
|
424
|
+
} else {
|
|
425
|
+
console.log(chalk.green('\nā
PR PASSED: AI Readiness Check'));
|
|
426
|
+
if (threshold) {
|
|
427
|
+
console.log(chalk.green(` Score: ${scoringResult.overallScore}/100 (threshold: ${threshold})`));
|
|
428
|
+
}
|
|
429
|
+
console.log(chalk.dim('\n š” Track historical trends: https://getaiready.dev ā Team plan $99/mo'));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
} catch (error) {
|
|
433
|
+
handleCLIError(error, 'Analysis');
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export const scanHelpText = `
|
|
438
|
+
EXAMPLES:
|
|
439
|
+
$ aiready scan # Analyze all tools
|
|
440
|
+
$ aiready scan --tools patterns,context # Skip consistency
|
|
441
|
+
$ aiready scan --score --threshold 75 # CI/CD with threshold
|
|
442
|
+
$ aiready scan --ci --threshold 70 # GitHub Actions gatekeeper
|
|
443
|
+
$ aiready scan --ci --fail-on major # Fail on major+ issues
|
|
444
|
+
$ aiready scan --output json --output-file report.json
|
|
445
|
+
|
|
446
|
+
CI/CD INTEGRATION (Gatekeeper Mode):
|
|
447
|
+
Use --ci for GitHub Actions integration:
|
|
448
|
+
- Outputs GitHub Actions annotations for PR checks
|
|
449
|
+
- Fails with exit code 1 if threshold not met
|
|
450
|
+
- Shows clear "blocked" message with remediation steps
|
|
451
|
+
|
|
452
|
+
Example GitHub Actions workflow:
|
|
453
|
+
- name: AI Readiness Check
|
|
454
|
+
run: aiready scan --ci --threshold 70
|
|
455
|
+
`;
|