@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.
@@ -1,10 +1,9 @@
1
1
  /**
2
- * Scan command - Run comprehensive AI-readiness analysis (patterns + context + consistency)
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
- type ToolScoringOutput,
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
- 'patterns',
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
- 'deps-health',
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
- let profileTools = options.tools
91
- ? options.tools.split(',').map((t: string) => {
92
- const tool = t.trim();
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
- 'ai-signal-clarity',
103
- 'agent-grounding',
104
- 'testability',
92
+ ToolName.AiSignalClarity,
93
+ ToolName.AgentGrounding,
94
+ ToolName.TestabilityIndex,
105
95
  ];
106
96
  break;
107
97
  case 'cost':
108
- profileTools = ['patterns', 'context'];
98
+ profileTools = [ToolName.PatternDetect, ToolName.ContextAnalyzer];
109
99
  break;
110
100
  case 'security':
111
- profileTools = ['consistency', 'testability'];
101
+ profileTools = [
102
+ ToolName.NamingConsistency,
103
+ ToolName.TestabilityIndex,
104
+ ];
112
105
  break;
113
106
  case 'onboarding':
114
- profileTools = ['context', 'consistency', 'agent-grounding'];
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 specified tools or defaults.`
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 patterns tool is enabled
134
+ // Apply smart defaults for pattern detection if requested
141
135
  let finalOptions = { ...baseOptions };
142
- if (baseOptions.tools.includes('patterns')) {
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 || ['patterns', 'context', 'consistency']).join(', ')
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
- console.log(
386
- ` ${idx + 1}. ${file} — ${issues.length} issue(s) (critical:${counts[Severity.Critical] || 0} major:${counts[Severity.Major] || 0} minor:${counts[Severity.Minor] || 0} info:${counts[Severity.Info] || 0})${sampleMsg}`
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
- const remaining = files.length - topFiles.length;
391
- if (remaining > 0) {
392
- console.log(
393
- chalk.dim(
394
- ` ... and ${remaining} more files with issues (use --output json for full details)`
395
- )
396
- );
397
- }
398
- }
399
- } else if (event.tool === 'doc-drift') {
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
- ` Packages Analyzed: ${chalk.bold(String(dr.summary?.packagesAnalyzed || 0))}`
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
- // Parse CLI weight overrides (if any)
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 prevReportStr = readFileSync(
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?.score || prevReport.scoring?.overallScore;
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
- console.log();
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
- } else if (diff < 0) {
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
- // Trend gating: if we regressed and CI is on or threshold is present, we could lower the threshold effectively,
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 compared to ${options.compareTo} (${prevScore} → ${scoringResult.overall})`
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
- // Unified Token Budget Analysis
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
- const barWidth = 20;
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
- ` Wasted Tokens: ${chalk.red(unifiedBudget.wastedTokens.total.toLocaleString())} (${((unifiedBudget.wastedTokens.total / unifiedBudget.totalContextTokens) * 100).toFixed(1)}%)`
273
+ ` Efficiency: ${(unifiedBudget.efficiencyRatio * 100).toFixed(0)}%`
614
274
  );
615
- console.log(` Waste Breakdown:`);
616
275
  console.log(
617
- ` • Duplication: ${unifiedBudget.wastedTokens.bySource.duplication.toLocaleString()} tokens`
276
+ ` Wasted Tokens: ${chalk.red(unifiedBudget.wastedTokens.total.toLocaleString())}`
618
277
  );
619
278
  console.log(
620
- ` • Fragmentation: ${unifiedBudget.wastedTokens.bySource.fragmentation.toLocaleString()} tokens`
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
- // Show concise breakdown; detailed breakdown only if config requests it
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
- const rd = getRatingDisplay(rating);
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
- // Helper to map CLI results to UnifiedReport schema
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
- let totalFilesSet = new Set<string>();
304
+ const totalFilesSet = new Set<string>();
666
305
  let criticalCount = 0;
667
306
  let majorCount = 0;
668
307
 
669
- // Collect from all spokes and normalize to AnalysisResult
670
- const collect = (
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
- allResults.push(normalizedResult);
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: scoring,
331
+ scoring,
764
332
  };
765
333
  };
766
334
 
767
- // Persist JSON summary when output format is json
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 userOutputFile = options.outputFile || finalOptions.output?.file;
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
- // CI/CD Gatekeeper Mode
834
- const isCI =
835
- options.ci ||
836
- process.env.CI === 'true' ||
837
- process.env.GITHUB_ACTIONS === 'true';
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 = `AI Readiness Score ${scoringResult.overall} is below threshold ${threshold}`;
385
+ failReason = `Score ${scoringResult.overall} < threshold ${threshold}`;
890
386
  }
891
387
 
892
- // Check fail-on severity
388
+ const report = mapToUnifiedReport(results, scoringResult);
893
389
  if (failOnLevel !== 'none') {
894
- const severityLevels = { critical: 4, major: 3, minor: 2, any: 1 };
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 ${criticalCount} critical issues`;
927
- } else if (minSeverity >= 3 && criticalCount + majorCount > 0) {
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 ${criticalCount} critical and ${majorCount} major issues`;
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('\n🚫 PR BLOCKED: AI Readiness Check Failed'));
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: AI Readiness Check'));
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 = `...`;