@aiready/cli 0.9.41 → 0.9.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -126,7 +126,8 @@ async function analyzeUnified(options) {
126
126
  const report = await analyzeDocDrift({
127
127
  rootDir: options.rootDir,
128
128
  include: options.include,
129
- exclude: options.exclude
129
+ exclude: options.exclude,
130
+ onProgress: options.onProgress
130
131
  });
131
132
  if (options.progressCallback) {
132
133
  options.progressCallback({ tool: "doc-drift", data: report });
@@ -139,7 +140,8 @@ async function analyzeUnified(options) {
139
140
  const report = await analyzeDeps({
140
141
  rootDir: options.rootDir,
141
142
  include: options.include,
142
- exclude: options.exclude
143
+ exclude: options.exclude,
144
+ onProgress: options.onProgress
143
145
  });
144
146
  if (options.progressCallback) {
145
147
  options.progressCallback({ tool: "deps-health", data: report });
@@ -152,7 +154,8 @@ async function analyzeUnified(options) {
152
154
  const report = await analyzeAiSignalClarity({
153
155
  rootDir: options.rootDir,
154
156
  include: options.include,
155
- exclude: options.exclude
157
+ exclude: options.exclude,
158
+ onProgress: options.onProgress
156
159
  });
157
160
  if (options.progressCallback) {
158
161
  options.progressCallback({ tool: "aiSignalClarity", data: report });
@@ -168,7 +171,8 @@ async function analyzeUnified(options) {
168
171
  const report = await analyzeAgentGrounding({
169
172
  rootDir: options.rootDir,
170
173
  include: options.include,
171
- exclude: options.exclude
174
+ exclude: options.exclude,
175
+ onProgress: options.onProgress
172
176
  });
173
177
  if (options.progressCallback) {
174
178
  options.progressCallback({ tool: "grounding", data: report });
@@ -181,7 +185,8 @@ async function analyzeUnified(options) {
181
185
  const report = await analyzeTestability({
182
186
  rootDir: options.rootDir,
183
187
  include: options.include,
184
- exclude: options.exclude
188
+ exclude: options.exclude,
189
+ onProgress: options.onProgress
185
190
  });
186
191
  if (options.progressCallback) {
187
192
  options.progressCallback({ tool: "testability", data: report });
@@ -194,7 +199,8 @@ async function analyzeUnified(options) {
194
199
  const report = await analyzeChangeAmplification({
195
200
  rootDir: options.rootDir,
196
201
  include: options.include,
197
- exclude: options.exclude
202
+ exclude: options.exclude,
203
+ onProgress: options.onProgress
198
204
  });
199
205
  if (options.progressCallback) {
200
206
  options.progressCallback({ tool: "changeAmplification", data: report });
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  analyzeUnified,
3
3
  generateUnifiedSummary
4
- } from "./chunk-HLBKROD3.mjs";
4
+ } from "./chunk-LLJMKNBI.mjs";
5
5
  export {
6
6
  analyzeUnified,
7
7
  generateUnifiedSummary
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiready/cli",
3
- "version": "0.9.41",
3
+ "version": "0.9.45",
4
4
  "description": "Unified CLI for AIReady analysis tools",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -11,17 +11,17 @@
11
11
  "dependencies": {
12
12
  "chalk": "^5.3.0",
13
13
  "commander": "^14.0.0",
14
- "@aiready/agent-grounding": "0.1.6",
15
- "@aiready/context-analyzer": "0.9.36",
16
- "@aiready/core": "0.9.33",
17
- "@aiready/consistency": "0.8.32",
18
- "@aiready/deps": "0.1.6",
19
- "@aiready/doc-drift": "0.1.6",
20
- "@aiready/change-amplification": "0.1.6",
21
- "@aiready/ai-signal-clarity": "0.1.6",
22
- "@aiready/pattern-detect": "0.11.32",
23
- "@aiready/visualizer": "0.1.38",
24
- "@aiready/testability": "0.1.6"
14
+ "@aiready/core": "0.9.37",
15
+ "@aiready/deps": "0.1.10",
16
+ "@aiready/doc-drift": "0.1.10",
17
+ "@aiready/agent-grounding": "0.1.10",
18
+ "@aiready/ai-signal-clarity": "0.1.10",
19
+ "@aiready/visualizer": "0.1.42",
20
+ "@aiready/consistency": "0.8.36",
21
+ "@aiready/context-analyzer": "0.9.40",
22
+ "@aiready/testability": "0.1.10",
23
+ "@aiready/pattern-detect": "0.11.36",
24
+ "@aiready/change-amplification": "0.1.10"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^24.0.0",
package/src/cli.ts CHANGED
@@ -16,6 +16,8 @@ import {
16
16
  visualizeHelpText,
17
17
  visualiseHelpText,
18
18
  changeAmplificationAction,
19
+ uploadAction,
20
+ uploadHelpText,
19
21
  } from './commands';
20
22
 
21
23
  const getDirname = () => {
@@ -111,6 +113,9 @@ program
111
113
  'Fail on issues: critical, major, any',
112
114
  'critical'
113
115
  )
116
+ .option('--api-key <key>', 'Platform API key for automatic upload')
117
+ .option('--upload', 'Automatically upload results to the platform')
118
+ .option('--server <url>', 'Custom platform URL')
114
119
  .addHelpText('after', scanHelpText)
115
120
  .action(async (directory, options) => {
116
121
  await scanAction(directory, options);
@@ -274,4 +279,17 @@ program
274
279
  await changeAmplificationAction(directory, options);
275
280
  });
276
281
 
282
+ // Upload command - Upload report JSON to platform
283
+ program
284
+ .command('upload')
285
+ .description('Upload an AIReady report JSON to the platform')
286
+ .argument('<file>', 'Report JSON file to upload')
287
+ .option('--api-key <key>', 'Platform API key')
288
+ .option('--repo-id <id>', 'Platform repository ID (optional)')
289
+ .option('--server <url>', 'Custom platform URL')
290
+ .addHelpText('after', uploadHelpText)
291
+ .action(async (file, options) => {
292
+ await uploadAction(file, options);
293
+ });
294
+
277
295
  program.parse();
@@ -10,7 +10,7 @@ export async function aiSignalClarityAction(
10
10
  directory: string,
11
11
  options: any
12
12
  ): Promise<ToolScoringOutput | undefined> {
13
- const { analyzeAiSignalClarity, calculateHallucinationScore } =
13
+ const { analyzeAiSignalClarity, calculateAiSignalClarityScore } =
14
14
  await import('@aiready/ai-signal-clarity');
15
15
 
16
16
  const config = await loadConfig(directory);
@@ -25,7 +25,7 @@ export async function aiSignalClarityAction(
25
25
  exclude: options.exclude,
26
26
  });
27
27
 
28
- const scoring = calculateHallucinationScore(report);
28
+ const scoring = calculateAiSignalClarityScore(report);
29
29
 
30
30
  if (options.output === 'json') {
31
31
  return scoring;
@@ -38,7 +38,7 @@ export async function consistencyAction(
38
38
 
39
39
  try {
40
40
  // Define defaults
41
- const defaults = {
41
+ const defaults: any = {
42
42
  checkNaming: true,
43
43
  checkPatterns: true,
44
44
  minSeverity: 'info' as const,
@@ -36,7 +36,7 @@ export async function contextAction(
36
36
 
37
37
  try {
38
38
  // Define defaults
39
- const defaults = {
39
+ const defaults: any = {
40
40
  maxDepth: 5,
41
41
  maxContextBudget: 10000,
42
42
  include: undefined,
@@ -15,3 +15,4 @@ export { aiSignalClarityAction } from './ai-signal-clarity';
15
15
  export { agentGroundingAction } from './agent-grounding';
16
16
  export { testabilityAction } from './testability';
17
17
  export { changeAmplificationAction } from './change-amplification';
18
+ export { uploadAction, uploadHelpText } from './upload';
@@ -15,9 +15,13 @@ import {
15
15
  calculateOverallScore,
16
16
  formatScore,
17
17
  formatToolScore,
18
+ calculateTokenBudget,
19
+ estimateCostFromBudget,
20
+ getModelPreset,
18
21
  getRating,
19
22
  getRatingDisplay,
20
23
  parseWeightString,
24
+ getRepoMetadata,
21
25
  type ToolScoringOutput,
22
26
  } from '@aiready/core';
23
27
  import { analyzeUnified } from '../index';
@@ -26,6 +30,7 @@ import {
26
30
  warnIfGraphCapExceeded,
27
31
  truncateArray,
28
32
  } from '../utils/helpers';
33
+ import { uploadAction } from './upload';
29
34
 
30
35
  interface ScanOptions {
31
36
  tools?: string;
@@ -41,6 +46,10 @@ interface ScanOptions {
41
46
  threshold?: string;
42
47
  ci?: boolean;
43
48
  failOn?: string;
49
+ model?: string;
50
+ apiKey?: string;
51
+ upload?: boolean;
52
+ server?: string;
44
53
  }
45
54
 
46
55
  export async function scanAction(directory: string, options: ScanOptions) {
@@ -50,6 +59,9 @@ export async function scanAction(directory: string, options: ScanOptions) {
50
59
  // Resolve directory to absolute path to ensure .aiready/ is created in the right location
51
60
  const resolvedDir = resolvePath(process.cwd(), directory || '.');
52
61
 
62
+ // Extract repo metadata for linkage
63
+ const repoMetadata = getRepoMetadata(resolvedDir);
64
+
53
65
  try {
54
66
  // Define defaults
55
67
  const defaults = {
@@ -74,11 +86,11 @@ export async function scanAction(directory: string, options: ScanOptions) {
74
86
 
75
87
  let profileTools = options.tools
76
88
  ? options.tools.split(',').map((t: string) => {
77
- const tool = t.trim();
78
- if (tool === 'hallucination' || tool === 'hallucination-risk')
79
- return 'aiSignalClarity';
80
- return tool;
81
- })
89
+ const tool = t.trim();
90
+ if (tool === 'hallucination' || tool === 'hallucination-risk')
91
+ return 'aiSignalClarity';
92
+ return tool;
93
+ })
82
94
  : undefined;
83
95
  if (options.profile) {
84
96
  switch (options.profile.toLowerCase()) {
@@ -424,9 +436,21 @@ export async function scanAction(directory: string, options: ScanOptions) {
424
436
  const results = await analyzeUnified({
425
437
  ...finalOptions,
426
438
  progressCallback,
439
+ onProgress: (processed: number, total: number, message: string) => {
440
+ // Clear line and print progress
441
+ process.stdout.write(
442
+ `\r\x1b[K [${processed}/${total}] ${message}...`
443
+ );
444
+ if (processed === total) {
445
+ process.stdout.write('\n'); // Move to next line when done
446
+ }
447
+ },
427
448
  suppressToolConfig: true,
428
449
  });
429
450
 
451
+ // Determine if we need to print a trailing newline because the last tool didn't finish normally or had 0 files
452
+ // But progressCallback already outputs `\n--- TOOL RESULTS ---` so it's fine.
453
+
430
454
  // Summarize tools and results to console
431
455
  console.log(chalk.cyan('\n=== AIReady Run Summary ==='));
432
456
  console.log(
@@ -477,6 +501,18 @@ export async function scanAction(directory: string, options: ScanOptions) {
477
501
  results.duplicates,
478
502
  results.patterns?.length || 0
479
503
  );
504
+
505
+ // Calculate token budget for patterns (waste = duplication)
506
+ const wastedTokens = results.duplicates.reduce((sum: number, d: any) => sum + (d.tokenCost || 0), 0);
507
+ patternScore.tokenBudget = calculateTokenBudget({
508
+ totalContextTokens: wastedTokens * 2, // Estimated context
509
+ wastedTokens: {
510
+ duplication: wastedTokens,
511
+ fragmentation: 0,
512
+ chattiness: 0
513
+ }
514
+ });
515
+
480
516
  toolScores.set('pattern-detect', patternScore);
481
517
  } catch (err) {
482
518
  void err;
@@ -490,6 +526,17 @@ export async function scanAction(directory: string, options: ScanOptions) {
490
526
  try {
491
527
  const ctxSummary = genContextSummary(results.context);
492
528
  const contextScore = calculateContextScore(ctxSummary);
529
+
530
+ // Calculate token budget for context (waste = fragmentation + depth overhead)
531
+ contextScore.tokenBudget = calculateTokenBudget({
532
+ totalContextTokens: ctxSummary.totalTokens,
533
+ wastedTokens: {
534
+ duplication: 0,
535
+ fragmentation: ctxSummary.totalPotentialSavings || 0,
536
+ chattiness: 0
537
+ }
538
+ });
539
+
493
540
  toolScores.set('context-analyzer', contextScore);
494
541
  } catch (err) {
495
542
  void err;
@@ -676,6 +723,49 @@ export async function scanAction(directory: string, options: ScanOptions) {
676
723
  }
677
724
  }
678
725
 
726
+ // Unified Token Budget Analysis
727
+ const totalWastedDuplication = Array.from(toolScores.values())
728
+ .reduce((sum, s) => sum + (s.tokenBudget?.wastedTokens.bySource.duplication || 0), 0);
729
+ const totalWastedFragmentation = Array.from(toolScores.values())
730
+ .reduce((sum, s) => sum + (s.tokenBudget?.wastedTokens.bySource.fragmentation || 0), 0);
731
+ const totalContext = Math.max(...Array.from(toolScores.values()).map(s => s.tokenBudget?.totalContextTokens || 0));
732
+
733
+ if (totalContext > 0) {
734
+ const unifiedBudget = calculateTokenBudget({
735
+ totalContextTokens: totalContext,
736
+ wastedTokens: {
737
+ duplication: totalWastedDuplication,
738
+ fragmentation: totalWastedFragmentation,
739
+ chattiness: 0
740
+ }
741
+ });
742
+
743
+ const targetModel = options.model || 'claude-4.6';
744
+ const modelPreset = getModelPreset(targetModel);
745
+ const costEstimate = estimateCostFromBudget(unifiedBudget, modelPreset);
746
+
747
+ const barWidth = 20;
748
+ const filled = Math.round(unifiedBudget.efficiencyRatio * barWidth);
749
+ const bar = chalk.green('█'.repeat(filled)) + chalk.dim('░'.repeat(barWidth - filled));
750
+
751
+ console.log(chalk.bold('\n📊 AI Token Budget Analysis (v0.13)'));
752
+ console.log(` Efficiency: [${bar}] ${(unifiedBudget.efficiencyRatio * 100).toFixed(0)}%`);
753
+ console.log(` Total Context: ${chalk.bold(unifiedBudget.totalContextTokens.toLocaleString())} tokens`);
754
+ console.log(` Wasted Tokens: ${chalk.red(unifiedBudget.wastedTokens.total.toLocaleString())} (${((unifiedBudget.wastedTokens.total / unifiedBudget.totalContextTokens) * 100).toFixed(1)}%)`);
755
+ console.log(` Waste Breakdown:`);
756
+ console.log(` • Duplication: ${unifiedBudget.wastedTokens.bySource.duplication.toLocaleString()} tokens`);
757
+ console.log(` • Fragmentation: ${unifiedBudget.wastedTokens.bySource.fragmentation.toLocaleString()} tokens`);
758
+ console.log(` Potential Savings: ${chalk.green(unifiedBudget.potentialRetrievableTokens.toLocaleString())} tokens retrievable`);
759
+ console.log(`\n Est. Monthly Cost (${modelPreset.name}): ${chalk.bold('$' + costEstimate.total)} [range: $${costEstimate.range[0]}-$${costEstimate.range[1]}]`);
760
+
761
+ // Attach unified budget to report for JSON persistence
762
+ (scoringResult as any).tokenBudget = unifiedBudget;
763
+ (scoringResult as any).costEstimate = {
764
+ model: modelPreset.name,
765
+ ...costEstimate
766
+ };
767
+ }
768
+
679
769
  // Show concise breakdown; detailed breakdown only if config requests it
680
770
  if (scoringResult.breakdown && scoringResult.breakdown.length > 0) {
681
771
  console.log(chalk.bold('\nTool breakdown:'));
@@ -711,13 +801,26 @@ export async function scanAction(directory: string, options: ScanOptions) {
711
801
  defaultFilename,
712
802
  resolvedDir
713
803
  );
714
- const outputData = { ...results, scoring: scoringResult };
804
+ const outputData = {
805
+ ...results,
806
+ scoring: scoringResult,
807
+ repository: repoMetadata,
808
+ };
715
809
  handleJSONOutput(
716
810
  outputData,
717
811
  outputPath,
718
812
  `✅ Report saved to ${outputPath}`
719
813
  );
720
814
 
815
+ // Automatic Upload
816
+ if (options.upload) {
817
+ console.log(chalk.blue('\n📤 Automatic upload triggered...'));
818
+ await uploadAction(outputPath, {
819
+ apiKey: options.apiKey,
820
+ server: options.server,
821
+ });
822
+ }
823
+
721
824
  // Warn if graph caps may be exceeded
722
825
  await warnIfGraphCapExceeded(outputData, resolvedDir);
723
826
  } else {
@@ -729,11 +832,24 @@ export async function scanAction(directory: string, options: ScanOptions) {
729
832
  defaultFilename,
730
833
  resolvedDir
731
834
  );
732
- const outputData = { ...results, scoring: scoringResult };
835
+ const outputData = {
836
+ ...results,
837
+ scoring: scoringResult,
838
+ repository: repoMetadata,
839
+ };
733
840
 
734
841
  try {
735
842
  writeFileSync(outputPath, JSON.stringify(outputData, null, 2));
736
843
  console.log(chalk.dim(`✅ Report auto-persisted to ${outputPath}`));
844
+
845
+ // Automatic Upload (from auto-persistent report)
846
+ if (options.upload) {
847
+ console.log(chalk.blue('\n📤 Automatic upload triggered...'));
848
+ await uploadAction(outputPath, {
849
+ apiKey: options.apiKey,
850
+ server: options.server,
851
+ });
852
+ }
737
853
  // Warn if graph caps may be exceeded
738
854
  await warnIfGraphCapExceeded(outputData, resolvedDir);
739
855
  } catch (err) {
@@ -885,6 +1001,8 @@ EXAMPLES:
885
1001
  $ aiready scan --ci --threshold 70 # GitHub Actions gatekeeper
886
1002
  $ aiready scan --ci --fail-on major # Fail on major+ issues
887
1003
  $ aiready scan --output json --output-file report.json
1004
+ $ aiready scan --upload --api-key ar_... # Automatic platform upload
1005
+ $ aiready scan --upload --server custom-url.com # Upload to custom platform
888
1006
 
889
1007
  PROFILES:
890
1008
  agentic: aiSignalClarity, grounding, testability
@@ -0,0 +1,87 @@
1
+ import fs from 'fs';
2
+ import path, { resolve as resolvePath } from 'path';
3
+ import chalk from 'chalk';
4
+ import {
5
+ handleCLIError,
6
+ } from '@aiready/core';
7
+
8
+ interface UploadOptions {
9
+ apiKey?: string;
10
+ repoId?: string;
11
+ server?: string;
12
+ }
13
+
14
+ export async function uploadAction(file: string, options: UploadOptions) {
15
+ const startTime = Date.now();
16
+ const filePath = resolvePath(process.cwd(), file);
17
+ const serverUrl = options.server || process.env.AIREADY_SERVER || 'https://dev.platform.getaiready.dev';
18
+ const apiKey = options.apiKey || process.env.AIREADY_API_KEY;
19
+
20
+ if (!apiKey) {
21
+ console.error(chalk.red('❌ API Key is required for upload.'));
22
+ console.log(chalk.dim(' Set AIREADY_API_KEY environment variable or use --api-key flag.'));
23
+ console.log(chalk.dim(' Get an API key from https://getaiready.dev/dashboard'));
24
+ process.exit(1);
25
+ }
26
+
27
+ if (!fs.existsSync(filePath)) {
28
+ console.error(chalk.red(`❌ File not found: ${filePath}`));
29
+ process.exit(1);
30
+ }
31
+
32
+ try {
33
+ console.log(chalk.blue(`🚀 Uploading report to ${serverUrl}...`));
34
+
35
+ // Read the report file
36
+ const reportData = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
37
+
38
+ // Prepare upload payload
39
+ // Note: repoId is optional if the metadata contains it, but for now we'll require it or infer from metadata
40
+ const repoId = options.repoId || reportData.repository?.repoId;
41
+
42
+ const res = await fetch(`${serverUrl}/api/analysis/upload`, {
43
+ method: 'POST',
44
+ headers: {
45
+ 'Content-Type': 'application/json',
46
+ 'Authorization': `Bearer ${apiKey}`,
47
+ },
48
+ body: JSON.stringify({
49
+ data: reportData,
50
+ repoId, // Might be null, server will handle mapping
51
+ }),
52
+ });
53
+
54
+ const result = (await res.json()) as any;
55
+
56
+ if (!res.ok) {
57
+ console.error(chalk.red(`❌ Upload failed: ${result.error || res.statusText}`));
58
+ if (res.status === 401) {
59
+ console.log(chalk.dim(' Hint: Your API key may be invalid or expired.'));
60
+ }
61
+ process.exit(1);
62
+ }
63
+
64
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
65
+ console.log(chalk.green(`\n✅ Upload successful! (${duration}s)`));
66
+ console.log(chalk.cyan(` View results: ${serverUrl}/dashboard`));
67
+
68
+ if (result.analysis) {
69
+ console.log(chalk.dim(` Analysis ID: ${result.analysis.id}`));
70
+ console.log(chalk.dim(` Score: ${result.analysis.aiScore}/100`));
71
+ }
72
+
73
+ } catch (error) {
74
+ handleCLIError(error, 'Upload');
75
+ }
76
+ }
77
+
78
+ export const uploadHelpText = `
79
+ EXAMPLES:
80
+ $ aiready upload report.json --api-key ar_...
81
+ $ aiready upload .aiready/latest.json
82
+ $ AIREADY_API_KEY=ar_... aiready upload report.json
83
+
84
+ ENVIRONMENT VARIABLES:
85
+ AIREADY_API_KEY Your platform API key
86
+ AIREADY_SERVER Custom platform URL (default: https://dev.platform.getaiready.dev)
87
+ `;
package/src/index.ts CHANGED
@@ -169,6 +169,7 @@ export async function analyzeUnified(
169
169
  rootDir: options.rootDir,
170
170
  include: options.include,
171
171
  exclude: options.exclude,
172
+ onProgress: options.onProgress,
172
173
  });
173
174
  if (options.progressCallback) {
174
175
  options.progressCallback({ tool: 'doc-drift', data: report });
@@ -184,6 +185,7 @@ export async function analyzeUnified(
184
185
  rootDir: options.rootDir,
185
186
  include: options.include,
186
187
  exclude: options.exclude,
188
+ onProgress: options.onProgress,
187
189
  });
188
190
  if (options.progressCallback) {
189
191
  options.progressCallback({ tool: 'deps-health', data: report });
@@ -200,6 +202,7 @@ export async function analyzeUnified(
200
202
  rootDir: options.rootDir,
201
203
  include: options.include,
202
204
  exclude: options.exclude,
205
+ onProgress: options.onProgress,
203
206
  });
204
207
  if (options.progressCallback) {
205
208
  options.progressCallback({ tool: 'aiSignalClarity', data: report });
@@ -219,6 +222,7 @@ export async function analyzeUnified(
219
222
  rootDir: options.rootDir,
220
223
  include: options.include,
221
224
  exclude: options.exclude,
225
+ onProgress: options.onProgress,
222
226
  });
223
227
  if (options.progressCallback) {
224
228
  options.progressCallback({ tool: 'grounding', data: report });
@@ -234,6 +238,7 @@ export async function analyzeUnified(
234
238
  rootDir: options.rootDir,
235
239
  include: options.include,
236
240
  exclude: options.exclude,
241
+ onProgress: options.onProgress,
237
242
  });
238
243
  if (options.progressCallback) {
239
244
  options.progressCallback({ tool: 'testability', data: report });
@@ -250,6 +255,7 @@ export async function analyzeUnified(
250
255
  rootDir: options.rootDir,
251
256
  include: options.include,
252
257
  exclude: options.exclude,
258
+ onProgress: options.onProgress,
253
259
  });
254
260
  if (options.progressCallback) {
255
261
  options.progressCallback({ tool: 'changeAmplification', data: report });