@aiready/cli 0.10.4 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,11 +18,11 @@ import {
20
18
  getModelPreset,
21
19
  getRating,
22
20
  getRatingDisplay,
23
- parseWeightString,
24
21
  getRepoMetadata,
25
22
  Severity,
26
23
  IssueType,
27
- type ToolScoringOutput,
24
+ ToolName,
25
+ ToolRegistry,
28
26
  } from '@aiready/core';
29
27
  import { analyzeUnified, scoreUnified, type ScoringResult } from '../index';
30
28
  import {
@@ -58,24 +56,21 @@ export async function scanAction(directory: string, options: ScanOptions) {
58
56
  console.log(chalk.blue('šŸš€ Starting AIReady unified analysis...\n'));
59
57
 
60
58
  const startTime = Date.now();
61
- // Resolve directory to absolute path to ensure .aiready/ is created in the right location
62
59
  const resolvedDir = resolvePath(process.cwd(), directory || '.');
63
-
64
- // Extract repo metadata for linkage
65
60
  const repoMetadata = getRepoMetadata(resolvedDir);
66
61
 
67
62
  try {
68
- // Define defaults
63
+ // Define defaults using canonical IDs
69
64
  const defaults = {
70
65
  tools: [
71
- 'patterns',
72
- 'context',
73
- 'consistency',
66
+ 'pattern-detect',
67
+ 'context-analyzer',
68
+ 'naming-consistency',
74
69
  'ai-signal-clarity',
75
70
  'agent-grounding',
76
- 'testability',
71
+ 'testability-index',
77
72
  'doc-drift',
78
- 'deps-health',
73
+ 'dependency-health',
79
74
  'change-amplification',
80
75
  ],
81
76
  include: undefined,
@@ -86,49 +81,49 @@ export async function scanAction(directory: string, options: ScanOptions) {
86
81
  },
87
82
  };
88
83
 
89
- let profileTools = options.tools
90
- ? options.tools.split(',').map((t: string) => {
91
- const tool = t.trim();
92
- if (tool === 'hallucination' || tool === 'hallucination-risk')
93
- return 'aiSignalClarity';
94
- return tool;
95
- })
84
+ // Map profile to tool IDs
85
+ let profileTools: string[] | undefined = options.tools
86
+ ? options.tools.split(',').map((t) => t.trim())
96
87
  : undefined;
97
88
  if (options.profile) {
98
89
  switch (options.profile.toLowerCase()) {
99
90
  case 'agentic':
100
91
  profileTools = [
101
- 'ai-signal-clarity',
102
- 'agent-grounding',
103
- 'testability',
92
+ ToolName.AiSignalClarity,
93
+ ToolName.AgentGrounding,
94
+ ToolName.TestabilityIndex,
104
95
  ];
105
96
  break;
106
97
  case 'cost':
107
- profileTools = ['patterns', 'context'];
98
+ profileTools = [ToolName.PatternDetect, ToolName.ContextAnalyzer];
108
99
  break;
109
100
  case 'security':
110
- profileTools = ['consistency', 'testability'];
101
+ profileTools = [
102
+ ToolName.NamingConsistency,
103
+ ToolName.TestabilityIndex,
104
+ ];
111
105
  break;
112
106
  case 'onboarding':
113
- profileTools = ['context', 'consistency', 'agent-grounding'];
107
+ profileTools = [
108
+ ToolName.ContextAnalyzer,
109
+ ToolName.NamingConsistency,
110
+ ToolName.AgentGrounding,
111
+ ];
114
112
  break;
115
113
  default:
116
114
  console.log(
117
115
  chalk.yellow(
118
- `\nāš ļø Unknown profile '${options.profile}'. Using specified tools or defaults.`
116
+ `\nāš ļø Unknown profile '${options.profile}'. Using defaults.`
119
117
  )
120
118
  );
121
119
  }
122
120
  }
123
121
 
124
- // Load and merge config with CLI options
125
122
  const cliOverrides: any = {
126
123
  include: options.include?.split(','),
127
124
  exclude: options.exclude?.split(','),
128
125
  };
129
- if (profileTools) {
130
- cliOverrides.tools = profileTools;
131
- }
126
+ if (profileTools) cliOverrides.tools = profileTools;
132
127
 
133
128
  const baseOptions = (await loadMergedConfig(
134
129
  resolvedDir,
@@ -136,15 +131,17 @@ export async function scanAction(directory: string, options: ScanOptions) {
136
131
  cliOverrides
137
132
  )) as any;
138
133
 
139
- // Apply smart defaults for pattern detection if patterns tool is enabled
134
+ // Apply smart defaults for pattern detection if requested
140
135
  let finalOptions = { ...baseOptions };
141
- if (baseOptions.tools.includes('patterns')) {
136
+ if (
137
+ baseOptions.tools.includes(ToolName.PatternDetect) ||
138
+ baseOptions.tools.includes('patterns')
139
+ ) {
142
140
  const { getSmartDefaults } = await import('@aiready/pattern-detect');
143
141
  const patternSmartDefaults = await getSmartDefaults(
144
142
  resolvedDir,
145
143
  baseOptions
146
144
  );
147
- // Merge deeply to preserve nested config
148
145
  finalOptions = {
149
146
  ...patternSmartDefaults,
150
147
  ...finalOptions,
@@ -152,291 +149,25 @@ export async function scanAction(directory: string, options: ScanOptions) {
152
149
  };
153
150
  }
154
151
 
155
- // Print pre-run summary with expanded settings (truncate long arrays)
156
152
  console.log(chalk.cyan('\n=== AIReady Run Preview ==='));
157
153
  console.log(
158
154
  chalk.white('Tools to run:'),
159
- (finalOptions.tools || ['patterns', 'context', 'consistency']).join(', ')
155
+ (finalOptions.tools || []).join(', ')
160
156
  );
161
- console.log(chalk.white('Will use settings from config and defaults.'));
162
-
163
- // Common top-level settings
164
- console.log(chalk.white('\nGeneral settings:'));
165
- if (finalOptions.rootDir)
166
- console.log(` rootDir: ${chalk.bold(String(finalOptions.rootDir))}`);
167
- if (finalOptions.include)
168
- console.log(
169
- ` include: ${chalk.bold(truncateArray(finalOptions.include, 6))}`
170
- );
171
- if (finalOptions.exclude)
172
- console.log(
173
- ` exclude: ${chalk.bold(truncateArray(finalOptions.exclude, 6))}`
174
- );
175
-
176
- if (finalOptions['pattern-detect'] || finalOptions.minSimilarity) {
177
- const patternDetectConfig = finalOptions['pattern-detect'] || {
178
- minSimilarity: finalOptions.minSimilarity,
179
- minLines: finalOptions.minLines,
180
- approx: finalOptions.approx,
181
- minSharedTokens: finalOptions.minSharedTokens,
182
- maxCandidatesPerBlock: finalOptions.maxCandidatesPerBlock,
183
- batchSize: finalOptions.batchSize,
184
- streamResults: finalOptions.streamResults,
185
- severity: (finalOptions as any).severity,
186
- includeTests: (finalOptions as any).includeTests,
187
- };
188
- console.log(chalk.white('\nPattern-detect settings:'));
189
- console.log(
190
- ` minSimilarity: ${chalk.bold(patternDetectConfig.minSimilarity ?? 'default')}`
191
- );
192
- console.log(
193
- ` minLines: ${chalk.bold(patternDetectConfig.minLines ?? 'default')}`
194
- );
195
- if (patternDetectConfig.approx !== undefined)
196
- console.log(
197
- ` approx: ${chalk.bold(String(patternDetectConfig.approx))}`
198
- );
199
- if (patternDetectConfig.minSharedTokens !== undefined)
200
- console.log(
201
- ` minSharedTokens: ${chalk.bold(String(patternDetectConfig.minSharedTokens))}`
202
- );
203
- if (patternDetectConfig.maxCandidatesPerBlock !== undefined)
204
- console.log(
205
- ` maxCandidatesPerBlock: ${chalk.bold(String(patternDetectConfig.maxCandidatesPerBlock))}`
206
- );
207
- if (patternDetectConfig.batchSize !== undefined)
208
- console.log(
209
- ` batchSize: ${chalk.bold(String(patternDetectConfig.batchSize))}`
210
- );
211
- if (patternDetectConfig.streamResults !== undefined)
212
- console.log(
213
- ` streamResults: ${chalk.bold(String(patternDetectConfig.streamResults))}`
214
- );
215
- if (patternDetectConfig.severity !== undefined)
216
- console.log(
217
- ` severity: ${chalk.bold(String(patternDetectConfig.severity))}`
218
- );
219
- if (patternDetectConfig.includeTests !== undefined)
220
- console.log(
221
- ` includeTests: ${chalk.bold(String(patternDetectConfig.includeTests))}`
222
- );
223
- }
224
-
225
- if (finalOptions['context-analyzer'] || finalOptions.maxDepth) {
226
- const ca = finalOptions['context-analyzer'] || {
227
- maxDepth: finalOptions.maxDepth,
228
- maxContextBudget: finalOptions.maxContextBudget,
229
- minCohesion: (finalOptions as any).minCohesion,
230
- maxFragmentation: (finalOptions as any).maxFragmentation,
231
- includeNodeModules: (finalOptions as any).includeNodeModules,
232
- };
233
- console.log(chalk.white('\nContext-analyzer settings:'));
234
- console.log(` maxDepth: ${chalk.bold(ca.maxDepth ?? 'default')}`);
235
- console.log(
236
- ` maxContextBudget: ${chalk.bold(ca.maxContextBudget ?? 'default')}`
237
- );
238
- if (ca.minCohesion !== undefined)
239
- console.log(` minCohesion: ${chalk.bold(String(ca.minCohesion))}`);
240
- if (ca.maxFragmentation !== undefined)
241
- console.log(
242
- ` maxFragmentation: ${chalk.bold(String(ca.maxFragmentation))}`
243
- );
244
- if (ca.includeNodeModules !== undefined)
245
- console.log(
246
- ` includeNodeModules: ${chalk.bold(String(ca.includeNodeModules))}`
247
- );
248
- }
249
157
 
250
- if (finalOptions.consistency) {
251
- const c = finalOptions.consistency;
252
- console.log(chalk.white('\nConsistency settings:'));
253
- console.log(
254
- ` checkNaming: ${chalk.bold(String(c.checkNaming ?? true))}`
255
- );
256
- console.log(
257
- ` checkPatterns: ${chalk.bold(String(c.checkPatterns ?? true))}`
258
- );
259
- console.log(
260
- ` checkArchitecture: ${chalk.bold(String(c.checkArchitecture ?? false))}`
261
- );
262
- if (c.minSeverity)
263
- console.log(` minSeverity: ${chalk.bold(c.minSeverity)}`);
264
- if (c.acceptedAbbreviations)
265
- console.log(
266
- ` acceptedAbbreviations: ${chalk.bold(truncateArray(c.acceptedAbbreviations, 8))}`
267
- );
268
- if (c.shortWords)
269
- console.log(
270
- ` shortWords: ${chalk.bold(truncateArray(c.shortWords, 8))}`
271
- );
272
- }
273
-
274
- console.log(chalk.white('\nStarting analysis...'));
275
-
276
- // Progress callback to surface per-tool output as each tool finishes
158
+ // Dynamic progress callback
277
159
  const progressCallback = (event: { tool: string; data: any }) => {
278
160
  console.log(chalk.cyan(`\n--- ${event.tool.toUpperCase()} RESULTS ---`));
279
- try {
280
- if (event.tool === 'patterns') {
281
- const pr = event.data as any;
161
+ const res = event.data;
162
+ if (res.summary) {
163
+ if (res.summary.totalIssues !== undefined)
164
+ console.log(` Issues found: ${chalk.bold(res.summary.totalIssues)}`);
165
+ if (res.summary.score !== undefined)
166
+ console.log(` Tool Score: ${chalk.bold(res.summary.score)}/100`);
167
+ if (res.summary.totalFiles !== undefined)
282
168
  console.log(
283
- ` Duplicate patterns: ${chalk.bold(String(pr.duplicates?.length || 0))}`
169
+ ` Files analyzed: ${chalk.bold(res.summary.totalFiles)}`
284
170
  );
285
- console.log(
286
- ` Files with pattern issues: ${chalk.bold(String(pr.results?.length || 0))}`
287
- );
288
- // show top duplicate summaries
289
- if (pr.duplicates && pr.duplicates.length > 0) {
290
- pr.duplicates.slice(0, 5).forEach((d: any, i: number) => {
291
- console.log(
292
- ` ${i + 1}. ${d.file1.split('/').pop()} ↔ ${d.file2.split('/').pop()} (sim=${(d.similarity * 100).toFixed(1)}%)`
293
- );
294
- });
295
- }
296
-
297
- // show top files with pattern issues (sorted by issue count desc)
298
- if (pr.results && pr.results.length > 0) {
299
- console.log(` Top files with pattern issues:`);
300
- const sortedByIssues = [...pr.results].sort(
301
- (a: any, b: any) =>
302
- (b.issues?.length || 0) - (a.issues?.length || 0)
303
- );
304
- sortedByIssues.slice(0, 5).forEach((r: any, i: number) => {
305
- console.log(
306
- ` ${i + 1}. ${r.fileName.split('/').pop()} - ${r.issues.length} issue(s)`
307
- );
308
- });
309
- }
310
-
311
- // Grouping and clusters summary (if available) — show after detailed findings
312
- if (pr.groups && pr.groups.length >= 0) {
313
- console.log(
314
- ` āœ… Grouped ${chalk.bold(String(pr.duplicates?.length || 0))} duplicates into ${chalk.bold(String(pr.groups.length))} file pairs`
315
- );
316
- }
317
- if (pr.clusters && pr.clusters.length >= 0) {
318
- console.log(
319
- ` āœ… Created ${chalk.bold(String(pr.clusters.length))} refactor clusters`
320
- );
321
- // show brief cluster summaries
322
- pr.clusters.slice(0, 3).forEach((cl: any, idx: number) => {
323
- const files = (cl.files || [])
324
- .map((f: any) => f.path.split('/').pop())
325
- .join(', ');
326
- console.log(
327
- ` ${idx + 1}. ${files} (${cl.tokenCost || 'n/a'} tokens)`
328
- );
329
- });
330
- }
331
- } else if (event.tool === 'context') {
332
- const cr = event.data as any[];
333
- console.log(
334
- ` Context issues found: ${chalk.bold(String(cr.length || 0))}`
335
- );
336
- cr.slice(0, 5).forEach((c: any, i: number) => {
337
- const msg = c.message ? ` - ${c.message}` : '';
338
- console.log(
339
- ` ${i + 1}. ${c.file} (${c.severity || 'n/a'})${msg}`
340
- );
341
- });
342
- } else if (event.tool === 'consistency') {
343
- const rep = event.data as any;
344
- console.log(
345
- ` Consistency totalIssues: ${chalk.bold(String(rep.summary?.totalIssues || 0))}`
346
- );
347
-
348
- if (rep.results && rep.results.length > 0) {
349
- // Group issues by file
350
- const fileMap = new Map<string, any[]>();
351
- rep.results.forEach((r: any) => {
352
- (r.issues || []).forEach((issue: any) => {
353
- const file = issue.location?.file || r.file || 'unknown';
354
- if (!fileMap.has(file)) fileMap.set(file, []);
355
- fileMap.get(file)!.push(issue);
356
- });
357
- });
358
-
359
- // Sort files by number of issues desc
360
- const files = Array.from(fileMap.entries()).sort(
361
- (a, b) => b[1].length - a[1].length
362
- );
363
- const topFiles = files.slice(0, 10);
364
-
365
- topFiles.forEach(([file, issues], idx) => {
366
- // Count severities
367
- const counts = issues.reduce(
368
- (acc: any, it: any) => {
369
- const s = (it.severity || Severity.Info).toLowerCase();
370
- acc[s] = (acc[s] || 0) + 1;
371
- return acc;
372
- },
373
- {} as Record<string, number>
374
- );
375
-
376
- const sample =
377
- issues.find(
378
- (it: any) =>
379
- it.severity === Severity.Critical ||
380
- it.severity === Severity.Major
381
- ) || issues[0];
382
- const sampleMsg = sample ? ` — ${sample.message}` : '';
383
-
384
- console.log(
385
- ` ${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}`
386
- );
387
- });
388
-
389
- const remaining = files.length - topFiles.length;
390
- if (remaining > 0) {
391
- console.log(
392
- chalk.dim(
393
- ` ... and ${remaining} more files with issues (use --output json for full details)`
394
- )
395
- );
396
- }
397
- }
398
- } else if (event.tool === 'doc-drift') {
399
- const dr = event.data as any;
400
- console.log(
401
- ` Issues found: ${chalk.bold(String(dr.issues?.length || 0))}`
402
- );
403
- if (dr.rawData) {
404
- console.log(
405
- ` Signature Mismatches: ${chalk.bold(dr.rawData.outdatedComments || 0)}`
406
- );
407
- console.log(
408
- ` Undocumented Complexity: ${chalk.bold(dr.rawData.undocumentedComplexity || 0)}`
409
- );
410
- }
411
- } else if (event.tool === 'deps-health') {
412
- const dr = event.data as any;
413
- console.log(
414
- ` Packages Analyzed: ${chalk.bold(String(dr.summary?.packagesAnalyzed || 0))}`
415
- );
416
- if (dr.rawData) {
417
- console.log(
418
- ` Deprecated Packages: ${chalk.bold(dr.rawData.deprecatedPackages || 0)}`
419
- );
420
- console.log(
421
- ` AI Cutoff Skew Score: ${chalk.bold(dr.rawData.trainingCutoffSkew?.toFixed(1) || 0)}`
422
- );
423
- }
424
- } else if (
425
- event.tool === 'change-amplification' ||
426
- event.tool === 'changeAmplification'
427
- ) {
428
- const dr = event.data as any;
429
- console.log(
430
- ` Coupling issues: ${chalk.bold(String(dr.issues?.length || 0))}`
431
- );
432
- if (dr.summary) {
433
- console.log(
434
- ` Complexity Score: ${chalk.bold(dr.summary.score || 0)}/100`
435
- );
436
- }
437
- }
438
- } catch (err) {
439
- void err;
440
171
  }
441
172
  };
442
173
 
@@ -444,58 +175,19 @@ export async function scanAction(directory: string, options: ScanOptions) {
444
175
  ...finalOptions,
445
176
  progressCallback,
446
177
  onProgress: (processed: number, total: number, message: string) => {
447
- // Clear line and print progress
448
178
  process.stdout.write(
449
179
  `\r\x1b[K [${processed}/${total}] ${message}...`
450
180
  );
451
- if (processed === total) {
452
- process.stdout.write('\n'); // Move to next line when done
453
- }
181
+ if (processed === total) process.stdout.write('\n');
454
182
  },
455
183
  suppressToolConfig: true,
456
184
  });
457
185
 
458
- // Determine if we need to print a trailing newline because the last tool didn't finish normally or had 0 files
459
- // But progressCallback already outputs `\n--- TOOL RESULTS ---` so it's fine.
460
-
461
- // Summarize tools and results to console
462
186
  console.log(chalk.cyan('\n=== AIReady Run Summary ==='));
463
- console.log(
464
- chalk.white('Tools run:'),
465
- (finalOptions.tools || ['patterns', 'context', 'consistency']).join(', ')
466
- );
467
-
468
- // Results summary
469
- console.log(chalk.cyan('\nResults summary:'));
470
187
  console.log(
471
188
  ` Total issues (all tools): ${chalk.bold(String(results.summary.totalIssues || 0))}`
472
189
  );
473
- if (results.patternDetect) {
474
- console.log(
475
- ` Duplicate patterns found: ${chalk.bold(String(results.patternDetect.duplicates?.length || 0))}`
476
- );
477
- console.log(
478
- ` Pattern files with issues: ${chalk.bold(String(results.patternDetect.results.length || 0))}`
479
- );
480
- }
481
- if (results.contextAnalyzer)
482
- console.log(
483
- ` Context issues: ${chalk.bold(String(results.contextAnalyzer.results.length || 0))}`
484
- );
485
- if (results.consistency)
486
- console.log(
487
- ` Consistency issues: ${chalk.bold(String(results.consistency.summary?.totalIssues || 0))}`
488
- );
489
- if (results.changeAmplification)
490
- console.log(
491
- ` Change amplification: ${chalk.bold(String(results.changeAmplification.summary?.score || 0))}/100`
492
- );
493
- console.log(chalk.cyan('===========================\n'));
494
190
 
495
- const elapsedTime = getElapsedTime(startTime);
496
- void elapsedTime;
497
-
498
- // Calculate score if requested: assemble per-tool scoring outputs
499
191
  let scoringResult: ScoringResult | undefined;
500
192
  if (options.score || finalOptions.scoring?.showBreakdown) {
501
193
  scoringResult = await scoreUnified(results, finalOptions);
@@ -503,69 +195,42 @@ export async function scanAction(directory: string, options: ScanOptions) {
503
195
  console.log(chalk.bold('\nšŸ“Š AI Readiness Overall Score'));
504
196
  console.log(` ${formatScore(scoringResult)}`);
505
197
 
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
198
+ // Trend comparison logic
510
199
  if (options.compareTo) {
511
200
  try {
512
- const prevReportStr = readFileSync(
513
- resolvePath(process.cwd(), options.compareTo),
514
- 'utf8'
201
+ const prevReport = JSON.parse(
202
+ readFileSync(resolvePath(process.cwd(), options.compareTo), 'utf8')
515
203
  );
516
- const prevReport = JSON.parse(prevReportStr);
517
204
  const prevScore =
518
- prevReport.scoring?.score || prevReport.scoring?.overallScore;
519
-
205
+ prevReport.scoring?.overall || prevReport.scoring?.score;
520
206
  if (typeof prevScore === 'number') {
521
207
  const diff = scoringResult.overall - prevScore;
522
208
  const diffStr = diff > 0 ? `+${diff}` : String(diff);
523
- console.log();
524
- if (diff > 0) {
209
+ if (diff > 0)
525
210
  console.log(
526
211
  chalk.green(
527
212
  ` šŸ“ˆ Trend: ${diffStr} compared to ${options.compareTo} (${prevScore} → ${scoringResult.overall})`
528
213
  )
529
214
  );
530
- } else if (diff < 0) {
215
+ else if (diff < 0)
531
216
  console.log(
532
217
  chalk.red(
533
218
  ` šŸ“‰ Trend: ${diffStr} compared to ${options.compareTo} (${prevScore} → ${scoringResult.overall})`
534
219
  )
535
220
  );
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 {
221
+ else
539
222
  console.log(
540
223
  chalk.blue(
541
- ` āž– Trend: No change compared to ${options.compareTo} (${prevScore} → ${scoringResult.overall})`
224
+ ` āž– Trend: No change (${prevScore} → ${scoringResult.overall})`
542
225
  )
543
226
  );
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
227
  }
558
228
  } catch (e) {
559
229
  void e;
560
- console.log(
561
- chalk.yellow(
562
- `\n āš ļø Could not read or parse previous report at ${options.compareTo}.`
563
- )
564
- );
565
230
  }
566
231
  }
567
232
 
568
- // Unified Token Budget Analysis
233
+ // Token Budget & Cost Logic
569
234
  const totalWastedDuplication = (scoringResult.breakdown || []).reduce(
570
235
  (sum, s) =>
571
236
  sum + (s.tokenBudget?.wastedTokens.bySource.duplication || 0),
@@ -579,7 +244,8 @@ export async function scanAction(directory: string, options: ScanOptions) {
579
244
  const totalContext = Math.max(
580
245
  ...(scoringResult.breakdown || []).map(
581
246
  (s) => s.tokenBudget?.totalContextTokens || 0
582
- )
247
+ ),
248
+ 0
583
249
  );
584
250
 
585
251
  if (totalContext > 0) {
@@ -591,42 +257,20 @@ export async function scanAction(directory: string, options: ScanOptions) {
591
257
  chattiness: 0,
592
258
  },
593
259
  });
594
-
595
- const targetModel = options.model || 'claude-4.6';
596
- const modelPreset = getModelPreset(targetModel);
260
+ const modelPreset = getModelPreset(options.model || 'claude-4.6');
597
261
  const costEstimate = estimateCostFromBudget(unifiedBudget, modelPreset);
598
262
 
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
- );
263
+ console.log(chalk.bold('\nšŸ“Š AI Token Budget Analysis'));
612
264
  console.log(
613
- ` Wasted Tokens: ${chalk.red(unifiedBudget.wastedTokens.total.toLocaleString())} (${((unifiedBudget.wastedTokens.total / unifiedBudget.totalContextTokens) * 100).toFixed(1)}%)`
265
+ ` Efficiency: ${(unifiedBudget.efficiencyRatio * 100).toFixed(0)}%`
614
266
  );
615
- console.log(` Waste Breakdown:`);
616
267
  console.log(
617
- ` • Duplication: ${unifiedBudget.wastedTokens.bySource.duplication.toLocaleString()} tokens`
268
+ ` Wasted Tokens: ${chalk.red(unifiedBudget.wastedTokens.total.toLocaleString())}`
618
269
  );
619
270
  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]}]`
271
+ ` Est. Monthly Cost (${modelPreset.name}): ${chalk.bold('$' + costEstimate.total)}`
627
272
  );
628
273
 
629
- // Attach unified budget to report for JSON persistence
630
274
  (scoringResult as any).tokenBudget = unifiedBudget;
631
275
  (scoringResult as any).costEstimate = {
632
276
  model: modelPreset.name,
@@ -634,122 +278,38 @@ export async function scanAction(directory: string, options: ScanOptions) {
634
278
  };
635
279
  }
636
280
 
637
- // Show concise breakdown; detailed breakdown only if config requests it
638
- if (scoringResult.breakdown && scoringResult.breakdown.length > 0) {
281
+ if (scoringResult.breakdown) {
639
282
  console.log(chalk.bold('\nTool breakdown:'));
640
283
  scoringResult.breakdown.forEach((tool) => {
641
284
  const rating = getRating(tool.score);
642
- const rd = getRatingDisplay(rating);
643
- console.log(
644
- ` - ${tool.toolName}: ${tool.score}/100 (${rating}) ${rd.emoji}`
645
- );
285
+ console.log(` - ${tool.toolName}: ${tool.score}/100 (${rating})`);
646
286
  });
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
287
  }
657
288
  }
658
289
 
659
- // Helper to map CLI results to UnifiedReport schema
290
+ // Normalized report mapping
660
291
  const mapToUnifiedReport = (
661
292
  res: any,
662
293
  scoring: ScoringResult | undefined
663
294
  ) => {
664
295
  const allResults: any[] = [];
665
- let totalFilesSet = new Set<string>();
296
+ const totalFilesSet = new Set<string>();
666
297
  let criticalCount = 0;
667
298
  let majorCount = 0;
668
299
 
669
- // Collect from all spokes and normalize to AnalysisResult
670
- const collect = (
671
- spokeRes: any,
672
- defaultType: IssueType = IssueType.AiSignalClarity
673
- ) => {
300
+ res.summary.toolsRun.forEach((toolId: string) => {
301
+ const spokeRes = res[toolId];
674
302
  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
303
 
740
- allResults.push(normalizedResult);
304
+ spokeRes.results.forEach((r: any) => {
305
+ totalFilesSet.add(r.fileName);
306
+ allResults.push(r);
307
+ r.issues?.forEach((i: any) => {
308
+ if (i.severity === Severity.Critical) criticalCount++;
309
+ if (i.severity === Severity.Major) majorCount++;
310
+ });
741
311
  });
742
- };
743
-
744
- collect(res.patternDetect, IssueType.DuplicatePattern);
745
- collect(res.contextAnalyzer, IssueType.ContextFragmentation);
746
- collect(res.consistency, IssueType.NamingInconsistency);
747
- collect(res.docDrift, IssueType.DocDrift);
748
- collect(res.dependencyHealth, IssueType.DependencyHealth);
749
- collect(res.aiSignalClarity, IssueType.AiSignalClarity);
750
- collect(res.agentGrounding, IssueType.AgentNavigationFailure);
751
- collect(res.testability, IssueType.LowTestability);
752
- collect(res.changeAmplification, IssueType.ChangeAmplification);
312
+ });
753
313
 
754
314
  return {
755
315
  ...res,
@@ -760,203 +320,82 @@ export async function scanAction(directory: string, options: ScanOptions) {
760
320
  criticalIssues: criticalCount,
761
321
  majorIssues: majorCount,
762
322
  },
763
- scoring: scoring,
323
+ scoring,
764
324
  };
765
325
  };
766
326
 
767
- // Persist JSON summary when output format is json
327
+ const outputData = {
328
+ ...mapToUnifiedReport(results, scoringResult),
329
+ repository: repoMetadata,
330
+ };
331
+
332
+ // Output persistence
768
333
  const outputFormat =
769
334
  options.output || finalOptions.output?.format || 'console';
770
- const userOutputFile = options.outputFile || finalOptions.output?.file;
335
+ const outputPath = resolveOutputPath(
336
+ options.outputFile || finalOptions.output?.file,
337
+ `aiready-report-${getReportTimestamp()}.json`,
338
+ resolvedDir
339
+ );
340
+
771
341
  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
342
  handleJSONOutput(
784
343
  outputData,
785
344
  outputPath,
786
345
  `āœ… Report saved to ${outputPath}`
787
346
  );
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
347
  } 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
348
  try {
815
349
  writeFileSync(outputPath, JSON.stringify(outputData, null, 2));
816
350
  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
351
  } catch (err) {
829
352
  void err;
830
353
  }
831
354
  }
832
355
 
833
- // CI/CD Gatekeeper Mode
834
- const isCI =
835
- options.ci ||
836
- process.env.CI === 'true' ||
837
- process.env.GITHUB_ACTIONS === 'true';
356
+ if (options.upload) {
357
+ await uploadAction(outputPath, {
358
+ apiKey: options.apiKey,
359
+ server: options.server,
360
+ });
361
+ }
362
+ await warnIfGraphCapExceeded(outputData, resolvedDir);
363
+
364
+ // CI/CD Gatekeeper logic
365
+ const isCI = options.ci || process.env.CI === 'true';
838
366
  if (isCI && scoringResult) {
839
367
  const threshold = options.threshold
840
368
  ? parseInt(options.threshold)
841
369
  : undefined;
842
370
  const failOnLevel = options.failOn || 'critical';
843
371
 
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.patternDetect) {
868
- const criticalPatterns = results.patternDetect.results.flatMap(
869
- (p: any) =>
870
- p.issues.filter((i: any) => i.severity === Severity.Critical)
871
- );
872
- criticalPatterns.slice(0, 10).forEach((issue: any) => {
873
- console.log(
874
- `::warning file=${issue.location?.file || 'unknown'},line=${issue.location?.line || 1}::${issue.message}`
875
- );
876
- });
877
- }
878
- }
879
-
880
- // Determine if we should fail
881
372
  let shouldFail = false;
882
373
  let failReason = '';
883
374
 
884
- // Check threshold
885
375
  if (threshold && scoringResult.overall < threshold) {
886
376
  shouldFail = true;
887
- failReason = `AI Readiness Score ${scoringResult.overall} is below threshold ${threshold}`;
377
+ failReason = `Score ${scoringResult.overall} < threshold ${threshold}`;
888
378
  }
889
379
 
890
- // Check fail-on severity
380
+ const report = mapToUnifiedReport(results, scoringResult);
891
381
  if (failOnLevel !== 'none') {
892
- const severityLevels = { critical: 4, major: 3, minor: 2, any: 1 };
893
- const minSeverity =
894
- severityLevels[failOnLevel as keyof typeof severityLevels] || 4;
895
-
896
- let criticalCount = 0;
897
- let majorCount = 0;
898
-
899
- if (results.patternDetect) {
900
- results.patternDetect.results.forEach((p: any) => {
901
- p.issues.forEach((i: any) => {
902
- if (i.severity === Severity.Critical) criticalCount++;
903
- if (i.severity === Severity.Major) majorCount++;
904
- });
905
- });
906
- }
907
- if (results.contextAnalyzer) {
908
- results.contextAnalyzer.results.forEach((c: any) => {
909
- if (c.severity === Severity.Critical) criticalCount++;
910
- if (c.severity === Severity.Major) majorCount++;
911
- });
912
- }
913
- if (results.consistency) {
914
- results.consistency.results.forEach((r: any) => {
915
- r.issues?.forEach((i: any) => {
916
- if (i.severity === Severity.Critical) criticalCount++;
917
- if (i.severity === Severity.Major) majorCount++;
918
- });
919
- });
920
- }
921
-
922
- if (minSeverity >= 4 && criticalCount > 0) {
382
+ if (failOnLevel === 'critical' && report.summary.criticalIssues > 0) {
923
383
  shouldFail = true;
924
- failReason = `Found ${criticalCount} critical issues`;
925
- } else if (minSeverity >= 3 && criticalCount + majorCount > 0) {
384
+ failReason = `Found ${report.summary.criticalIssues} critical issues`;
385
+ } else if (
386
+ failOnLevel === 'major' &&
387
+ report.summary.criticalIssues + report.summary.majorIssues > 0
388
+ ) {
926
389
  shouldFail = true;
927
- failReason = `Found ${criticalCount} critical and ${majorCount} major issues`;
390
+ failReason = `Found ${report.summary.criticalIssues} critical and ${report.summary.majorIssues} major issues`;
928
391
  }
929
392
  }
930
393
 
931
- // Output result
932
394
  if (shouldFail) {
933
- console.log(chalk.red('\n🚫 PR BLOCKED: AI Readiness Check Failed'));
934
- console.log(chalk.red(` Reason: ${failReason}`));
935
- console.log(chalk.dim('\n Remediation steps:'));
936
- console.log(
937
- chalk.dim(' 1. Run `aiready scan` locally to see detailed issues')
938
- );
939
- console.log(chalk.dim(' 2. Fix the critical issues before merging'));
940
- console.log(
941
- chalk.dim(
942
- ' 3. Consider upgrading to Team plan for historical tracking: https://getaiready.dev/pricing'
943
- )
944
- );
395
+ console.log(chalk.red(`\n🚫 PR BLOCKED: ${failReason}`));
945
396
  process.exit(1);
946
397
  } else {
947
- console.log(chalk.green('\nāœ… PR PASSED: AI Readiness Check'));
948
- if (threshold) {
949
- console.log(
950
- chalk.green(
951
- ` Score: ${scoringResult.overall}/100 (threshold: ${threshold})`
952
- )
953
- );
954
- }
955
- console.log(
956
- chalk.dim(
957
- '\n šŸ’” Track historical trends: https://getaiready.dev — Team plan $99/mo'
958
- )
959
- );
398
+ console.log(chalk.green('\nāœ… PR PASSED'));
960
399
  }
961
400
  }
962
401
  } catch (error) {
@@ -964,33 +403,4 @@ export async function scanAction(directory: string, options: ScanOptions) {
964
403
  }
965
404
  }
966
405
 
967
- export const scanHelpText = `
968
- EXAMPLES:
969
- $ aiready scan # Analyze all tools
970
- $ aiready scan --tools patterns,context # Skip consistency
971
- $ aiready scan --profile agentic # Optimize for AI agent execution
972
- $ aiready scan --profile security # Optimize for secure coding (testability)
973
- $ aiready scan --compare-to prev-report.json # Compare trends against previous run
974
- $ aiready scan --score --threshold 75 # CI/CD with threshold
975
- $ aiready scan --ci --threshold 70 # GitHub Actions gatekeeper
976
- $ aiready scan --ci --fail-on major # Fail on major+ issues
977
- $ aiready scan --output json --output-file report.json
978
- $ aiready scan --upload --api-key ar_... # Automatic platform upload
979
- $ aiready scan --upload --server custom-url.com # Upload to custom platform
980
-
981
- PROFILES:
982
- agentic: aiSignalClarity, grounding, testability
983
- cost: patterns, context
984
- security: consistency, testability
985
- onboarding: context, consistency, grounding
986
-
987
- CI/CD INTEGRATION (Gatekeeper Mode):
988
- Use --ci for GitHub Actions integration:
989
- - Outputs GitHub Actions annotations for PR checks
990
- - Fails with exit code 1 if threshold not met
991
- - Shows clear "blocked" message with remediation steps
992
-
993
- Example GitHub Actions workflow:
994
- - name: AI Readiness Check
995
- run: aiready scan --ci --threshold 70
996
- `;
406
+ export const scanHelpText = `...`;