@aiready/cli 0.10.6 ā 0.12.1
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/.turbo/turbo-build.log +8 -8
- package/.turbo/turbo-lint.log +9 -1
- package/.turbo/turbo-test.log +43 -4
- package/dist/chunk-LTUQDJPO.mjs +229 -0
- package/dist/chunk-N56YAZVN.mjs +194 -0
- package/dist/chunk-YBZKPKW3.mjs +161 -0
- package/dist/cli.js +237 -910
- package/dist/cli.mjs +92 -584
- package/dist/index.js +149 -369
- package/dist/index.mjs +1 -1
- package/package.json +12 -12
- package/src/.aiready/aiready-report-20260307-092852.json +50609 -0
- package/src/.aiready/aiready-report-20260307-094301.json +50609 -0
- package/src/.aiready/aiready-report-20260307-094831.json +50609 -0
- package/src/.aiready/aiready-report-20260307-100539.json +50609 -0
- package/src/.aiready/aiready-report-20260307-103508.json +51329 -0
- package/src/__tests__/cli.test.ts +55 -29
- package/src/commands/scan.ts +116 -700
- package/src/index.ts +191 -455
- package/dist/chunk-EQ2HQSTJ.mjs +0 -414
- package/dist/chunk-R3O7QPKD.mjs +0 -419
- package/dist/chunk-VQCWYJYJ.mjs +0 -438
- package/dist/chunk-VUCNUYI7.mjs +0 -417
package/src/commands/scan.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Scan command - Run comprehensive AI-readiness analysis
|
|
2
|
+
* Scan command - Run comprehensive AI-readiness analysis using the tool registry
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import { writeFileSync, readFileSync } from 'fs';
|
|
7
|
-
// 'join' was unused
|
|
8
7
|
import { resolve as resolvePath } from 'path';
|
|
9
8
|
import {
|
|
10
9
|
loadMergedConfig,
|
|
@@ -12,7 +11,6 @@ import {
|
|
|
12
11
|
handleCLIError,
|
|
13
12
|
getElapsedTime,
|
|
14
13
|
resolveOutputPath,
|
|
15
|
-
calculateOverallScore,
|
|
16
14
|
formatScore,
|
|
17
15
|
formatToolScore,
|
|
18
16
|
calculateTokenBudget,
|
|
@@ -20,12 +18,11 @@ import {
|
|
|
20
18
|
getModelPreset,
|
|
21
19
|
getRating,
|
|
22
20
|
getRatingDisplay,
|
|
23
|
-
parseWeightString,
|
|
24
21
|
getRepoMetadata,
|
|
25
22
|
Severity,
|
|
26
23
|
IssueType,
|
|
27
24
|
ToolName,
|
|
28
|
-
|
|
25
|
+
ToolRegistry,
|
|
29
26
|
} from '@aiready/core';
|
|
30
27
|
import { analyzeUnified, scoreUnified, type ScoringResult } from '../index';
|
|
31
28
|
import {
|
|
@@ -59,24 +56,21 @@ export async function scanAction(directory: string, options: ScanOptions) {
|
|
|
59
56
|
console.log(chalk.blue('š Starting AIReady unified analysis...\n'));
|
|
60
57
|
|
|
61
58
|
const startTime = Date.now();
|
|
62
|
-
// Resolve directory to absolute path to ensure .aiready/ is created in the right location
|
|
63
59
|
const resolvedDir = resolvePath(process.cwd(), directory || '.');
|
|
64
|
-
|
|
65
|
-
// Extract repo metadata for linkage
|
|
66
60
|
const repoMetadata = getRepoMetadata(resolvedDir);
|
|
67
61
|
|
|
68
62
|
try {
|
|
69
|
-
// Define defaults
|
|
63
|
+
// Define defaults using canonical IDs
|
|
70
64
|
const defaults = {
|
|
71
65
|
tools: [
|
|
72
|
-
'
|
|
73
|
-
'context',
|
|
74
|
-
'consistency',
|
|
66
|
+
'pattern-detect',
|
|
67
|
+
'context-analyzer',
|
|
68
|
+
'naming-consistency',
|
|
75
69
|
'ai-signal-clarity',
|
|
76
70
|
'agent-grounding',
|
|
77
|
-
'testability',
|
|
71
|
+
'testability-index',
|
|
78
72
|
'doc-drift',
|
|
79
|
-
'
|
|
73
|
+
'dependency-health',
|
|
80
74
|
'change-amplification',
|
|
81
75
|
],
|
|
82
76
|
include: undefined,
|
|
@@ -87,49 +81,49 @@ export async function scanAction(directory: string, options: ScanOptions) {
|
|
|
87
81
|
},
|
|
88
82
|
};
|
|
89
83
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (tool === 'hallucination' || tool === 'hallucination-risk')
|
|
94
|
-
return 'aiSignalClarity';
|
|
95
|
-
return tool;
|
|
96
|
-
})
|
|
84
|
+
// Map profile to tool IDs
|
|
85
|
+
let profileTools: string[] | undefined = options.tools
|
|
86
|
+
? options.tools.split(',').map((t) => t.trim())
|
|
97
87
|
: undefined;
|
|
98
88
|
if (options.profile) {
|
|
99
89
|
switch (options.profile.toLowerCase()) {
|
|
100
90
|
case 'agentic':
|
|
101
91
|
profileTools = [
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
92
|
+
ToolName.AiSignalClarity,
|
|
93
|
+
ToolName.AgentGrounding,
|
|
94
|
+
ToolName.TestabilityIndex,
|
|
105
95
|
];
|
|
106
96
|
break;
|
|
107
97
|
case 'cost':
|
|
108
|
-
profileTools = [
|
|
98
|
+
profileTools = [ToolName.PatternDetect, ToolName.ContextAnalyzer];
|
|
109
99
|
break;
|
|
110
100
|
case 'security':
|
|
111
|
-
profileTools = [
|
|
101
|
+
profileTools = [
|
|
102
|
+
ToolName.NamingConsistency,
|
|
103
|
+
ToolName.TestabilityIndex,
|
|
104
|
+
];
|
|
112
105
|
break;
|
|
113
106
|
case 'onboarding':
|
|
114
|
-
profileTools = [
|
|
107
|
+
profileTools = [
|
|
108
|
+
ToolName.ContextAnalyzer,
|
|
109
|
+
ToolName.NamingConsistency,
|
|
110
|
+
ToolName.AgentGrounding,
|
|
111
|
+
];
|
|
115
112
|
break;
|
|
116
113
|
default:
|
|
117
114
|
console.log(
|
|
118
115
|
chalk.yellow(
|
|
119
|
-
`\nā ļø Unknown profile '${options.profile}'. Using
|
|
116
|
+
`\nā ļø Unknown profile '${options.profile}'. Using defaults.`
|
|
120
117
|
)
|
|
121
118
|
);
|
|
122
119
|
}
|
|
123
120
|
}
|
|
124
121
|
|
|
125
|
-
// Load and merge config with CLI options
|
|
126
122
|
const cliOverrides: any = {
|
|
127
123
|
include: options.include?.split(','),
|
|
128
124
|
exclude: options.exclude?.split(','),
|
|
129
125
|
};
|
|
130
|
-
if (profileTools)
|
|
131
|
-
cliOverrides.tools = profileTools;
|
|
132
|
-
}
|
|
126
|
+
if (profileTools) cliOverrides.tools = profileTools;
|
|
133
127
|
|
|
134
128
|
const baseOptions = (await loadMergedConfig(
|
|
135
129
|
resolvedDir,
|
|
@@ -137,15 +131,17 @@ export async function scanAction(directory: string, options: ScanOptions) {
|
|
|
137
131
|
cliOverrides
|
|
138
132
|
)) as any;
|
|
139
133
|
|
|
140
|
-
// Apply smart defaults for pattern detection if
|
|
134
|
+
// Apply smart defaults for pattern detection if requested
|
|
141
135
|
let finalOptions = { ...baseOptions };
|
|
142
|
-
if (
|
|
136
|
+
if (
|
|
137
|
+
baseOptions.tools.includes(ToolName.PatternDetect) ||
|
|
138
|
+
baseOptions.tools.includes('patterns')
|
|
139
|
+
) {
|
|
143
140
|
const { getSmartDefaults } = await import('@aiready/pattern-detect');
|
|
144
141
|
const patternSmartDefaults = await getSmartDefaults(
|
|
145
142
|
resolvedDir,
|
|
146
143
|
baseOptions
|
|
147
144
|
);
|
|
148
|
-
// Merge deeply to preserve nested config
|
|
149
145
|
finalOptions = {
|
|
150
146
|
...patternSmartDefaults,
|
|
151
147
|
...finalOptions,
|
|
@@ -153,291 +149,33 @@ export async function scanAction(directory: string, options: ScanOptions) {
|
|
|
153
149
|
};
|
|
154
150
|
}
|
|
155
151
|
|
|
156
|
-
// Print pre-run summary with expanded settings (truncate long arrays)
|
|
157
152
|
console.log(chalk.cyan('\n=== AIReady Run Preview ==='));
|
|
158
153
|
console.log(
|
|
159
154
|
chalk.white('Tools to run:'),
|
|
160
|
-
(finalOptions.tools || [
|
|
155
|
+
(finalOptions.tools || []).join(', ')
|
|
161
156
|
);
|
|
162
|
-
console.log(chalk.white('Will use settings from config and defaults.'));
|
|
163
|
-
|
|
164
|
-
// Common top-level settings
|
|
165
|
-
console.log(chalk.white('\nGeneral settings:'));
|
|
166
|
-
if (finalOptions.rootDir)
|
|
167
|
-
console.log(` rootDir: ${chalk.bold(String(finalOptions.rootDir))}`);
|
|
168
|
-
if (finalOptions.include)
|
|
169
|
-
console.log(
|
|
170
|
-
` include: ${chalk.bold(truncateArray(finalOptions.include, 6))}`
|
|
171
|
-
);
|
|
172
|
-
if (finalOptions.exclude)
|
|
173
|
-
console.log(
|
|
174
|
-
` exclude: ${chalk.bold(truncateArray(finalOptions.exclude, 6))}`
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
if (finalOptions['pattern-detect'] || finalOptions.minSimilarity) {
|
|
178
|
-
const patternDetectConfig = finalOptions['pattern-detect'] || {
|
|
179
|
-
minSimilarity: finalOptions.minSimilarity,
|
|
180
|
-
minLines: finalOptions.minLines,
|
|
181
|
-
approx: finalOptions.approx,
|
|
182
|
-
minSharedTokens: finalOptions.minSharedTokens,
|
|
183
|
-
maxCandidatesPerBlock: finalOptions.maxCandidatesPerBlock,
|
|
184
|
-
batchSize: finalOptions.batchSize,
|
|
185
|
-
streamResults: finalOptions.streamResults,
|
|
186
|
-
severity: (finalOptions as any).severity,
|
|
187
|
-
includeTests: (finalOptions as any).includeTests,
|
|
188
|
-
};
|
|
189
|
-
console.log(chalk.white('\nPattern-detect settings:'));
|
|
190
|
-
console.log(
|
|
191
|
-
` minSimilarity: ${chalk.bold(patternDetectConfig.minSimilarity ?? 'default')}`
|
|
192
|
-
);
|
|
193
|
-
console.log(
|
|
194
|
-
` minLines: ${chalk.bold(patternDetectConfig.minLines ?? 'default')}`
|
|
195
|
-
);
|
|
196
|
-
if (patternDetectConfig.approx !== undefined)
|
|
197
|
-
console.log(
|
|
198
|
-
` approx: ${chalk.bold(String(patternDetectConfig.approx))}`
|
|
199
|
-
);
|
|
200
|
-
if (patternDetectConfig.minSharedTokens !== undefined)
|
|
201
|
-
console.log(
|
|
202
|
-
` minSharedTokens: ${chalk.bold(String(patternDetectConfig.minSharedTokens))}`
|
|
203
|
-
);
|
|
204
|
-
if (patternDetectConfig.maxCandidatesPerBlock !== undefined)
|
|
205
|
-
console.log(
|
|
206
|
-
` maxCandidatesPerBlock: ${chalk.bold(String(patternDetectConfig.maxCandidatesPerBlock))}`
|
|
207
|
-
);
|
|
208
|
-
if (patternDetectConfig.batchSize !== undefined)
|
|
209
|
-
console.log(
|
|
210
|
-
` batchSize: ${chalk.bold(String(patternDetectConfig.batchSize))}`
|
|
211
|
-
);
|
|
212
|
-
if (patternDetectConfig.streamResults !== undefined)
|
|
213
|
-
console.log(
|
|
214
|
-
` streamResults: ${chalk.bold(String(patternDetectConfig.streamResults))}`
|
|
215
|
-
);
|
|
216
|
-
if (patternDetectConfig.severity !== undefined)
|
|
217
|
-
console.log(
|
|
218
|
-
` severity: ${chalk.bold(String(patternDetectConfig.severity))}`
|
|
219
|
-
);
|
|
220
|
-
if (patternDetectConfig.includeTests !== undefined)
|
|
221
|
-
console.log(
|
|
222
|
-
` includeTests: ${chalk.bold(String(patternDetectConfig.includeTests))}`
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (finalOptions['context-analyzer'] || finalOptions.maxDepth) {
|
|
227
|
-
const ca = finalOptions['context-analyzer'] || {
|
|
228
|
-
maxDepth: finalOptions.maxDepth,
|
|
229
|
-
maxContextBudget: finalOptions.maxContextBudget,
|
|
230
|
-
minCohesion: (finalOptions as any).minCohesion,
|
|
231
|
-
maxFragmentation: (finalOptions as any).maxFragmentation,
|
|
232
|
-
includeNodeModules: (finalOptions as any).includeNodeModules,
|
|
233
|
-
};
|
|
234
|
-
console.log(chalk.white('\nContext-analyzer settings:'));
|
|
235
|
-
console.log(` maxDepth: ${chalk.bold(ca.maxDepth ?? 'default')}`);
|
|
236
|
-
console.log(
|
|
237
|
-
` maxContextBudget: ${chalk.bold(ca.maxContextBudget ?? 'default')}`
|
|
238
|
-
);
|
|
239
|
-
if (ca.minCohesion !== undefined)
|
|
240
|
-
console.log(` minCohesion: ${chalk.bold(String(ca.minCohesion))}`);
|
|
241
|
-
if (ca.maxFragmentation !== undefined)
|
|
242
|
-
console.log(
|
|
243
|
-
` maxFragmentation: ${chalk.bold(String(ca.maxFragmentation))}`
|
|
244
|
-
);
|
|
245
|
-
if (ca.includeNodeModules !== undefined)
|
|
246
|
-
console.log(
|
|
247
|
-
` includeNodeModules: ${chalk.bold(String(ca.includeNodeModules))}`
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (finalOptions.consistency) {
|
|
252
|
-
const c = finalOptions.consistency;
|
|
253
|
-
console.log(chalk.white('\nConsistency settings:'));
|
|
254
|
-
console.log(
|
|
255
|
-
` checkNaming: ${chalk.bold(String(c.checkNaming ?? true))}`
|
|
256
|
-
);
|
|
257
|
-
console.log(
|
|
258
|
-
` checkPatterns: ${chalk.bold(String(c.checkPatterns ?? true))}`
|
|
259
|
-
);
|
|
260
|
-
console.log(
|
|
261
|
-
` checkArchitecture: ${chalk.bold(String(c.checkArchitecture ?? false))}`
|
|
262
|
-
);
|
|
263
|
-
if (c.minSeverity)
|
|
264
|
-
console.log(` minSeverity: ${chalk.bold(c.minSeverity)}`);
|
|
265
|
-
if (c.acceptedAbbreviations)
|
|
266
|
-
console.log(
|
|
267
|
-
` acceptedAbbreviations: ${chalk.bold(truncateArray(c.acceptedAbbreviations, 8))}`
|
|
268
|
-
);
|
|
269
|
-
if (c.shortWords)
|
|
270
|
-
console.log(
|
|
271
|
-
` shortWords: ${chalk.bold(truncateArray(c.shortWords, 8))}`
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
console.log(chalk.white('\nStarting analysis...'));
|
|
276
|
-
|
|
277
|
-
// Progress callback to surface per-tool output as each tool finishes
|
|
278
|
-
const progressCallback = (event: { tool: string; data: any }) => {
|
|
279
|
-
console.log(chalk.cyan(`\n--- ${event.tool.toUpperCase()} RESULTS ---`));
|
|
280
|
-
try {
|
|
281
|
-
if (event.tool === 'patterns') {
|
|
282
|
-
const pr = event.data as any;
|
|
283
|
-
console.log(
|
|
284
|
-
` Duplicate patterns: ${chalk.bold(String(pr.duplicates?.length || 0))}`
|
|
285
|
-
);
|
|
286
|
-
console.log(
|
|
287
|
-
` Files with pattern issues: ${chalk.bold(String(pr.results?.length || 0))}`
|
|
288
|
-
);
|
|
289
|
-
// show top duplicate summaries
|
|
290
|
-
if (pr.duplicates && pr.duplicates.length > 0) {
|
|
291
|
-
pr.duplicates.slice(0, 5).forEach((d: any, i: number) => {
|
|
292
|
-
console.log(
|
|
293
|
-
` ${i + 1}. ${d.file1.split('/').pop()} ā ${d.file2.split('/').pop()} (sim=${(d.similarity * 100).toFixed(1)}%)`
|
|
294
|
-
);
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// show top files with pattern issues (sorted by issue count desc)
|
|
299
|
-
if (pr.results && pr.results.length > 0) {
|
|
300
|
-
console.log(` Top files with pattern issues:`);
|
|
301
|
-
const sortedByIssues = [...pr.results].sort(
|
|
302
|
-
(a: any, b: any) =>
|
|
303
|
-
(b.issues?.length || 0) - (a.issues?.length || 0)
|
|
304
|
-
);
|
|
305
|
-
sortedByIssues.slice(0, 5).forEach((r: any, i: number) => {
|
|
306
|
-
console.log(
|
|
307
|
-
` ${i + 1}. ${r.fileName.split('/').pop()} - ${r.issues.length} issue(s)`
|
|
308
|
-
);
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Grouping and clusters summary (if available) ā show after detailed findings
|
|
313
|
-
if (pr.groups && pr.groups.length >= 0) {
|
|
314
|
-
console.log(
|
|
315
|
-
` ā
Grouped ${chalk.bold(String(pr.duplicates?.length || 0))} duplicates into ${chalk.bold(String(pr.groups.length))} file pairs`
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
if (pr.clusters && pr.clusters.length >= 0) {
|
|
319
|
-
console.log(
|
|
320
|
-
` ā
Created ${chalk.bold(String(pr.clusters.length))} refactor clusters`
|
|
321
|
-
);
|
|
322
|
-
// show brief cluster summaries
|
|
323
|
-
pr.clusters.slice(0, 3).forEach((cl: any, idx: number) => {
|
|
324
|
-
const files = (cl.files || [])
|
|
325
|
-
.map((f: any) => f.path.split('/').pop())
|
|
326
|
-
.join(', ');
|
|
327
|
-
console.log(
|
|
328
|
-
` ${idx + 1}. ${files} (${cl.tokenCost || 'n/a'} tokens)`
|
|
329
|
-
);
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
} else if (event.tool === 'context') {
|
|
333
|
-
const cr = event.data as any[];
|
|
334
|
-
console.log(
|
|
335
|
-
` Context issues found: ${chalk.bold(String(cr.length || 0))}`
|
|
336
|
-
);
|
|
337
|
-
cr.slice(0, 5).forEach((c: any, i: number) => {
|
|
338
|
-
const msg = c.message ? ` - ${c.message}` : '';
|
|
339
|
-
console.log(
|
|
340
|
-
` ${i + 1}. ${c.file} (${c.severity || 'n/a'})${msg}`
|
|
341
|
-
);
|
|
342
|
-
});
|
|
343
|
-
} else if (event.tool === 'consistency') {
|
|
344
|
-
const rep = event.data as any;
|
|
345
|
-
console.log(
|
|
346
|
-
` Consistency totalIssues: ${chalk.bold(String(rep.summary?.totalIssues || 0))}`
|
|
347
|
-
);
|
|
348
|
-
|
|
349
|
-
if (rep.results && rep.results.length > 0) {
|
|
350
|
-
// Group issues by file
|
|
351
|
-
const fileMap = new Map<string, any[]>();
|
|
352
|
-
rep.results.forEach((r: any) => {
|
|
353
|
-
(r.issues || []).forEach((issue: any) => {
|
|
354
|
-
const file = issue.location?.file || r.file || 'unknown';
|
|
355
|
-
if (!fileMap.has(file)) fileMap.set(file, []);
|
|
356
|
-
fileMap.get(file)!.push(issue);
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
// Sort files by number of issues desc
|
|
361
|
-
const files = Array.from(fileMap.entries()).sort(
|
|
362
|
-
(a, b) => b[1].length - a[1].length
|
|
363
|
-
);
|
|
364
|
-
const topFiles = files.slice(0, 10);
|
|
365
|
-
|
|
366
|
-
topFiles.forEach(([file, issues], idx) => {
|
|
367
|
-
// Count severities
|
|
368
|
-
const counts = issues.reduce(
|
|
369
|
-
(acc: any, it: any) => {
|
|
370
|
-
const s = (it.severity || Severity.Info).toLowerCase();
|
|
371
|
-
acc[s] = (acc[s] || 0) + 1;
|
|
372
|
-
return acc;
|
|
373
|
-
},
|
|
374
|
-
{} as Record<string, number>
|
|
375
|
-
);
|
|
376
|
-
|
|
377
|
-
const sample =
|
|
378
|
-
issues.find(
|
|
379
|
-
(it: any) =>
|
|
380
|
-
it.severity === Severity.Critical ||
|
|
381
|
-
it.severity === Severity.Major
|
|
382
|
-
) || issues[0];
|
|
383
|
-
const sampleMsg = sample ? ` ā ${sample.message}` : '';
|
|
384
157
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
158
|
+
// Dynamic progress callback
|
|
159
|
+
const progressCallback = (event: any) => {
|
|
160
|
+
// Handle progress messages
|
|
161
|
+
if (event.message) {
|
|
162
|
+
process.stdout.write(`\r\x1b[K [${event.tool}] ${event.message}`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
389
165
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const dr = event.data as any;
|
|
401
|
-
console.log(
|
|
402
|
-
` Issues found: ${chalk.bold(String(dr.issues?.length || 0))}`
|
|
403
|
-
);
|
|
404
|
-
if (dr.rawData) {
|
|
405
|
-
console.log(
|
|
406
|
-
` Signature Mismatches: ${chalk.bold(dr.rawData.outdatedComments || 0)}`
|
|
407
|
-
);
|
|
408
|
-
console.log(
|
|
409
|
-
` Undocumented Complexity: ${chalk.bold(dr.rawData.undocumentedComplexity || 0)}`
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
} else if (event.tool === 'deps-health') {
|
|
413
|
-
const dr = event.data as any;
|
|
166
|
+
// Handle tool completion
|
|
167
|
+
process.stdout.write('\n'); // Clear the progress line
|
|
168
|
+
console.log(chalk.cyan(`--- ${event.tool.toUpperCase()} RESULTS ---`));
|
|
169
|
+
const res = event.data;
|
|
170
|
+
if (res && res.summary) {
|
|
171
|
+
if (res.summary.totalIssues !== undefined)
|
|
172
|
+
console.log(` Issues found: ${chalk.bold(res.summary.totalIssues)}`);
|
|
173
|
+
if (res.summary.score !== undefined)
|
|
174
|
+
console.log(` Tool Score: ${chalk.bold(res.summary.score)}/100`);
|
|
175
|
+
if (res.summary.totalFiles !== undefined)
|
|
414
176
|
console.log(
|
|
415
|
-
`
|
|
177
|
+
` Files analyzed: ${chalk.bold(res.summary.totalFiles)}`
|
|
416
178
|
);
|
|
417
|
-
if (dr.rawData) {
|
|
418
|
-
console.log(
|
|
419
|
-
` Deprecated Packages: ${chalk.bold(dr.rawData.deprecatedPackages || 0)}`
|
|
420
|
-
);
|
|
421
|
-
console.log(
|
|
422
|
-
` AI Cutoff Skew Score: ${chalk.bold(dr.rawData.trainingCutoffSkew?.toFixed(1) || 0)}`
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
} else if (
|
|
426
|
-
event.tool === 'change-amplification' ||
|
|
427
|
-
event.tool === 'changeAmplification'
|
|
428
|
-
) {
|
|
429
|
-
const dr = event.data as any;
|
|
430
|
-
console.log(
|
|
431
|
-
` Coupling issues: ${chalk.bold(String(dr.issues?.length || 0))}`
|
|
432
|
-
);
|
|
433
|
-
if (dr.summary) {
|
|
434
|
-
console.log(
|
|
435
|
-
` Complexity Score: ${chalk.bold(dr.summary.score || 0)}/100`
|
|
436
|
-
);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
} catch (err) {
|
|
440
|
-
void err;
|
|
441
179
|
}
|
|
442
180
|
};
|
|
443
181
|
|
|
@@ -445,57 +183,19 @@ export async function scanAction(directory: string, options: ScanOptions) {
|
|
|
445
183
|
...finalOptions,
|
|
446
184
|
progressCallback,
|
|
447
185
|
onProgress: (processed: number, total: number, message: string) => {
|
|
448
|
-
// Clear line and print progress
|
|
449
186
|
process.stdout.write(
|
|
450
187
|
`\r\x1b[K [${processed}/${total}] ${message}...`
|
|
451
188
|
);
|
|
452
|
-
if (processed === total)
|
|
453
|
-
process.stdout.write('\n'); // Move to next line when done
|
|
454
|
-
}
|
|
189
|
+
if (processed === total) process.stdout.write('\n');
|
|
455
190
|
},
|
|
456
191
|
suppressToolConfig: true,
|
|
457
192
|
});
|
|
458
193
|
|
|
459
|
-
// Determine if we need to print a trailing newline because the last tool didn't finish normally or had 0 files
|
|
460
|
-
// But progressCallback already outputs `\n--- TOOL RESULTS ---` so it's fine.
|
|
461
|
-
|
|
462
|
-
// Summarize tools and results to console
|
|
463
194
|
console.log(chalk.cyan('\n=== AIReady Run Summary ==='));
|
|
464
|
-
console.log(
|
|
465
|
-
chalk.white('Tools run:'),
|
|
466
|
-
(finalOptions.tools || ['patterns', 'context', 'consistency']).join(', ')
|
|
467
|
-
);
|
|
468
|
-
|
|
469
|
-
console.log(chalk.cyan('\nResults summary:'));
|
|
470
195
|
console.log(
|
|
471
196
|
` Total issues (all tools): ${chalk.bold(String(results.summary.totalIssues || 0))}`
|
|
472
197
|
);
|
|
473
|
-
if (results[ToolName.PatternDetect]) {
|
|
474
|
-
console.log(
|
|
475
|
-
` Duplicate patterns found: ${chalk.bold(String(results[ToolName.PatternDetect].duplicates?.length || 0))}`
|
|
476
|
-
);
|
|
477
|
-
console.log(
|
|
478
|
-
` Pattern files with issues: ${chalk.bold(String(results[ToolName.PatternDetect].results.length || 0))}`
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
if (results[ToolName.ContextAnalyzer])
|
|
482
|
-
console.log(
|
|
483
|
-
` Context issues: ${chalk.bold(String(results[ToolName.ContextAnalyzer].results.length || 0))}`
|
|
484
|
-
);
|
|
485
|
-
if (results[ToolName.NamingConsistency])
|
|
486
|
-
console.log(
|
|
487
|
-
` Consistency issues: ${chalk.bold(String(results[ToolName.NamingConsistency].summary?.totalIssues || 0))}`
|
|
488
|
-
);
|
|
489
|
-
if (results[ToolName.ChangeAmplification])
|
|
490
|
-
console.log(
|
|
491
|
-
` Change amplification: ${chalk.bold(String(results[ToolName.ChangeAmplification].summary?.score || 0))}/100`
|
|
492
|
-
);
|
|
493
|
-
console.log(chalk.cyan('===========================\n'));
|
|
494
198
|
|
|
495
|
-
const elapsedTime = getElapsedTime(startTime);
|
|
496
|
-
void elapsedTime;
|
|
497
|
-
|
|
498
|
-
// Calculate score if requested: assemble per-tool scoring outputs
|
|
499
199
|
let scoringResult: ScoringResult | undefined;
|
|
500
200
|
if (options.score || finalOptions.scoring?.showBreakdown) {
|
|
501
201
|
scoringResult = await scoreUnified(results, finalOptions);
|
|
@@ -503,69 +203,42 @@ export async function scanAction(directory: string, options: ScanOptions) {
|
|
|
503
203
|
console.log(chalk.bold('\nš AI Readiness Overall Score'));
|
|
504
204
|
console.log(` ${formatScore(scoringResult)}`);
|
|
505
205
|
|
|
506
|
-
//
|
|
507
|
-
// Note: weights are already handled inside scoreUnified via finalOptions and calculateOverallScore
|
|
508
|
-
|
|
509
|
-
// Check if we need to compare to a previous report
|
|
206
|
+
// Trend comparison logic
|
|
510
207
|
if (options.compareTo) {
|
|
511
208
|
try {
|
|
512
|
-
const
|
|
513
|
-
resolvePath(process.cwd(), options.compareTo),
|
|
514
|
-
'utf8'
|
|
209
|
+
const prevReport = JSON.parse(
|
|
210
|
+
readFileSync(resolvePath(process.cwd(), options.compareTo), 'utf8')
|
|
515
211
|
);
|
|
516
|
-
const prevReport = JSON.parse(prevReportStr);
|
|
517
212
|
const prevScore =
|
|
518
|
-
prevReport.scoring?.
|
|
519
|
-
|
|
213
|
+
prevReport.scoring?.overall || prevReport.scoring?.score;
|
|
520
214
|
if (typeof prevScore === 'number') {
|
|
521
215
|
const diff = scoringResult.overall - prevScore;
|
|
522
216
|
const diffStr = diff > 0 ? `+${diff}` : String(diff);
|
|
523
|
-
|
|
524
|
-
if (diff > 0) {
|
|
217
|
+
if (diff > 0)
|
|
525
218
|
console.log(
|
|
526
219
|
chalk.green(
|
|
527
220
|
` š Trend: ${diffStr} compared to ${options.compareTo} (${prevScore} ā ${scoringResult.overall})`
|
|
528
221
|
)
|
|
529
222
|
);
|
|
530
|
-
|
|
223
|
+
else if (diff < 0)
|
|
531
224
|
console.log(
|
|
532
225
|
chalk.red(
|
|
533
226
|
` š Trend: ${diffStr} compared to ${options.compareTo} (${prevScore} ā ${scoringResult.overall})`
|
|
534
227
|
)
|
|
535
228
|
);
|
|
536
|
-
|
|
537
|
-
// but for now, we just highlight the regression.
|
|
538
|
-
} else {
|
|
229
|
+
else
|
|
539
230
|
console.log(
|
|
540
231
|
chalk.blue(
|
|
541
|
-
` ā Trend: No change
|
|
232
|
+
` ā Trend: No change (${prevScore} ā ${scoringResult.overall})`
|
|
542
233
|
)
|
|
543
234
|
);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// Add trend info to scoringResult for programmatic use
|
|
547
|
-
(scoringResult as any).trend = {
|
|
548
|
-
previousScore: prevScore,
|
|
549
|
-
difference: diff,
|
|
550
|
-
};
|
|
551
|
-
} else {
|
|
552
|
-
console.log(
|
|
553
|
-
chalk.yellow(
|
|
554
|
-
`\n ā ļø Previous report at ${options.compareTo} does not contain an overall score.`
|
|
555
|
-
)
|
|
556
|
-
);
|
|
557
235
|
}
|
|
558
236
|
} catch (e) {
|
|
559
237
|
void e;
|
|
560
|
-
console.log(
|
|
561
|
-
chalk.yellow(
|
|
562
|
-
`\n ā ļø Could not read or parse previous report at ${options.compareTo}.`
|
|
563
|
-
)
|
|
564
|
-
);
|
|
565
238
|
}
|
|
566
239
|
}
|
|
567
240
|
|
|
568
|
-
//
|
|
241
|
+
// Token Budget & Cost Logic
|
|
569
242
|
const totalWastedDuplication = (scoringResult.breakdown || []).reduce(
|
|
570
243
|
(sum, s) =>
|
|
571
244
|
sum + (s.tokenBudget?.wastedTokens.bySource.duplication || 0),
|
|
@@ -579,7 +252,8 @@ export async function scanAction(directory: string, options: ScanOptions) {
|
|
|
579
252
|
const totalContext = Math.max(
|
|
580
253
|
...(scoringResult.breakdown || []).map(
|
|
581
254
|
(s) => s.tokenBudget?.totalContextTokens || 0
|
|
582
|
-
)
|
|
255
|
+
),
|
|
256
|
+
0
|
|
583
257
|
);
|
|
584
258
|
|
|
585
259
|
if (totalContext > 0) {
|
|
@@ -591,42 +265,20 @@ export async function scanAction(directory: string, options: ScanOptions) {
|
|
|
591
265
|
chattiness: 0,
|
|
592
266
|
},
|
|
593
267
|
});
|
|
594
|
-
|
|
595
|
-
const targetModel = options.model || 'claude-4.6';
|
|
596
|
-
const modelPreset = getModelPreset(targetModel);
|
|
268
|
+
const modelPreset = getModelPreset(options.model || 'claude-4.6');
|
|
597
269
|
const costEstimate = estimateCostFromBudget(unifiedBudget, modelPreset);
|
|
598
270
|
|
|
599
|
-
|
|
600
|
-
const filled = Math.round(unifiedBudget.efficiencyRatio * barWidth);
|
|
601
|
-
const bar =
|
|
602
|
-
chalk.green('ā'.repeat(filled)) +
|
|
603
|
-
chalk.dim('ā'.repeat(barWidth - filled));
|
|
604
|
-
|
|
605
|
-
console.log(chalk.bold('\nš AI Token Budget Analysis (v0.13)'));
|
|
606
|
-
console.log(
|
|
607
|
-
` Efficiency: [${bar}] ${(unifiedBudget.efficiencyRatio * 100).toFixed(0)}%`
|
|
608
|
-
);
|
|
609
|
-
console.log(
|
|
610
|
-
` Total Context: ${chalk.bold(unifiedBudget.totalContextTokens.toLocaleString())} tokens`
|
|
611
|
-
);
|
|
271
|
+
console.log(chalk.bold('\nš AI Token Budget Analysis'));
|
|
612
272
|
console.log(
|
|
613
|
-
`
|
|
273
|
+
` Efficiency: ${(unifiedBudget.efficiencyRatio * 100).toFixed(0)}%`
|
|
614
274
|
);
|
|
615
|
-
console.log(` Waste Breakdown:`);
|
|
616
275
|
console.log(
|
|
617
|
-
`
|
|
276
|
+
` Wasted Tokens: ${chalk.red(unifiedBudget.wastedTokens.total.toLocaleString())}`
|
|
618
277
|
);
|
|
619
278
|
console.log(
|
|
620
|
-
`
|
|
621
|
-
);
|
|
622
|
-
console.log(
|
|
623
|
-
` Potential Savings: ${chalk.green(unifiedBudget.potentialRetrievableTokens.toLocaleString())} tokens retrievable`
|
|
624
|
-
);
|
|
625
|
-
console.log(
|
|
626
|
-
`\n Est. Monthly Cost (${modelPreset.name}): ${chalk.bold('$' + costEstimate.total)} [range: $${costEstimate.range[0]}-$${costEstimate.range[1]}]`
|
|
279
|
+
` Est. Monthly Cost (${modelPreset.name}): ${chalk.bold('$' + costEstimate.total)}`
|
|
627
280
|
);
|
|
628
281
|
|
|
629
|
-
// Attach unified budget to report for JSON persistence
|
|
630
282
|
(scoringResult as any).tokenBudget = unifiedBudget;
|
|
631
283
|
(scoringResult as any).costEstimate = {
|
|
632
284
|
model: modelPreset.name,
|
|
@@ -634,122 +286,38 @@ export async function scanAction(directory: string, options: ScanOptions) {
|
|
|
634
286
|
};
|
|
635
287
|
}
|
|
636
288
|
|
|
637
|
-
|
|
638
|
-
if (scoringResult.breakdown && scoringResult.breakdown.length > 0) {
|
|
289
|
+
if (scoringResult.breakdown) {
|
|
639
290
|
console.log(chalk.bold('\nTool breakdown:'));
|
|
640
291
|
scoringResult.breakdown.forEach((tool) => {
|
|
641
292
|
const rating = getRating(tool.score);
|
|
642
|
-
|
|
643
|
-
console.log(
|
|
644
|
-
` - ${tool.toolName}: ${tool.score}/100 (${rating}) ${rd.emoji}`
|
|
645
|
-
);
|
|
293
|
+
console.log(` - ${tool.toolName}: ${tool.score}/100 (${rating})`);
|
|
646
294
|
});
|
|
647
|
-
console.log();
|
|
648
|
-
|
|
649
|
-
if (finalOptions.scoring?.showBreakdown) {
|
|
650
|
-
console.log(chalk.bold('Detailed tool breakdown:'));
|
|
651
|
-
scoringResult.breakdown.forEach((tool) => {
|
|
652
|
-
console.log(formatToolScore(tool));
|
|
653
|
-
});
|
|
654
|
-
console.log();
|
|
655
|
-
}
|
|
656
295
|
}
|
|
657
296
|
}
|
|
658
297
|
|
|
659
|
-
//
|
|
298
|
+
// Normalized report mapping
|
|
660
299
|
const mapToUnifiedReport = (
|
|
661
300
|
res: any,
|
|
662
301
|
scoring: ScoringResult | undefined
|
|
663
302
|
) => {
|
|
664
303
|
const allResults: any[] = [];
|
|
665
|
-
|
|
304
|
+
const totalFilesSet = new Set<string>();
|
|
666
305
|
let criticalCount = 0;
|
|
667
306
|
let majorCount = 0;
|
|
668
307
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
spokeRes: any,
|
|
672
|
-
defaultType: IssueType = IssueType.AiSignalClarity
|
|
673
|
-
) => {
|
|
308
|
+
res.summary.toolsRun.forEach((toolId: string) => {
|
|
309
|
+
const spokeRes = res[toolId];
|
|
674
310
|
if (!spokeRes || !spokeRes.results) return;
|
|
675
|
-
spokeRes.results.forEach((r: any) => {
|
|
676
|
-
const fileName = r.fileName || r.file || 'unknown';
|
|
677
|
-
totalFilesSet.add(fileName);
|
|
678
|
-
|
|
679
|
-
// Enforce strict AnalysisResult schema
|
|
680
|
-
const normalizedResult = {
|
|
681
|
-
fileName,
|
|
682
|
-
issues: [] as any[],
|
|
683
|
-
metrics: r.metrics || { tokenCost: r.tokenCost || 0 },
|
|
684
|
-
};
|
|
685
|
-
|
|
686
|
-
if (r.issues && Array.isArray(r.issues)) {
|
|
687
|
-
r.issues.forEach((i: any) => {
|
|
688
|
-
const normalizedIssue =
|
|
689
|
-
typeof i === 'string'
|
|
690
|
-
? {
|
|
691
|
-
type: defaultType,
|
|
692
|
-
severity: (r.severity || Severity.Info) as Severity,
|
|
693
|
-
message: i,
|
|
694
|
-
location: { file: fileName, line: 1 },
|
|
695
|
-
}
|
|
696
|
-
: {
|
|
697
|
-
type: i.type || defaultType,
|
|
698
|
-
severity: (i.severity ||
|
|
699
|
-
r.severity ||
|
|
700
|
-
Severity.Info) as Severity,
|
|
701
|
-
message: i.message || String(i),
|
|
702
|
-
location: i.location || { file: fileName, line: 1 },
|
|
703
|
-
suggestion: i.suggestion,
|
|
704
|
-
};
|
|
705
|
-
|
|
706
|
-
if (
|
|
707
|
-
normalizedIssue.severity === Severity.Critical ||
|
|
708
|
-
normalizedIssue.severity === 'critical'
|
|
709
|
-
)
|
|
710
|
-
criticalCount++;
|
|
711
|
-
if (
|
|
712
|
-
normalizedIssue.severity === Severity.Major ||
|
|
713
|
-
normalizedIssue.severity === 'major'
|
|
714
|
-
)
|
|
715
|
-
majorCount++;
|
|
716
|
-
|
|
717
|
-
normalizedResult.issues.push(normalizedIssue);
|
|
718
|
-
});
|
|
719
|
-
} else if (r.severity) {
|
|
720
|
-
// handle context-analyzer style if issues missing but severity present
|
|
721
|
-
const normalizedIssue = {
|
|
722
|
-
type: defaultType,
|
|
723
|
-
severity: r.severity as Severity,
|
|
724
|
-
message: r.message || 'General issue',
|
|
725
|
-
location: { file: fileName, line: 1 },
|
|
726
|
-
};
|
|
727
|
-
if (
|
|
728
|
-
normalizedIssue.severity === Severity.Critical ||
|
|
729
|
-
normalizedIssue.severity === 'critical'
|
|
730
|
-
)
|
|
731
|
-
criticalCount++;
|
|
732
|
-
if (
|
|
733
|
-
normalizedIssue.severity === Severity.Major ||
|
|
734
|
-
normalizedIssue.severity === 'major'
|
|
735
|
-
)
|
|
736
|
-
majorCount++;
|
|
737
|
-
normalizedResult.issues.push(normalizedIssue);
|
|
738
|
-
}
|
|
739
311
|
|
|
740
|
-
|
|
312
|
+
spokeRes.results.forEach((r: any) => {
|
|
313
|
+
totalFilesSet.add(r.fileName);
|
|
314
|
+
allResults.push(r);
|
|
315
|
+
r.issues?.forEach((i: any) => {
|
|
316
|
+
if (i.severity === Severity.Critical) criticalCount++;
|
|
317
|
+
if (i.severity === Severity.Major) majorCount++;
|
|
318
|
+
});
|
|
741
319
|
});
|
|
742
|
-
};
|
|
743
|
-
|
|
744
|
-
collect(res[ToolName.PatternDetect], IssueType.DuplicatePattern);
|
|
745
|
-
collect(res[ToolName.ContextAnalyzer], IssueType.ContextFragmentation);
|
|
746
|
-
collect(res[ToolName.NamingConsistency], IssueType.NamingInconsistency);
|
|
747
|
-
collect(res[ToolName.DocDrift], IssueType.DocDrift);
|
|
748
|
-
collect(res[ToolName.DependencyHealth], IssueType.DependencyHealth);
|
|
749
|
-
collect(res[ToolName.AiSignalClarity], IssueType.AiSignalClarity);
|
|
750
|
-
collect(res[ToolName.AgentGrounding], IssueType.AgentNavigationFailure);
|
|
751
|
-
collect(res[ToolName.TestabilityIndex], IssueType.LowTestability);
|
|
752
|
-
collect(res[ToolName.ChangeAmplification], IssueType.ChangeAmplification);
|
|
320
|
+
});
|
|
753
321
|
|
|
754
322
|
return {
|
|
755
323
|
...res,
|
|
@@ -760,205 +328,82 @@ export async function scanAction(directory: string, options: ScanOptions) {
|
|
|
760
328
|
criticalIssues: criticalCount,
|
|
761
329
|
majorIssues: majorCount,
|
|
762
330
|
},
|
|
763
|
-
scoring
|
|
331
|
+
scoring,
|
|
764
332
|
};
|
|
765
333
|
};
|
|
766
334
|
|
|
767
|
-
|
|
335
|
+
const outputData = {
|
|
336
|
+
...mapToUnifiedReport(results, scoringResult),
|
|
337
|
+
repository: repoMetadata,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// Output persistence
|
|
768
341
|
const outputFormat =
|
|
769
342
|
options.output || finalOptions.output?.format || 'console';
|
|
770
|
-
const
|
|
343
|
+
const outputPath = resolveOutputPath(
|
|
344
|
+
options.outputFile || finalOptions.output?.file,
|
|
345
|
+
`aiready-report-${getReportTimestamp()}.json`,
|
|
346
|
+
resolvedDir
|
|
347
|
+
);
|
|
348
|
+
|
|
771
349
|
if (outputFormat === 'json') {
|
|
772
|
-
const timestamp = getReportTimestamp();
|
|
773
|
-
const defaultFilename = `aiready-report-${timestamp}.json`;
|
|
774
|
-
const outputPath = resolveOutputPath(
|
|
775
|
-
userOutputFile,
|
|
776
|
-
defaultFilename,
|
|
777
|
-
resolvedDir
|
|
778
|
-
);
|
|
779
|
-
const outputData = {
|
|
780
|
-
...mapToUnifiedReport(results, scoringResult),
|
|
781
|
-
repository: repoMetadata,
|
|
782
|
-
};
|
|
783
350
|
handleJSONOutput(
|
|
784
351
|
outputData,
|
|
785
352
|
outputPath,
|
|
786
353
|
`ā
Report saved to ${outputPath}`
|
|
787
354
|
);
|
|
788
|
-
|
|
789
|
-
// Automatic Upload
|
|
790
|
-
if (options.upload) {
|
|
791
|
-
console.log(chalk.blue('\nš¤ Automatic upload triggered...'));
|
|
792
|
-
await uploadAction(outputPath, {
|
|
793
|
-
apiKey: options.apiKey,
|
|
794
|
-
server: options.server,
|
|
795
|
-
});
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// Warn if graph caps may be exceeded
|
|
799
|
-
await warnIfGraphCapExceeded(outputData, resolvedDir);
|
|
800
355
|
} else {
|
|
801
|
-
// Auto-persist report even in console mode for downstream tools
|
|
802
|
-
const timestamp = getReportTimestamp();
|
|
803
|
-
const defaultFilename = `aiready-report-${timestamp}.json`;
|
|
804
|
-
const outputPath = resolveOutputPath(
|
|
805
|
-
userOutputFile,
|
|
806
|
-
defaultFilename,
|
|
807
|
-
resolvedDir
|
|
808
|
-
);
|
|
809
|
-
const outputData = {
|
|
810
|
-
...mapToUnifiedReport(results, scoringResult),
|
|
811
|
-
repository: repoMetadata,
|
|
812
|
-
};
|
|
813
|
-
|
|
814
356
|
try {
|
|
815
357
|
writeFileSync(outputPath, JSON.stringify(outputData, null, 2));
|
|
816
358
|
console.log(chalk.dim(`ā
Report auto-persisted to ${outputPath}`));
|
|
817
|
-
|
|
818
|
-
// Automatic Upload (from auto-persistent report)
|
|
819
|
-
if (options.upload) {
|
|
820
|
-
console.log(chalk.blue('\nš¤ Automatic upload triggered...'));
|
|
821
|
-
await uploadAction(outputPath, {
|
|
822
|
-
apiKey: options.apiKey,
|
|
823
|
-
server: options.server,
|
|
824
|
-
});
|
|
825
|
-
}
|
|
826
|
-
// Warn if graph caps may be exceeded
|
|
827
|
-
await warnIfGraphCapExceeded(outputData, resolvedDir);
|
|
828
359
|
} catch (err) {
|
|
829
360
|
void err;
|
|
830
361
|
}
|
|
831
362
|
}
|
|
832
363
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
364
|
+
if (options.upload) {
|
|
365
|
+
await uploadAction(outputPath, {
|
|
366
|
+
apiKey: options.apiKey,
|
|
367
|
+
server: options.server,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
await warnIfGraphCapExceeded(outputData, resolvedDir);
|
|
371
|
+
|
|
372
|
+
// CI/CD Gatekeeper logic
|
|
373
|
+
const isCI = options.ci || process.env.CI === 'true';
|
|
838
374
|
if (isCI && scoringResult) {
|
|
839
375
|
const threshold = options.threshold
|
|
840
376
|
? parseInt(options.threshold)
|
|
841
377
|
: undefined;
|
|
842
378
|
const failOnLevel = options.failOn || 'critical';
|
|
843
379
|
|
|
844
|
-
// Output GitHub Actions annotations
|
|
845
|
-
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
846
|
-
console.log(`\n::group::AI Readiness Score`);
|
|
847
|
-
console.log(`score=${scoringResult.overall}`);
|
|
848
|
-
if (scoringResult.breakdown) {
|
|
849
|
-
scoringResult.breakdown.forEach((tool) => {
|
|
850
|
-
console.log(`${tool.toolName}=${tool.score}`);
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
|
-
console.log('::endgroup::');
|
|
854
|
-
|
|
855
|
-
// Output annotation for score
|
|
856
|
-
if (threshold && scoringResult.overall < threshold) {
|
|
857
|
-
console.log(
|
|
858
|
-
`::error::AI Readiness Score ${scoringResult.overall} is below threshold ${threshold}`
|
|
859
|
-
);
|
|
860
|
-
} else if (threshold) {
|
|
861
|
-
console.log(
|
|
862
|
-
`::notice::AI Readiness Score: ${scoringResult.overall}/100 (threshold: ${threshold})`
|
|
863
|
-
);
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
// Output annotations for critical issues
|
|
867
|
-
if (results[ToolName.PatternDetect]) {
|
|
868
|
-
const criticalPatterns = results[
|
|
869
|
-
ToolName.PatternDetect
|
|
870
|
-
].results.flatMap((p: any) =>
|
|
871
|
-
p.issues.filter((i: any) => i.severity === Severity.Critical)
|
|
872
|
-
);
|
|
873
|
-
|
|
874
|
-
criticalPatterns.slice(0, 10).forEach((issue: any) => {
|
|
875
|
-
console.log(
|
|
876
|
-
`::warning file=${issue.location?.file || 'unknown'},line=${issue.location?.line || 1}::${issue.message}`
|
|
877
|
-
);
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
// Determine if we should fail
|
|
883
380
|
let shouldFail = false;
|
|
884
381
|
let failReason = '';
|
|
885
382
|
|
|
886
|
-
// Check threshold
|
|
887
383
|
if (threshold && scoringResult.overall < threshold) {
|
|
888
384
|
shouldFail = true;
|
|
889
|
-
failReason = `
|
|
385
|
+
failReason = `Score ${scoringResult.overall} < threshold ${threshold}`;
|
|
890
386
|
}
|
|
891
387
|
|
|
892
|
-
|
|
388
|
+
const report = mapToUnifiedReport(results, scoringResult);
|
|
893
389
|
if (failOnLevel !== 'none') {
|
|
894
|
-
|
|
895
|
-
const minSeverity =
|
|
896
|
-
severityLevels[failOnLevel as keyof typeof severityLevels] || 4;
|
|
897
|
-
|
|
898
|
-
let criticalCount = 0;
|
|
899
|
-
let majorCount = 0;
|
|
900
|
-
|
|
901
|
-
if (results[ToolName.PatternDetect]) {
|
|
902
|
-
results[ToolName.PatternDetect].results.forEach((p: any) => {
|
|
903
|
-
p.issues.forEach((i: any) => {
|
|
904
|
-
if (i.severity === Severity.Critical) criticalCount++;
|
|
905
|
-
if (i.severity === Severity.Major) majorCount++;
|
|
906
|
-
});
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
if (results[ToolName.ContextAnalyzer]) {
|
|
910
|
-
results[ToolName.ContextAnalyzer].results.forEach((c: any) => {
|
|
911
|
-
if (c.severity === Severity.Critical) criticalCount++;
|
|
912
|
-
if (c.severity === Severity.Major) majorCount++;
|
|
913
|
-
});
|
|
914
|
-
}
|
|
915
|
-
if (results[ToolName.NamingConsistency]) {
|
|
916
|
-
results[ToolName.NamingConsistency].results.forEach((r: any) => {
|
|
917
|
-
r.issues?.forEach((i: any) => {
|
|
918
|
-
if (i.severity === Severity.Critical) criticalCount++;
|
|
919
|
-
if (i.severity === Severity.Major) majorCount++;
|
|
920
|
-
});
|
|
921
|
-
});
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
if (minSeverity >= 4 && criticalCount > 0) {
|
|
390
|
+
if (failOnLevel === 'critical' && report.summary.criticalIssues > 0) {
|
|
925
391
|
shouldFail = true;
|
|
926
|
-
failReason = `Found ${
|
|
927
|
-
} else if (
|
|
392
|
+
failReason = `Found ${report.summary.criticalIssues} critical issues`;
|
|
393
|
+
} else if (
|
|
394
|
+
failOnLevel === 'major' &&
|
|
395
|
+
report.summary.criticalIssues + report.summary.majorIssues > 0
|
|
396
|
+
) {
|
|
928
397
|
shouldFail = true;
|
|
929
|
-
failReason = `Found ${
|
|
398
|
+
failReason = `Found ${report.summary.criticalIssues} critical and ${report.summary.majorIssues} major issues`;
|
|
930
399
|
}
|
|
931
400
|
}
|
|
932
401
|
|
|
933
|
-
// Output result
|
|
934
402
|
if (shouldFail) {
|
|
935
|
-
console.log(chalk.red(
|
|
936
|
-
console.log(chalk.red(` Reason: ${failReason}`));
|
|
937
|
-
console.log(chalk.dim('\n Remediation steps:'));
|
|
938
|
-
console.log(
|
|
939
|
-
chalk.dim(' 1. Run `aiready scan` locally to see detailed issues')
|
|
940
|
-
);
|
|
941
|
-
console.log(chalk.dim(' 2. Fix the critical issues before merging'));
|
|
942
|
-
console.log(
|
|
943
|
-
chalk.dim(
|
|
944
|
-
' 3. Consider upgrading to Team plan for historical tracking: https://getaiready.dev/pricing'
|
|
945
|
-
)
|
|
946
|
-
);
|
|
403
|
+
console.log(chalk.red(`\nš« PR BLOCKED: ${failReason}`));
|
|
947
404
|
process.exit(1);
|
|
948
405
|
} else {
|
|
949
|
-
console.log(chalk.green('\nā
PR PASSED
|
|
950
|
-
if (threshold) {
|
|
951
|
-
console.log(
|
|
952
|
-
chalk.green(
|
|
953
|
-
` Score: ${scoringResult.overall}/100 (threshold: ${threshold})`
|
|
954
|
-
)
|
|
955
|
-
);
|
|
956
|
-
}
|
|
957
|
-
console.log(
|
|
958
|
-
chalk.dim(
|
|
959
|
-
'\n š” Track historical trends: https://getaiready.dev ā Team plan $99/mo'
|
|
960
|
-
)
|
|
961
|
-
);
|
|
406
|
+
console.log(chalk.green('\nā
PR PASSED'));
|
|
962
407
|
}
|
|
963
408
|
}
|
|
964
409
|
} catch (error) {
|
|
@@ -966,33 +411,4 @@ export async function scanAction(directory: string, options: ScanOptions) {
|
|
|
966
411
|
}
|
|
967
412
|
}
|
|
968
413
|
|
|
969
|
-
export const scanHelpText =
|
|
970
|
-
EXAMPLES:
|
|
971
|
-
$ aiready scan # Analyze all tools
|
|
972
|
-
$ aiready scan --tools patterns,context # Skip consistency
|
|
973
|
-
$ aiready scan --profile agentic # Optimize for AI agent execution
|
|
974
|
-
$ aiready scan --profile security # Optimize for secure coding (testability)
|
|
975
|
-
$ aiready scan --compare-to prev-report.json # Compare trends against previous run
|
|
976
|
-
$ aiready scan --score --threshold 75 # CI/CD with threshold
|
|
977
|
-
$ aiready scan --ci --threshold 70 # GitHub Actions gatekeeper
|
|
978
|
-
$ aiready scan --ci --fail-on major # Fail on major+ issues
|
|
979
|
-
$ aiready scan --output json --output-file report.json
|
|
980
|
-
$ aiready scan --upload --api-key ar_... # Automatic platform upload
|
|
981
|
-
$ aiready scan --upload --server custom-url.com # Upload to custom platform
|
|
982
|
-
|
|
983
|
-
PROFILES:
|
|
984
|
-
agentic: aiSignalClarity, grounding, testability
|
|
985
|
-
cost: patterns, context
|
|
986
|
-
security: consistency, testability
|
|
987
|
-
onboarding: context, consistency, grounding
|
|
988
|
-
|
|
989
|
-
CI/CD INTEGRATION (Gatekeeper Mode):
|
|
990
|
-
Use --ci for GitHub Actions integration:
|
|
991
|
-
- Outputs GitHub Actions annotations for PR checks
|
|
992
|
-
- Fails with exit code 1 if threshold not met
|
|
993
|
-
- Shows clear "blocked" message with remediation steps
|
|
994
|
-
|
|
995
|
-
Example GitHub Actions workflow:
|
|
996
|
-
- name: AI Readiness Check
|
|
997
|
-
run: aiready scan --ci --threshold 70
|
|
998
|
-
`;
|
|
414
|
+
export const scanHelpText = `...`;
|