@canonry/canonry 5.28.1 → 5.29.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.
Files changed (33) hide show
  1. package/assets/assets/{AuditHistoryPanel-CWd9UN6L.js → AuditHistoryPanel-B_H7nIp7.js} +1 -1
  2. package/assets/assets/{BacklinksPage-B2gPTzyE.js → BacklinksPage-rhms6z2S.js} +1 -1
  3. package/assets/assets/{HistoryPage-xtkG8SZN.js → HistoryPage-CWPETgrG.js} +1 -1
  4. package/assets/assets/MeasurementPropertyPage-D9-54Je9.js +1 -0
  5. package/assets/assets/ProjectPage-eNVzxsZW.js +8 -0
  6. package/assets/assets/{RunRow-DnSo7Nle.js → RunRow-DQPW7FIF.js} +1 -1
  7. package/assets/assets/{RunsPage-FAEuo4Ra.js → RunsPage-BOM_RkNi.js} +1 -1
  8. package/assets/assets/{SettingsPage-C4OZ1HB9.js → SettingsPage-BDnT6wtM.js} +1 -1
  9. package/assets/assets/SiteHealthSection-lcZA1XpM.js +4 -0
  10. package/assets/assets/{TrafficPage-D03S6xF6.js → TrafficPage-D4nGSfXx.js} +2 -2
  11. package/assets/assets/{TrafficSourceDetailPage-DBu6oFFK.js → TrafficSourceDetailPage-CvZcCbic.js} +1 -1
  12. package/assets/assets/{extract-error-message-BuH2CCaJ.js → extract-error-message-BJ3f0StM.js} +1 -1
  13. package/assets/assets/index-Cncchurw.js +86 -0
  14. package/assets/assets/{react-sigma_core.esm.min-CxwZsgxR.js → react-sigma_core.esm.min-c7qzyLLe.js} +1 -1
  15. package/assets/assets/{v2-overview-adapter-DTCKxzZT.js → v2-overview-adapter-Dsd6dWtE.js} +1 -1
  16. package/assets/index.html +1 -1
  17. package/dist/{chunk-DX2JUC5H.js → chunk-ASYPQYVV.js} +3 -3
  18. package/dist/{chunk-KVQHG3WB.js → chunk-FKL5EGOW.js} +6302 -5914
  19. package/dist/{chunk-EP4STEPI.js → chunk-N2SVFXOB.js} +164 -96
  20. package/dist/{chunk-ONTEFAA7.js → chunk-PTTVDWMN.js} +57 -3
  21. package/dist/{chunk-7RKCDOWO.js → chunk-RHZKOXDZ.js} +263 -190
  22. package/dist/{chunk-MPICT6V7.js → chunk-WL2SHCPU.js} +3 -3
  23. package/dist/{chunk-FNGITGDF.js → chunk-XWBQWAVE.js} +2 -2
  24. package/dist/cli.js +134 -123
  25. package/dist/{demo-server-HWWIMGD6.js → demo-server-VGUIDC5T.js} +24 -32
  26. package/dist/index.js +6 -6
  27. package/dist/{intelligence-service-6LDMVI66.js → intelligence-service-IQA4U3RA.js} +2 -2
  28. package/dist/mcp.js +4 -4
  29. package/package.json +12 -12
  30. package/assets/assets/MeasurementPropertyPage-DxIMU3_T.js +0 -1
  31. package/assets/assets/ProjectPage-CMAFZq_s.js +0 -8
  32. package/assets/assets/SiteHealthSection-BzMxlz8-.js +0 -4
  33. package/assets/assets/index-Ba-Zs45G.js +0 -86
package/dist/cli.js CHANGED
@@ -35,11 +35,11 @@ import {
35
35
  trackCliCommandFinished,
36
36
  trackEvent,
37
37
  waitForServerRuntimeStartup
38
- } from "./chunk-EP4STEPI.js";
38
+ } from "./chunk-N2SVFXOB.js";
39
39
  import {
40
40
  autoSyncSkills,
41
41
  formatAutoSyncNotice
42
- } from "./chunk-DX2JUC5H.js";
42
+ } from "./chunk-ASYPQYVV.js";
43
43
  import {
44
44
  configExists,
45
45
  createApiClient,
@@ -56,13 +56,13 @@ import {
56
56
  saveConfig,
57
57
  saveConfigPatch,
58
58
  shouldPrintServeSkillsNudge
59
- } from "./chunk-FNGITGDF.js";
59
+ } from "./chunk-XWBQWAVE.js";
60
60
  import {
61
61
  DEFAULT_DEMO_TRUSTED_PROXIES,
62
62
  trustedProxiesProblem,
63
63
  trustedProxyProblem
64
64
  } from "./chunk-RB2F4D3Z.js";
65
- import "./chunk-ONTEFAA7.js";
65
+ import "./chunk-PTTVDWMN.js";
66
66
  import {
67
67
  CliError,
68
68
  EXIT_SYSTEM_ERROR,
@@ -74,7 +74,7 @@ import {
74
74
  runMeasurementDraftAction,
75
75
  systemError,
76
76
  usageError
77
- } from "./chunk-MPICT6V7.js";
77
+ } from "./chunk-WL2SHCPU.js";
78
78
  import {
79
79
  CLOUDFLARE_WORKER_BINDINGS,
80
80
  CLOUDFLARE_WORKER_GENERATED_MARKER,
@@ -89,7 +89,7 @@ import {
89
89
  queries,
90
90
  renderReportHtml,
91
91
  runs
92
- } from "./chunk-7RKCDOWO.js";
92
+ } from "./chunk-RHZKOXDZ.js";
93
93
  import {
94
94
  AdsDeliverySnapshotStatuses,
95
95
  AdsHistoricalCampaignRollupStatuses,
@@ -102,11 +102,13 @@ import {
102
102
  CheckScopes,
103
103
  CheckStatuses,
104
104
  CitationStates,
105
+ GaMoverChangeBases,
105
106
  MeasurementEvidenceShapes,
106
107
  OUTSTANDING_PROVIDER_BATCH_STATUSES,
107
108
  ProviderBatchStatuses,
108
109
  READ_ONLY_SCOPE,
109
110
  REPORT_PERIOD_OPTIONS,
111
+ RatioUnits,
110
112
  ResearchRunStatuses,
111
113
  RunKinds,
112
114
  RunStatuses,
@@ -144,9 +146,9 @@ import {
144
146
  formatGbpMetricLabel,
145
147
  formatIsoDate,
146
148
  formatMicros,
147
- formatRatio,
149
+ formatPercent,
148
150
  formatRunErrorOneLine,
149
- formatWholePercent,
151
+ formatSignedPointDelta,
150
152
  gbpNegativeReviewMaxStarsSchema,
151
153
  googleAdsMetricsWindowSchema,
152
154
  isVisibilityCompareClassMetric,
@@ -185,7 +187,7 @@ import {
185
187
  visibilityCompareSelectionSchema,
186
188
  visibilityReportRequestSchema,
187
189
  winnabilityClassSchema
188
- } from "./chunk-KVQHG3WB.js";
190
+ } from "./chunk-FKL5EGOW.js";
189
191
 
190
192
  // src/cli.ts
191
193
  import { pathToFileURL } from "url";
@@ -450,7 +452,7 @@ function parseDemoTrustedProxies(values) {
450
452
  async function demoCommand(options) {
451
453
  const { host, port } = parseDemoListenOptions(options);
452
454
  const trustProxy = parseDemoTrustedProxies(options.trustProxy);
453
- const { createDemoServer } = await import("./demo-server-HWWIMGD6.js");
455
+ const { createDemoServer } = await import("./demo-server-VGUIDC5T.js");
454
456
  const app = await createDemoServer({ trustProxy });
455
457
  let stopping = false;
456
458
  const stop = () => {
@@ -686,7 +688,7 @@ function formatSummaryAndDomains(project, response) {
686
688
  lines.push(`Target: ${s.targetDomain}`);
687
689
  lines.push(`Linking domains: ${s.totalLinkingDomains}`);
688
690
  lines.push(`Total hosts: ${s.totalHosts}`);
689
- lines.push(`Top-10 share: ${s.top10HostsShare}`);
691
+ lines.push(`Top-10 share: ${formatPercent(Number(s.top10HostsShare))}`);
690
692
  if (s.excludedLinkingDomains !== void 0 && s.excludedLinkingDomains > 0) {
691
693
  lines.push(`Excluded: ${s.excludedLinkingDomains} crawler/proxy domains (${s.excludedHosts ?? 0} hosts)`);
692
694
  }
@@ -1199,7 +1201,7 @@ async function bingCoverage(project, format) {
1199
1201
  console.log(`
1200
1202
  Bing Index Coverage for "${project}"
1201
1203
  `);
1202
- console.log(` SUMMARY: ${pctColor}${summary.indexed} / ${summary.total} pages indexed (${summary.percentage}%)${reset}${unknownNote}
1204
+ console.log(` SUMMARY: ${pctColor}${summary.indexed} / ${summary.total} pages indexed (${formatPercent(summary.percentage, "percent")})${reset}${unknownNote}
1203
1205
  `);
1204
1206
  if (result.indexed.length > 0) {
1205
1207
  console.log(` INDEXED (${result.indexed.length}):`);
@@ -1424,7 +1426,7 @@ async function bingPerformance(project, format) {
1424
1426
  for (const row of rows.slice(0, 50)) {
1425
1427
  const query = row.query.length > 38 ? row.query.slice(0, 35) + "..." : row.query;
1426
1428
  console.log(
1427
- ` ${query.padEnd(40)}${String(row.clicks).padEnd(8)}${String(row.impressions).padEnd(8)}${(row.ctr * 100).toFixed(1).padStart(5)}% ${row.averagePosition.toFixed(1).padStart(5)}`
1429
+ ` ${query.padEnd(40)}${String(row.clicks).padEnd(8)}${String(row.impressions).padEnd(8)}${formatPercent(row.ctr).padStart(6)} ${row.averagePosition.toFixed(1).padStart(5)}`
1428
1430
  );
1429
1431
  }
1430
1432
  if (rows.length > 50) {
@@ -3609,7 +3611,7 @@ async function adsSummary(project, opts) {
3609
3611
  console.log(`Structure: ${result.campaignCount} campaigns / ${result.adGroupCount} ad groups / ${result.adCount} ads`);
3610
3612
  console.log(`Window: ${result.window.from ?? "\u2014"} \u2192 ${result.window.to ?? "\u2014"}${partialDayNote(result.window.inProgressDate)}`);
3611
3613
  console.log(`Impressions: ${result.totals.impressions}`);
3612
- console.log(`Clicks: ${result.totals.clicks}${result.totals.ctr != null ? ` (CTR ${(result.totals.ctr * 100).toFixed(2)}%)` : ""}`);
3614
+ console.log(`Clicks: ${result.totals.clicks}${result.totals.ctr != null ? ` (CTR ${formatPercent(result.totals.ctr)})` : ""}`);
3613
3615
  console.log(`Spend: ${formatMicros(result.totals.spendMicros, result.currencyCode ?? "USD")}${result.totals.cpcMicros != null ? ` (CPC ${formatMicros(result.totals.cpcMicros, result.currencyCode ?? "USD")})` : ""}`);
3614
3616
  console.log(`Last synced: ${result.lastSyncedAt ?? "never"}`);
3615
3617
  }
@@ -4233,8 +4235,7 @@ async function gaTraffic(project, opts) {
4233
4235
  console.log(` Organic Sessions: ${result.totalOrganicSessions}`);
4234
4236
  console.log(` Total Users: ${result.totalUsers ?? "unavailable for this range"}`);
4235
4237
  if (result.aiSessionsDeduped > 0) {
4236
- const share = result.totalSessions > 0 ? Math.round(result.aiSessionsDeduped / result.totalSessions * 100) : 0;
4237
- console.log(` AI Sessions (deduped): ${result.aiSessionsDeduped} (${share}% of total)`);
4238
+ console.log(` AI Sessions (deduped): ${result.aiSessionsDeduped} (${result.aiSharePctDisplay} of total)`);
4238
4239
  if (result.paidAiSessionsDeduped > 0) {
4239
4240
  console.log(` Paid AI: ${result.paidAiSessionsDeduped} (${result.paidAiSharePctDisplay} of total)`);
4240
4241
  }
@@ -4244,13 +4245,13 @@ async function gaTraffic(project, opts) {
4244
4245
  const attrWidth = 12;
4245
4246
  const classWidth = 8;
4246
4247
  console.log(" AI REFERRAL SOURCES");
4247
- console.log(` ${"SOURCE".padEnd(25)} ${"MEDIUM".padEnd(15)} ${"CLASS".padEnd(classWidth)} ${"ATTRIBUTION".padEnd(attrWidth)} ${"SESSIONS".padEnd(10)}`);
4248
- console.log(` ${"\u2500".repeat(25)} ${"\u2500".repeat(15)} ${"\u2500".repeat(classWidth)} ${"\u2500".repeat(attrWidth)} ${"\u2500".repeat(10)}`);
4248
+ console.log(` ${"SOURCE".padEnd(25)} ${"MEDIUM".padEnd(15)} ${"CLASS".padEnd(classWidth)} ${"ATTRIBUTION".padEnd(attrWidth)} ${"SESSIONS".padEnd(10)} SHARE`);
4249
+ console.log(` ${"\u2500".repeat(25)} ${"\u2500".repeat(15)} ${"\u2500".repeat(classWidth)} ${"\u2500".repeat(attrWidth)} ${"\u2500".repeat(10)} ${"\u2500".repeat(6)}`);
4249
4250
  for (const ref of result.aiReferrals) {
4250
4251
  const dimLabel = ref.sourceDimension === "first_user" ? "first-visit" : ref.sourceDimension === "manual_utm" ? "utm" : "session";
4251
4252
  const classLabel = ref.trafficClass === "paid" ? "paid" : "organic";
4252
4253
  console.log(
4253
- ` ${ref.source.padEnd(25)} ${ref.medium.padEnd(15)} ${classLabel.padEnd(classWidth)} ${dimLabel.padEnd(attrWidth)} ${String(ref.sessions).padEnd(10)}`
4254
+ ` ${ref.source.padEnd(25)} ${ref.medium.padEnd(15)} ${classLabel.padEnd(classWidth)} ${dimLabel.padEnd(attrWidth)} ${String(ref.sessions).padEnd(10)} ${formatPercent(ref.share)}`
4254
4255
  );
4255
4256
  }
4256
4257
  console.log();
@@ -4273,16 +4274,15 @@ async function gaTraffic(project, opts) {
4273
4274
  if (result.socialReferrals.length > 0) {
4274
4275
  const chanWidth = 12;
4275
4276
  if (result.socialSessions > 0) {
4276
- const share = result.totalSessions > 0 ? Math.round(result.socialSessions / result.totalSessions * 100) : 0;
4277
- console.log(` Social Sessions: ${result.socialSessions} (${share}% of total)`);
4277
+ console.log(` Social Sessions: ${result.socialSessions} (${result.socialSharePctDisplay} of total)`);
4278
4278
  }
4279
4279
  console.log(" SOCIAL REFERRAL SOURCES");
4280
- console.log(` ${"SOURCE".padEnd(25)} ${"MEDIUM".padEnd(15)} ${"CHANNEL".padEnd(chanWidth)} ${"SESSIONS".padEnd(10)}`);
4281
- console.log(` ${"\u2500".repeat(25)} ${"\u2500".repeat(15)} ${"\u2500".repeat(chanWidth)} ${"\u2500".repeat(10)}`);
4280
+ console.log(` ${"SOURCE".padEnd(25)} ${"MEDIUM".padEnd(15)} ${"CHANNEL".padEnd(chanWidth)} ${"SESSIONS".padEnd(10)} SHARE`);
4281
+ console.log(` ${"\u2500".repeat(25)} ${"\u2500".repeat(15)} ${"\u2500".repeat(chanWidth)} ${"\u2500".repeat(10)} ${"\u2500".repeat(6)}`);
4282
4282
  for (const ref of result.socialReferrals) {
4283
4283
  const chanLabel = ref.channelGroup === "Paid Social" ? "paid" : "organic";
4284
4284
  console.log(
4285
- ` ${ref.source.padEnd(25)} ${ref.medium.padEnd(15)} ${chanLabel.padEnd(chanWidth)} ${String(ref.sessions).padEnd(10)}`
4285
+ ` ${ref.source.padEnd(25)} ${ref.medium.padEnd(15)} ${chanLabel.padEnd(chanWidth)} ${String(ref.sessions).padEnd(10)} ${formatPercent(ref.share)}`
4286
4286
  );
4287
4287
  }
4288
4288
  console.log();
@@ -4290,12 +4290,12 @@ async function gaTraffic(project, opts) {
4290
4290
  if (result.topPages.length > 0) {
4291
4291
  const pageWidth = Math.min(60, Math.max(15, ...result.topPages.map((r) => r.landingPage.length)));
4292
4292
  console.log(` TOP LANDING PAGES`);
4293
- console.log(` ${"PAGE".padEnd(pageWidth)} ${"SESSIONS".padEnd(10)}${"ORGANIC".padEnd(10)}`);
4294
- console.log(` ${"\u2500".repeat(pageWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(10)}`);
4293
+ console.log(` ${"PAGE".padEnd(pageWidth)} ${"SESSIONS".padEnd(10)}${"ORGANIC".padEnd(10)}ORGANIC %`);
4294
+ console.log(` ${"\u2500".repeat(pageWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(10)}${"\u2500".repeat(9)}`);
4295
4295
  for (const row of result.topPages) {
4296
4296
  const page = row.landingPage.length > pageWidth ? row.landingPage.slice(0, pageWidth - 3) + "..." : row.landingPage;
4297
4297
  console.log(
4298
- ` ${page.padEnd(pageWidth)} ${String(row.sessions).padEnd(10)}${String(row.organicSessions).padEnd(10)}`
4298
+ ` ${page.padEnd(pageWidth)} ${String(row.sessions).padEnd(10)}${String(row.organicSessions).padEnd(10)}${formatPercent(row.organicShare)}`
4299
4299
  );
4300
4300
  }
4301
4301
  }
@@ -4488,6 +4488,19 @@ async function gaCoverage(project, format) {
4488
4488
  );
4489
4489
  }
4490
4490
  }
4491
+ function fmtTrend(pct) {
4492
+ return pct === null ? "n/a" : `${pct >= 0 ? "+" : ""}${formatPercent(pct, "percent")}`;
4493
+ }
4494
+ function fmtMoverChange(mover) {
4495
+ switch (mover.changeBasis) {
4496
+ case GaMoverChangeBases.new:
4497
+ return "new";
4498
+ case GaMoverChangeBases["small-base"]:
4499
+ return `${mover.changeSessions > 0 ? "+" : ""}${mover.changeSessions} sessions`;
4500
+ case GaMoverChangeBases.percent:
4501
+ return fmtTrend(mover.changePct);
4502
+ }
4503
+ }
4491
4504
  function windowLine(traffic) {
4492
4505
  if (!traffic.windowStart || !traffic.windowEnd) return null;
4493
4506
  const span = traffic.windowDays === null ? "" : ` (${traffic.windowDays} days)`;
@@ -4516,14 +4529,13 @@ async function gaSocialReferralSummary(project, opts) {
4516
4529
  const trendWindow = windowLine(traffic);
4517
4530
  if (trendWindow) console.log(`${trendWindow}
4518
4531
  `);
4519
- console.log(` Sessions: ${traffic.socialSessions} (${traffic.socialSharePct}% of ${traffic.totalSessions} total)`);
4532
+ console.log(` Sessions: ${traffic.socialSessions} (${traffic.socialSharePctDisplay} of ${traffic.totalSessions} total)`);
4520
4533
  console.log();
4521
- const fmtTrend = (pct4) => pct4 === null ? "n/a" : `${pct4 >= 0 ? "+" : ""}${pct4}%`;
4522
4534
  console.log(` 7d trend: ${fmtTrend(trend.trend7dPct)} (${trend.socialSessions7d} vs ${trend.socialSessionsPrev7d})`);
4523
4535
  console.log(` 30d trend: ${fmtTrend(trend.trend30dPct)} (${trend.socialSessions30d} vs ${trend.socialSessionsPrev30d})`);
4524
4536
  if (trend.biggestMover) {
4525
4537
  const m = trend.biggestMover;
4526
- console.log(` Mover: ${m.source} (${m.changePct >= 0 ? "+" : ""}${m.changePct}%, ${m.sessionsPrev7d}\u2192${m.sessions7d})`);
4538
+ console.log(` Mover: ${m.source} (${fmtMoverChange(m)}, ${m.sessionsPrev7d}\u2192${m.sessions7d})`);
4527
4539
  }
4528
4540
  console.log();
4529
4541
  if (traffic.socialReferrals.length > 0) {
@@ -4552,7 +4564,7 @@ async function gaSocialReferralSummary(project, opts) {
4552
4564
  const socialWindow = windowLine(traffic);
4553
4565
  if (socialWindow) console.log(`${socialWindow}
4554
4566
  `);
4555
- console.log(` Sessions: ${traffic.socialSessions} (${traffic.socialSharePct}% of ${traffic.totalSessions} total)`);
4567
+ console.log(` Sessions: ${traffic.socialSessions} (${traffic.socialSharePctDisplay} of ${traffic.totalSessions} total)`);
4556
4568
  if (traffic.socialReferrals.length > 0) {
4557
4569
  console.log();
4558
4570
  console.log(" TOP SOURCES");
@@ -4565,7 +4577,6 @@ async function gaSocialReferralSummary(project, opts) {
4565
4577
  async function gaAttribution(project, opts) {
4566
4578
  const client = getClient7();
4567
4579
  const traffic = await client.gaTraffic(project);
4568
- const fmtTrend = (pct4) => pct4 === null ? "n/a" : `${pct4 >= 0 ? "+" : ""}${pct4}%`;
4569
4580
  if (opts?.trend) {
4570
4581
  const trend = await client.gaAttributionTrend(project);
4571
4582
  if (isMachineFormat(opts.format)) {
@@ -4625,25 +4636,25 @@ async function gaAttribution(project, opts) {
4625
4636
  console.log(` Total Sessions: ${traffic.totalSessions}`);
4626
4637
  console.log(` Total Users: ${traffic.totalUsers ?? "unavailable for this range"}`);
4627
4638
  console.log();
4628
- console.log(" CHANNEL BREAKDOWN 7d trend 30d trend");
4629
- console.log(` Organic Search: ${String(traffic.channelBreakdown.organic.sessions).padEnd(6)} (${traffic.channelBreakdown.organic.sharePctDisplay.padStart(4)}) ${fmtTrend(trend.organic.trend7dPct).padEnd(12)} ${fmtTrend(trend.organic.trend30dPct)}`);
4630
- console.log(` Social: ${String(traffic.channelBreakdown.social.sessions).padEnd(6)} (${traffic.channelBreakdown.social.sharePctDisplay.padStart(4)}) ${fmtTrend(trend.social.trend7dPct).padEnd(12)} ${fmtTrend(trend.social.trend30dPct)}`);
4631
- console.log(` Direct: ${String(traffic.channelBreakdown.direct.sessions).padEnd(6)} (${traffic.channelBreakdown.direct.sharePctDisplay.padStart(4)}) ${fmtTrend(trend.direct.trend7dPct).padEnd(12)} ${fmtTrend(trend.direct.trend30dPct)}`);
4632
- console.log(` AI Referrals: ${String(traffic.channelBreakdown.ai.sessions).padEnd(6)} (${traffic.channelBreakdown.ai.sharePctDisplay.padStart(4)}) ${fmtTrend(trend.ai.trend7dPct).padEnd(12)} ${fmtTrend(trend.ai.trend30dPct)} (lower bound \u2014 sessionSource only; referrer-stripped traffic falls under Direct)`);
4639
+ console.log(` CHANNEL BREAKDOWN${" ".repeat(20)}7d trend 30d trend`);
4640
+ console.log(` Organic Search: ${String(traffic.channelBreakdown.organic.sessions).padEnd(6)} (${traffic.channelBreakdown.organic.sharePctDisplay.padStart(6)}) ${fmtTrend(trend.organic.trend7dPct).padEnd(12)} ${fmtTrend(trend.organic.trend30dPct)}`);
4641
+ console.log(` Social: ${String(traffic.channelBreakdown.social.sessions).padEnd(6)} (${traffic.channelBreakdown.social.sharePctDisplay.padStart(6)}) ${fmtTrend(trend.social.trend7dPct).padEnd(12)} ${fmtTrend(trend.social.trend30dPct)}`);
4642
+ console.log(` Direct: ${String(traffic.channelBreakdown.direct.sessions).padEnd(6)} (${traffic.channelBreakdown.direct.sharePctDisplay.padStart(6)}) ${fmtTrend(trend.direct.trend7dPct).padEnd(12)} ${fmtTrend(trend.direct.trend30dPct)}`);
4643
+ console.log(` AI Referrals: ${String(traffic.channelBreakdown.ai.sessions).padEnd(6)} (${traffic.channelBreakdown.ai.sharePctDisplay.padStart(6)}) ${fmtTrend(trend.ai.trend7dPct).padEnd(12)} ${fmtTrend(trend.ai.trend30dPct)} (lower bound \u2014 sessionSource only; referrer-stripped traffic falls under Direct)`);
4633
4644
  if (traffic.paidAiSessionsBySession > 0) {
4634
- console.log(` Paid AI: ${String(traffic.paidAiSessionsBySession).padEnd(6)} (${traffic.paidAiSharePctBySessionDisplay.padStart(4)})`);
4645
+ console.log(` Paid AI: ${String(traffic.paidAiSessionsBySession).padEnd(6)} (${traffic.paidAiSharePctBySessionDisplay.padStart(6)})`);
4635
4646
  }
4636
- console.log(` Other: ${String(traffic.channelBreakdown.other.sessions).padEnd(6)} (${traffic.channelBreakdown.other.sharePctDisplay.padStart(4)})`);
4647
+ console.log(` Other: ${String(traffic.channelBreakdown.other.sessions).padEnd(6)} (${traffic.channelBreakdown.other.sharePctDisplay.padStart(6)})`);
4637
4648
  console.log(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
4638
- console.log(` Total: ${String(traffic.totalSessions).padEnd(6)} ${fmtTrend(trend.total.trend7dPct).padEnd(12)} ${fmtTrend(trend.total.trend30dPct)}`);
4649
+ console.log(` Total: ${String(traffic.totalSessions).padEnd(6)}${" ".repeat(13)}${fmtTrend(trend.total.trend7dPct).padEnd(12)} ${fmtTrend(trend.total.trend30dPct)}`);
4639
4650
  if (trend.aiBiggestMover) {
4640
4651
  const m = trend.aiBiggestMover;
4641
4652
  console.log(`
4642
- AI Mover: ${m.source} (${m.changePct >= 0 ? "+" : ""}${m.changePct}%, ${m.sessionsPrev7d}\u2192${m.sessions7d} sessions/7d)`);
4653
+ AI Mover: ${m.source} (${fmtMoverChange(m)}, ${m.sessionsPrev7d}\u2192${m.sessions7d} sessions/7d)`);
4643
4654
  }
4644
4655
  if (trend.socialBiggestMover) {
4645
4656
  const m = trend.socialBiggestMover;
4646
- console.log(` Social Mover: ${m.source} (${m.changePct >= 0 ? "+" : ""}${m.changePct}%, ${m.sessionsPrev7d}\u2192${m.sessions7d} sessions/7d)`);
4657
+ console.log(` Social Mover: ${m.source} (${fmtMoverChange(m)}, ${m.sessionsPrev7d}\u2192${m.sessions7d} sessions/7d)`);
4647
4658
  }
4648
4659
  if (traffic.lastSyncedAt) {
4649
4660
  console.log(`
@@ -5150,7 +5161,7 @@ async function gbpKeywords(project, opts) {
5150
5161
  console.log("No GBP keyword impressions stored. Run `canonry gbp sync <project>` first.");
5151
5162
  return;
5152
5163
  }
5153
- console.log(`${response.total} keyword(s), ${response.thresholdedPct}% privacy-thresholded. Top by impressions:`);
5164
+ console.log(`${response.total} keyword(s), ${formatPercent(response.thresholdedPct, "percent")} privacy-thresholded. Top by impressions:`);
5154
5165
  for (const k of response.keywords.slice(0, 15)) {
5155
5166
  const val = k.valueCount !== null ? String(k.valueCount) : `<${k.valueThreshold ?? "?"}`;
5156
5167
  console.log(` ${val.padStart(8)} ${k.keyword}`);
@@ -5271,9 +5282,9 @@ ${response.total} ${label}${shown}:`);
5271
5282
  console.log(` ${r.updateTime.slice(0, 10)} ${stars}${where} ${r.reviewerName ?? "Anonymous"} ${text} [${r.origin}, ${r.alertState}]`);
5272
5283
  }
5273
5284
  }
5274
- function fmtDelta(pct4) {
5275
- if (pct4 === null) return "n/a";
5276
- return `${pct4 >= 0 ? "+" : ""}${pct4}%`;
5285
+ function fmtDelta(pct) {
5286
+ if (pct === null) return "n/a";
5287
+ return `${pct >= 0 ? "+" : ""}${formatPercent(pct, "percent")}`;
5277
5288
  }
5278
5289
  async function gbpSummary(project, opts) {
5279
5290
  const client = getClient8();
@@ -5299,7 +5310,7 @@ async function gbpSummary(project, opts) {
5299
5310
  }
5300
5311
  }
5301
5312
  console.log(`
5302
- Keywords: ${s.keywords.total} tracked, ${s.keywords.thresholdedPct}% privacy-thresholded`);
5313
+ Keywords: ${s.keywords.total} tracked, ${formatPercent(s.keywords.thresholdedPct, "percent")} privacy-thresholded`);
5303
5314
  console.log(`Place actions: ${s.placeActions.total} CTA(s) \u2014 reservation:${s.placeActions.hasReservationCta ? "yes" : "no"} booking:${s.placeActions.hasBookingCta ? "yes" : "no"} direct-merchant:${s.placeActions.hasDirectMerchantCta ? "yes" : "no"}`);
5304
5315
  console.log(`Lodging API: ${s.lodging.lodgingLocationCount} profile(s), ${s.lodging.populatedLodgingCount} with readable groups, ${s.lodging.emptyLodgingCount} with 0 readable groups`);
5305
5316
  const pc = s.profileCompleteness;
@@ -5654,13 +5665,10 @@ function performanceMicros(micros, currencyCode) {
5654
5665
  if (currencyCode === null) return (micros / 1e6).toFixed(2);
5655
5666
  return formatMicros(micros, currencyCode);
5656
5667
  }
5657
- function performanceRatio(ratio) {
5658
- return ratio === null ? "\u2014" : `${(ratio * 100).toFixed(2)}%`;
5659
- }
5660
5668
  function performanceChange(ratio) {
5661
5669
  if (ratio === null) return "";
5662
5670
  const sign = ratio > 0 ? "+" : "";
5663
- return ` (${sign}${(ratio * 100).toFixed(1)}% vs prior period)`;
5671
+ return ` (${sign}${formatPercent(ratio)} vs prior period)`;
5664
5672
  }
5665
5673
  async function googleAdsPerformance(client, project, opts) {
5666
5674
  const result = await client.getGoogleAdsPerformance(project, opts.window ? { window: opts.window } : {});
@@ -5676,9 +5684,9 @@ async function googleAdsPerformance(client, project, opts) {
5676
5684
  console.log(`As of: ${result.source.asOfDate}${result.source.openDate ? ` (${result.source.openDate} still open, excluded)` : ""}`);
5677
5685
  console.log(`Account: ${result.source.customerId} (${currency ?? "?"}, ${result.source.timeZone ?? "unknown zone"})`);
5678
5686
  console.log(`Impressions: ${totals.impressions}${performanceChange(comparison?.change.impressions ?? null)}`);
5679
- console.log(`Clicks: ${totals.clicks} (CTR ${performanceRatio(totals.ctr)})${performanceChange(comparison?.change.clicks ?? null)}`);
5687
+ console.log(`Clicks: ${totals.clicks} (CTR ${formatPercent(totals.ctr)})${performanceChange(comparison?.change.clicks ?? null)}`);
5680
5688
  console.log(`Cost: ${performanceMicros(totals.costMicros, currency)} (CPC ${performanceMicros(totals.cpcMicros, currency)})${performanceChange(comparison?.change.costMicros ?? null)}`);
5681
- console.log(`Conversions: ${totals.conversions} (rate ${performanceRatio(totals.conversionRate)}, cost/conv ${performanceMicros(totals.costPerConversionMicros, currency)})`);
5689
+ console.log(`Conversions: ${totals.conversions} (rate ${formatPercent(totals.conversionRate)}, cost/conv ${performanceMicros(totals.costPerConversionMicros, currency)})`);
5682
5690
  if (!comparison) {
5683
5691
  console.log(`Comparison: unavailable (${result.comparisonUnavailableReason ?? "unknown"})`);
5684
5692
  }
@@ -8296,7 +8304,7 @@ async function googlePerformanceDaily(project, opts) {
8296
8304
  }
8297
8305
  console.log(` Clicks: ${clicks.toLocaleString()}`);
8298
8306
  console.log(` Impressions: ${impressions.toLocaleString()}`);
8299
- console.log(` CTR: ${(ctr * 100).toFixed(2)}%`);
8307
+ console.log(` CTR: ${formatPercent(ctr)}`);
8300
8308
  console.log(` Position: ${position == null ? "\u2014" : position.toFixed(1)}`);
8301
8309
  const fits = data.trends;
8302
8310
  const trendLines = [];
@@ -8325,7 +8333,7 @@ Trend (least-squares fit over ${days} day${days === 1 ? "" : "s"}):`);
8325
8333
  No period comparison: period comparison needs property-level daily data. Sync again, or pick a range covered by property totals.`
8326
8334
  );
8327
8335
  } else if (cmp) {
8328
- const pct4 = (ratio, prior, trailing, inverted) => {
8336
+ const pct = (ratio, prior, trailing, inverted) => {
8329
8337
  if (ratio === null) {
8330
8338
  if (prior !== null && trailing !== null && prior <= 0 && trailing > 0) {
8331
8339
  return `new in last ${cmp.days} day${cmp.days === 1 ? "" : "s"}`;
@@ -8336,9 +8344,7 @@ No period comparison: period comparison needs property-level daily data. Sync ag
8336
8344
  }
8337
8345
  if (ratio === 0) return "no change";
8338
8346
  const better = inverted ? ratio < 0 : ratio > 0;
8339
- const magnitude = Math.abs(ratio * 100);
8340
- const shown = magnitude < 0.1 ? "<0.1" : magnitude.toFixed(1);
8341
- return `${ratio > 0 ? "+" : "-"}${shown}% ${better ? "better" : "worse"}`;
8347
+ return `${ratio > 0 ? "+" : "-"}${formatPercent(Math.abs(ratio))} ${better ? "better" : "worse"}`;
8342
8348
  };
8343
8349
  console.log(
8344
8350
  `
@@ -8355,7 +8361,7 @@ Last ${cmp.days} day${cmp.days === 1 ? "" : "s"} (${cmp.trailing.startDate} to $
8355
8361
  ["CTR", cmp.change.ctr, cmp.prior.ctr, cmp.trailing.ctr, false],
8356
8362
  ["Position", cmp.change.position, cmp.prior.position, cmp.trailing.position, true]
8357
8363
  ]) {
8358
- console.log(` ${`${label}:`.padEnd(13)}${pct4(ratio, prior, trailing, inverted)}`);
8364
+ console.log(` ${`${label}:`.padEnd(13)}${pct(ratio, prior, trailing, inverted)}`);
8359
8365
  }
8360
8366
  }
8361
8367
  console.log();
@@ -8363,7 +8369,7 @@ Last ${cmp.days} day${cmp.days === 1 ? "" : "s"} (${cmp.trailing.startDate} to $
8363
8369
  console.log(` ${"\u2500".repeat(12)}${"\u2500".repeat(10)}${"\u2500".repeat(12)}${"\u2500".repeat(10)}${"\u2500".repeat(9)}`);
8364
8370
  for (const row of data.daily) {
8365
8371
  console.log(
8366
- ` ${row.date.padEnd(12)}${row.clicks.toLocaleString().padStart(10)}${row.impressions.toLocaleString().padStart(12)}${(row.ctr * 100).toFixed(2).padStart(9)}%${(row.position == null ? "\u2014" : row.position.toFixed(1)).padStart(9)}`
8372
+ ` ${row.date.padEnd(12)}${row.clicks.toLocaleString().padStart(10)}${row.impressions.toLocaleString().padStart(12)}${formatPercent(row.ctr).padStart(10)}${(row.position == null ? "\u2014" : row.position.toFixed(1)).padStart(9)}`
8367
8373
  );
8368
8374
  }
8369
8375
  }
@@ -8394,7 +8400,7 @@ async function googleTopPages(project, opts) {
8394
8400
  for (const row of data.rows) {
8395
8401
  const page = row.page.length > pageWidth ? row.page.slice(0, pageWidth - 3) + "..." : row.page;
8396
8402
  console.log(
8397
- ` ${page.padEnd(pageWidth)}${row.clicks.toLocaleString().padStart(10)}${row.impressions.toLocaleString().padStart(12)}${(row.ctr * 100).toFixed(2).padStart(9)}%`
8403
+ ` ${page.padEnd(pageWidth)}${row.clicks.toLocaleString().padStart(10)}${row.impressions.toLocaleString().padStart(12)}${formatPercent(row.ctr).padStart(10)}`
8398
8404
  );
8399
8405
  }
8400
8406
  console.log();
@@ -8403,7 +8409,7 @@ async function googleTopPages(project, opts) {
8403
8409
  console.log(`Property total (${days} day${days === 1 ? "" : "s"}, source: ${data.totalsSource}):`);
8404
8410
  console.log(` Clicks: ${clicks.toLocaleString()}`);
8405
8411
  console.log(` Impressions: ${impressions.toLocaleString()}`);
8406
- console.log(` CTR: ${(ctr * 100).toFixed(2)}%`);
8412
+ console.log(` CTR: ${formatPercent(ctr)}`);
8407
8413
  console.log();
8408
8414
  console.log(" The page rows above are a ranking. They do not add up to this total:");
8409
8415
  console.log(" Google withholds rare queries and repeats an impression per page.");
@@ -8450,7 +8456,7 @@ async function googleQueryTotals(project, opts) {
8450
8456
  for (const row of rows.slice(0, 50)) {
8451
8457
  const query = row.query.length > 38 ? row.query.slice(0, 35) + "..." : row.query;
8452
8458
  console.log(
8453
- ` ${query.padEnd(40)}${row.clicks.toLocaleString().padStart(8)}${row.impressions.toLocaleString().padStart(10)}${`${(row.ctr * 100).toFixed(1)}%`.padStart(8)}${row.position.toFixed(1).padStart(7)}${String(row.days).padStart(6)}${showSource ? ` ${row.source}` : ""}`
8459
+ ` ${query.padEnd(40)}${row.clicks.toLocaleString().padStart(8)}${row.impressions.toLocaleString().padStart(10)}${formatPercent(row.ctr).padStart(8)}${row.position.toFixed(1).padStart(7)}${String(row.days).padStart(6)}${showSource ? ` ${row.source}` : ""}`
8454
8460
  );
8455
8461
  }
8456
8462
  if (rows.length > 50) {
@@ -8518,7 +8524,7 @@ async function googlePerformance(project, opts) {
8518
8524
  for (const row of rows.slice(0, 50)) {
8519
8525
  const query = row.query.length > 28 ? row.query.slice(0, 25) + "..." : row.query;
8520
8526
  console.log(
8521
- ` ${row.date.padEnd(12)}${query.padEnd(30)}${String(row.clicks).padEnd(8)}${String(row.impressions).padEnd(8)}${(row.ctr * 100).toFixed(1).padStart(5)}% ${row.position.toFixed(1).padStart(5)}`
8527
+ ` ${row.date.padEnd(12)}${query.padEnd(30)}${String(row.clicks).padEnd(8)}${String(row.impressions).padEnd(8)}${formatPercent(row.ctr).padStart(6)} ${row.position.toFixed(1).padStart(5)}`
8522
8528
  );
8523
8529
  }
8524
8530
  if (rows.length > 50) {
@@ -8594,10 +8600,12 @@ async function googleCoverage(project, format) {
8594
8600
  }
8595
8601
  const pctColor = summary.percentage >= 80 ? "\x1B[32m" : summary.percentage >= 50 ? "\x1B[33m" : "\x1B[31m";
8596
8602
  const reset = "\x1B[0m";
8603
+ const legacyServer = summary.indexedShare === void 0;
8604
+ const shareText = legacyServer ? `(${formatPercent(summary.percentage, "percent")})` : `(${formatPercent(summary.indexedShare)}) \xB7 ${summary.notIndexed} not indexed (${formatPercent(summary.notIndexedShare)})`;
8597
8605
  console.log(`
8598
8606
  Index Coverage for "${project}"
8599
8607
  `);
8600
- console.log(` SUMMARY: ${pctColor}${summary.indexed} / ${summary.total} pages indexed (${summary.percentage}%)${reset}
8608
+ console.log(` SUMMARY: ${pctColor}${summary.indexed} / ${summary.total} pages indexed ${shareText}${reset}
8601
8609
  `);
8602
8610
  if (result.indexed.length > 0) {
8603
8611
  console.log(` INDEXED (${result.indexed.length}):`);
@@ -10582,7 +10590,7 @@ function unattributedText(metric) {
10582
10590
  }
10583
10591
  function metricText(metric) {
10584
10592
  if (metric.state === "unavailable") return METRIC_REASONS[metric.reason] ?? `not measured (${metric.reason})`;
10585
- const percent = `${Math.round(metric.value * 100)}%`;
10593
+ const percent = formatPercent(metric.value);
10586
10594
  return metric.numerator === void 0 || metric.denominator === void 0 ? percent : `${metric.numerator} of ${metric.denominator} (${percent})`;
10587
10595
  }
10588
10596
  async function showMeasurementProperty(project, opts) {
@@ -11484,7 +11492,6 @@ function printMetrics(data) {
11484
11492
  console.log(`
11485
11493
  Citation Rate Trends (${data.window})`);
11486
11494
  console.log("\u2500".repeat(50));
11487
- const pct4 = (n) => `${(n * 100).toFixed(1)}%`;
11488
11495
  const modelChangeNotice = buildModelChangeNotice(readModelPointerChanges(data));
11489
11496
  if (modelChangeNotice?.kind === "change") {
11490
11497
  console.log(`
@@ -11492,13 +11499,14 @@ Citation Rate Trends (${data.window})`);
11492
11499
  console.log(` ${modelChangeNotice.text}`);
11493
11500
  console.log("");
11494
11501
  }
11495
- console.log(` Overall: ${pct4(data.overall.citationRate)} (${data.overall.cited}/${data.overall.total})`);
11502
+ console.log(` Overall: ${formatPercent(data.overall.citationRate)} (${data.overall.cited}/${data.overall.total})`);
11496
11503
  console.log(` Trend: ${data.trend}`);
11504
+ printWindowChange(data);
11497
11505
  if (Object.keys(data.byProvider).length > 0) {
11498
11506
  console.log(`
11499
11507
  By Provider:`);
11500
11508
  for (const [provider, metric] of Object.entries(data.byProvider)) {
11501
- console.log(` ${provider.padEnd(10)} ${pct4(metric.citationRate).padStart(6)} (${metric.cited}/${metric.total})`);
11509
+ console.log(` ${provider.padEnd(10)} ${formatPercent(metric.citationRate).padStart(6)} (${metric.cited}/${metric.total})`);
11502
11510
  }
11503
11511
  }
11504
11512
  if (data.buckets.length > 0) {
@@ -11506,7 +11514,7 @@ Citation Rate Trends (${data.window})`);
11506
11514
  Timeline (dates in UTC):`);
11507
11515
  for (const bucket of data.buckets) {
11508
11516
  const bar = bucket.total > 0 ? "\u2588".repeat(Math.round(bucket.citationRate * 20)) : "";
11509
- console.log(` ${bucketDates(bucket).padEnd(BUCKET_DATE_WIDTH)} ${pct4(bucket.citationRate).padStart(6)} ${bar}`);
11517
+ console.log(` ${bucketDates(bucket).padEnd(BUCKET_DATE_WIDTH)} ${formatPercent(bucket.citationRate).padStart(6)} ${bar}`);
11510
11518
  }
11511
11519
  }
11512
11520
  const providersInBuckets = [...new Set(data.buckets.flatMap((b) => Object.keys(b.byProvider ?? {})))].sort();
@@ -11519,7 +11527,7 @@ Citation Rate Trends (${data.window})`);
11519
11527
  const metric = bucket.byProvider?.[provider];
11520
11528
  if (!metric) continue;
11521
11529
  const bar = metric.total > 0 ? "\u2588".repeat(Math.round(metric.citationRate * 20)) : "";
11522
- console.log(` ${bucketDates(bucket).padEnd(BUCKET_DATE_WIDTH)} ${pct4(metric.citationRate).padStart(6)} ${bar}`);
11530
+ console.log(` ${bucketDates(bucket).padEnd(BUCKET_DATE_WIDTH)} ${formatPercent(metric.citationRate).padStart(6)} ${bar}`);
11523
11531
  }
11524
11532
  }
11525
11533
  }
@@ -11567,6 +11575,22 @@ Citation Rate Trends (${data.window})`);
11567
11575
  }
11568
11576
  }
11569
11577
  }
11578
+ function printWindowChange(data) {
11579
+ const windowChange = data.windowChange;
11580
+ if (!windowChange) return;
11581
+ const shareScope = data.mentionShareScope === "non-brand" ? "non-brand queries" : "pooled queries \xB7 classification unavailable";
11582
+ const series = [
11583
+ ["Mentioned", windowChange.mentionRate],
11584
+ ["Cited", windowChange.citationRate],
11585
+ [`Mention share \xB7 ${shareScope}`, windowChange.mentionShare]
11586
+ ];
11587
+ console.log(`
11588
+ Change Across the Window (first \u2192 latest bucket):`);
11589
+ for (const [label, change] of series) {
11590
+ const text = change ? `${formatPercent(change.first)} \u2192 ${formatPercent(change.latest)} (${formatSignedPointDelta(change.delta)})` : "fewer than two buckets to compare";
11591
+ console.log(` ${label}: ${text}`);
11592
+ }
11593
+ }
11570
11594
  function readServedModelAttribution(data) {
11571
11595
  const legacyCompatible = data;
11572
11596
  return legacyCompatible.servedModelAttribution ?? {};
@@ -11626,9 +11650,8 @@ Source Origin Breakdown`);
11626
11650
  return;
11627
11651
  }
11628
11652
  for (const cat of data.overall) {
11629
- const pct4 = `${(cat.percentage * 100).toFixed(1)}%`;
11630
11653
  const domains = cat.topDomains.slice(0, 3).map((d) => d.domain).join(", ");
11631
- console.log(` ${cat.label.padEnd(20)} ${pct4.padStart(6)} (${cat.count}) ${domains}`);
11654
+ console.log(` ${cat.label.padEnd(20)} ${formatPercent(cat.percentage).padStart(6)} (${cat.count}) ${domains}`);
11632
11655
  }
11633
11656
  }
11634
11657
 
@@ -11659,7 +11682,6 @@ Usage: ${SOURCES_USAGE}`, {
11659
11682
  details: { command: "sources", usage: SOURCES_USAGE, option: "include-by-query", value }
11660
11683
  });
11661
11684
  }
11662
- var pct = (n) => `${(n * 100).toFixed(1)}%`;
11663
11685
  async function showSources(project, options) {
11664
11686
  if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit <= 0)) {
11665
11687
  throw usageError(`Error: --limit must be a positive integer
@@ -11731,12 +11753,12 @@ function describeScope(data) {
11731
11753
  function printSurfaceClasses(list) {
11732
11754
  for (const c of list.bySurfaceClass) {
11733
11755
  const noun = c.domainCount === 1 ? "domain" : "domains";
11734
- console.log(` ${c.label.padEnd(28)} ${pct(c.percentage).padStart(6)} (${c.count}) ${c.domainCount} ${noun}`);
11756
+ console.log(` ${c.label.padEnd(28)} ${formatPercent(c.percentage).padStart(6)} (${c.count}) ${c.domainCount} ${noun}`);
11735
11757
  }
11736
11758
  }
11737
11759
  function printRankedEntries(list, indent = "") {
11738
11760
  for (const e of list.entries) {
11739
- console.log(`${indent} ${e.domain.padEnd(32)} ${String(e.count).padStart(4)} ${pct(e.percentage).padStart(6)} ${e.surfaceClass}`);
11761
+ console.log(`${indent} ${e.domain.padEnd(32)} ${String(e.count).padStart(4)} ${formatPercent(e.percentage).padStart(6)} ${e.surfaceClass}`);
11740
11762
  }
11741
11763
  if (list.truncatedDomainCount > 0) {
11742
11764
  console.log(`${indent} \u2026 +${list.truncatedDomainCount} more domains (${list.truncatedCitedSlots} cited slots)`);
@@ -14670,9 +14692,9 @@ async function showHealth(project, opts) {
14670
14692
  console.log("Date Mention Rate Mentioned/Total Cited Rate Cited/Total");
14671
14693
  console.log("\u2500".repeat(86));
14672
14694
  for (const snap of snapshots) {
14673
- const mRate = (snap.overallMentionRate * 100).toFixed(1).padStart(5) + "%";
14695
+ const mRate = formatPercent(snap.overallMentionRate).padStart(6);
14674
14696
  const mRatio = `${snap.mentionedPairs}/${snap.totalPairs}`.padEnd(15);
14675
- const cRate = (snap.overallCitedRate * 100).toFixed(1).padStart(5) + "%";
14697
+ const cRate = formatPercent(snap.overallCitedRate).padStart(6);
14676
14698
  const cRatio = `${snap.citedPairs}/${snap.totalPairs}`;
14677
14699
  const date = snap.createdAt.slice(0, 19).padEnd(25);
14678
14700
  console.log(`${date} ${mRate} ${mRatio} ${cRate} ${cRatio}`);
@@ -14688,17 +14710,13 @@ async function showHealth(project, opts) {
14688
14710
  console.log("No health data yet \u2014 run a sweep first (canonry run <project>).");
14689
14711
  return;
14690
14712
  }
14691
- const mentionRate = (health.overallMentionRate * 100).toFixed(1);
14692
- const citedRate = (health.overallCitedRate * 100).toFixed(1);
14693
- console.log(`Health: ${mentionRate}% mentioned (${health.mentionedPairs}/${health.totalPairs} pairs)`);
14694
- console.log(` ${citedRate}% cited (${health.citedPairs}/${health.totalPairs} pairs)`);
14713
+ console.log(`Health: ${formatPercent(health.overallMentionRate)} mentioned (${health.mentionedPairs}/${health.totalPairs} pairs)`);
14714
+ console.log(` ${formatPercent(health.overallCitedRate)} cited (${health.citedPairs}/${health.totalPairs} pairs)`);
14695
14715
  console.log("");
14696
14716
  if (health.providerBreakdown && Object.keys(health.providerBreakdown).length > 0) {
14697
14717
  console.log("Provider Breakdown:");
14698
14718
  for (const [provider, stats] of Object.entries(health.providerBreakdown)) {
14699
- const pMention = (stats.mentionRate * 100).toFixed(1);
14700
- const pCited = (stats.citedRate * 100).toFixed(1);
14701
- console.log(` ${provider.padEnd(15)} ${pMention}% mentioned (${stats.mentioned}/${stats.total}) ${pCited}% cited (${stats.cited}/${stats.total})`);
14719
+ console.log(` ${provider.padEnd(15)} ${formatPercent(stats.mentionRate)} mentioned (${stats.mentioned}/${stats.total}) ${formatPercent(stats.citedRate)} cited (${stats.cited}/${stats.total})`);
14702
14720
  }
14703
14721
  }
14704
14722
  }
@@ -14798,8 +14816,8 @@ function renderHuman2(overview) {
14798
14816
  printScore("Competitor press.", scores.competitorPressure);
14799
14817
  printScore("Run status ", scores.runStatus);
14800
14818
  console.log(`
14801
- Queries cited: ${queryCounts.citedQueries}/${queryCounts.totalQueries} (${pct2(queryCounts.citedRate)})`);
14802
- console.log(` Queries mentioned: ${queryCounts.mentionedQueries}/${queryCounts.totalQueries} (${pct2(queryCounts.mentionRate)})`);
14819
+ Queries cited: ${queryCounts.citedQueries}/${queryCounts.totalQueries} (${formatPercent(queryCounts.citedRate)})`);
14820
+ console.log(` Queries mentioned: ${queryCounts.mentionedQueries}/${queryCounts.totalQueries} (${formatPercent(queryCounts.mentionRate)})`);
14803
14821
  if (movementComparison.hasPreviousRun) {
14804
14822
  const comparisonLabel = movementComparison.querySetChanged ? `changed (+${movementComparison.addedQueryCount} added, -${movementComparison.removedQueryCount} removed); movement compares ${movementComparison.comparableQueryCount} shared` : `unchanged; ${movementComparison.comparableQueryCount} comparable`;
14805
14823
  console.log(` Query basket: ${comparisonLabel}`);
@@ -14811,14 +14829,14 @@ function renderHuman2(overview) {
14811
14829
  if (providers.length > 0) {
14812
14830
  console.log("\n Providers:");
14813
14831
  for (const p of providers) {
14814
- console.log(` ${p.provider.padEnd(12)} ${p.cited}/${p.total} (${pct2(p.citedRate)})`);
14832
+ console.log(` ${p.provider.padEnd(12)} ${p.cited}/${p.total} (${formatPercent(p.citedRate)})`);
14815
14833
  }
14816
14834
  }
14817
14835
  if (providerScores.length > 0) {
14818
14836
  console.log("\n Models:");
14819
14837
  for (const m of providerScores) {
14820
14838
  const label = `${m.provider}/${m.model ?? "unknown"}`.padEnd(28);
14821
- console.log(` ${label} ${m.cited}/${m.total} (${m.score}%)`);
14839
+ console.log(` ${label} ${m.cited}/${m.total} (${formatPercent(m.score, "percent")})`);
14822
14840
  }
14823
14841
  }
14824
14842
  if (transitions.since) {
@@ -14840,7 +14858,7 @@ function renderHuman2(overview) {
14840
14858
  }
14841
14859
  if (health) {
14842
14860
  console.log(`
14843
- Health: ${pct2(health.overallCitedRate)} cited (${health.citedPairs}/${health.totalPairs} pairs)`);
14861
+ Health: ${formatPercent(health.overallCitedRate)} cited (${health.citedPairs}/${health.totalPairs} pairs)`);
14844
14862
  }
14845
14863
  if (topInsights.length > 0) {
14846
14864
  console.log("\n Top insights:");
@@ -14853,7 +14871,7 @@ function renderHuman2(overview) {
14853
14871
  Run history (last ${runHistory.length}):`);
14854
14872
  for (const point of runHistory) {
14855
14873
  const bar = "\u2588".repeat(Math.round(point.citationRate / 10));
14856
- console.log(` ${point.createdAt.slice(0, 10)} ${String(point.citationRate).padStart(3)}% ${bar}`);
14874
+ console.log(` ${point.createdAt.slice(0, 10)} ${formatPercent(point.citationRate, "percent").padStart(6)} ${bar}`);
14857
14875
  }
14858
14876
  }
14859
14877
  if (suggestedQueries.rows.length > 0) {
@@ -14873,23 +14891,19 @@ function printScore(prefix, score) {
14873
14891
  console.log(` ${prefix} ${tone} ${value} ${score.delta}`);
14874
14892
  }
14875
14893
  function printMentionShareBreakdown(mentionShare) {
14876
- const { breakdown } = mentionShare;
14877
- if (breakdown.perCompetitor.length === 0) return;
14878
- const total = breakdown.projectMentionSnapshots + breakdown.competitorMentionSnapshots;
14879
- if (total === 0) return;
14880
- const youPct = (breakdown.projectMentionSnapshots / total * 100).toFixed(1);
14881
- console.log(` you${" ".repeat(28)} ${breakdown.projectMentionSnapshots} mentions (${youPct}% of combined)`);
14882
- for (const row of breakdown.perCompetitor.slice(0, 3)) {
14883
- const pct4 = (row.mentionSnapshots / total * 100).toFixed(1);
14884
- console.log(` ${row.domain.padEnd(30)} ${row.mentionSnapshots} mentions (${pct4}% of combined)`);
14894
+ const legacyCompatible = mentionShare.breakdown;
14895
+ const ranking = legacyCompatible.ranking ?? [];
14896
+ const project = ranking.find((row) => row.kind === "project");
14897
+ if (!project) return;
14898
+ const competitors = ranking.filter((row) => row.kind === "competitor");
14899
+ console.log(` you${" ".repeat(28)} ${project.mentionSnapshots} mentions (${formatPercent(project.share)} of combined)`);
14900
+ for (const row of competitors.slice(0, 3)) {
14901
+ console.log(` ${(row.domain ?? "").padEnd(30)} ${row.mentionSnapshots} mentions (${formatPercent(row.share)} of combined)`);
14885
14902
  }
14886
- if (breakdown.perCompetitor.length > 3) {
14887
- console.log(` + ${breakdown.perCompetitor.length - 3} more competitor${breakdown.perCompetitor.length - 3 === 1 ? "" : "s"} (--format json for full breakdown)`);
14903
+ if (competitors.length > 3) {
14904
+ console.log(` + ${competitors.length - 3} more competitor${competitors.length - 3 === 1 ? "" : "s"} (--format json for full breakdown)`);
14888
14905
  }
14889
14906
  }
14890
- function pct2(value) {
14891
- return `${(value * 100).toFixed(1)}%`;
14892
- }
14893
14907
 
14894
14908
  // src/commands/search.ts
14895
14909
  async function searchProject(project, opts) {
@@ -15220,9 +15234,9 @@ async function listContentGaps(project, opts) {
15220
15234
  console.log(`${response.gaps.length} gap${response.gaps.length === 1 ? "" : "s"} found`);
15221
15235
  console.log("");
15222
15236
  for (const gap of response.gaps) {
15223
- const missPct = formatWholePercent(gap.missRate);
15224
- console.log(`${missPct.padStart(4)} ${gap.competitorCount} competitor(s) ${gap.query}`);
15225
- console.log(` competitors: ${gap.competitorDomains.join(", ")}`);
15237
+ const missPct = formatPercent(gap.missRate);
15238
+ console.log(`${missPct.padStart(6)} ${gap.competitorCount} competitor(s) ${gap.query}`);
15239
+ console.log(` competitors: ${gap.competitorDomains.join(", ")}`);
15226
15240
  console.log("");
15227
15241
  }
15228
15242
  }
@@ -16826,10 +16840,10 @@ async function technicalAeoScore(project, opts) {
16826
16840
  lines.push(`As of ${score.auditedAt}`);
16827
16841
  if (score.factors.length > 0) {
16828
16842
  lines.push("");
16829
- lines.push(`${"Factor".padEnd(32)}${"Wt".padStart(4)}${"Avg".padStart(6)}${"Status".padStart(9)} Pass/Part/Fail`);
16843
+ lines.push(`${"Factor".padEnd(32)}${"Share".padStart(7)}${"Avg".padStart(6)}${"Status".padStart(9)} Pass/Part/Fail`);
16830
16844
  for (const f of score.factors) {
16831
16845
  lines.push(
16832
- `${f.name.slice(0, 31).padEnd(32)}${String(f.weight).padStart(4)}${String(f.avgScore).padStart(6)}${f.status.padStart(9)} ${f.pagesPassing}/${f.pagesPartial}/${f.pagesFailing}`
16846
+ `${f.name.slice(0, 31).padEnd(32)}${formatPercent(f.sharePct, RatioUnits.percent).padStart(7)}${String(f.avgScore).padStart(6)}${f.status.padStart(9)} ${f.pagesPassing}/${f.pagesPartial}/${f.pagesFailing}`
16833
16847
  );
16834
16848
  }
16835
16849
  }
@@ -17117,7 +17131,8 @@ async function technicalAeoPageAudit(project, opts) {
17117
17131
  if (res.factors.length > 0) {
17118
17132
  lines.push("", "Factors:");
17119
17133
  for (const factor of res.factors) {
17120
- lines.push(` ${factor.name}: ${factor.score}/100 (${factor.status})`);
17134
+ const share = typeof factor.sharePct === "number" ? ` \xB7 worth ${formatPercent(factor.sharePct, RatioUnits.percent)} of the page score` : "";
17135
+ lines.push(` ${factor.name}: ${factor.score}/100 (${factor.status})${share}`);
17121
17136
  for (const finding of factor.findings) lines.push(` [${finding.code}] ${finding.message}`);
17122
17137
  for (const recommendation of factor.recommendations) lines.push(` Fix: ${recommendation}`);
17123
17138
  }
@@ -17810,9 +17825,6 @@ async function showVisibilityStats(project, opts) {
17810
17825
  }
17811
17826
  printVisibilityStats(data);
17812
17827
  }
17813
- function pct3(rate) {
17814
- return rate === null ? "\u2014" : formatRatio(rate);
17815
- }
17816
17828
  async function showVisibilityCompare(project, opts) {
17817
17829
  if (!opts.from || !opts.to) throw new Error("visibility-compare requires --from <YYYY-MM> and --to <YYYY-MM>");
17818
17830
  const client = createApiClient();
@@ -17829,8 +17841,7 @@ function periodCell(p) {
17829
17841
  return `unavailable: no competitive frame (${p.numerator} observed)`;
17830
17842
  }
17831
17843
  if (p.point === null || p.ciLow === null || p.ciHigh === null) return "no data";
17832
- const p1 = (v) => formatRatio(v);
17833
- return `${p1(p.point)} [${p1(p.ciLow)}, ${p1(p.ciHigh)}]`;
17844
+ return `${formatPercent(p.point)} [${formatPercent(p.ciLow)}, ${formatPercent(p.ciHigh)}]`;
17834
17845
  }
17835
17846
  function verdictCell(m) {
17836
17847
  switch (m.verdict) {
@@ -17940,9 +17951,9 @@ function printVisibilityStats(data) {
17940
17951
  const rows = data.queries.map((q) => ({
17941
17952
  label: q.query,
17942
17953
  cited: citedCell(q),
17943
- citedPct: pct3(q.citedRate),
17954
+ citedPct: formatPercent(q.citedRate),
17944
17955
  ment: mentionedCell(q),
17945
- mentPct: pct3(q.mentionRate)
17956
+ mentPct: formatPercent(q.mentionRate)
17946
17957
  }));
17947
17958
  const queryWidth = Math.max(7, ...rows.map((r) => r.label.length));
17948
17959
  const citedWidth = Math.max(7, ...rows.map((r) => r.cited.length));
@@ -17972,9 +17983,9 @@ function printVisibilityStats(data) {
17972
17983
  [
17973
17984
  "TOTAL".padEnd(queryWidth),
17974
17985
  citedCell(data.totals).padEnd(citedWidth),
17975
- pct3(data.totals.citedRate).padStart(7),
17986
+ formatPercent(data.totals.citedRate).padStart(7),
17976
17987
  mentionedCell(data.totals).padEnd(mentWidth),
17977
- pct3(data.totals.mentionRate).padStart(7)
17988
+ formatPercent(data.totals.mentionRate).padStart(7)
17978
17989
  ].join(" ")
17979
17990
  );
17980
17991
  if (data.groupBy === "provider" && data.byProvider && data.byProvider.length > 0) {
@@ -17986,9 +17997,9 @@ function printVisibilityStats(data) {
17986
17997
  [
17987
17998
  ` ${p.provider}`.padEnd(provWidth + 2),
17988
17999
  citedCell(p).padEnd(citedWidth),
17989
- pct3(p.citedRate).padStart(7),
18000
+ formatPercent(p.citedRate).padStart(7),
17990
18001
  mentionedCell(p).padEnd(mentWidth),
17991
- pct3(p.mentionRate).padStart(7)
18002
+ formatPercent(p.mentionRate).padStart(7)
17992
18003
  ].join(" ")
17993
18004
  );
17994
18005
  }
@@ -17996,7 +18007,7 @@ function printVisibilityStats(data) {
17996
18007
  const sov = data.shareOfVoice;
17997
18008
  if (sov) {
17998
18009
  console.log("");
17999
- const pctStr = sov.availability !== void 0 ? shareOfVoiceLabel(sov.percent, sov) : sov.percent !== null ? `${sov.percent}%` : sov.competitorCount === 0 ? "\u2014 (no competitors configured)" : sov.snapshotsWithAnswerText === 0 ? "\u2014 (no answer text in scope)" : "\u2014 (no brands mentioned in scope)";
18010
+ const pctStr = sov.availability !== void 0 ? shareOfVoiceLabel(sov.percent, sov) : sov.percent !== null ? formatPercent(sov.percent, "percent") : sov.competitorCount === 0 ? "\u2014 (no competitors configured)" : sov.snapshotsWithAnswerText === 0 ? "\u2014 (no answer text in scope)" : "\u2014 (no brands mentioned in scope)";
18000
18011
  const scope = sov.queryClass === "pooled" ? "pooled queries \xB7 classification unavailable" : `${sov.queryClass} queries`;
18001
18012
  console.log(
18002
18013
  `Share of voice (${scope}${sov.measurementScope === "all-markets" ? " \xB7 all markets" : ""}): ${pctStr} (you ${sov.projectMentions} vs competitors ${sov.competitorMentions} brand mentions across ${sov.snapshotsWithAnswerText} answers)`