@ainyc/canonry 4.8.0 → 4.11.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.
@@ -4,7 +4,7 @@ import {
4
4
  configExists,
5
5
  loadConfig,
6
6
  saveConfigPatch
7
- } from "./chunk-5KIFQH52.js";
7
+ } from "./chunk-ZR4AVT4T.js";
8
8
  import {
9
9
  DEFAULT_RUN_HISTORY_LIMIT,
10
10
  IntelligenceService,
@@ -61,7 +61,7 @@ import {
61
61
  runs,
62
62
  schedules,
63
63
  usageCounters
64
- } from "./chunk-QEPFB7UW.js";
64
+ } from "./chunk-5G6WYP2S.js";
65
65
  import {
66
66
  AGENT_MEMORY_VALUE_MAX_BYTES,
67
67
  AGENT_PROVIDER_IDS,
@@ -91,6 +91,8 @@ import {
91
91
  citationStateToCited,
92
92
  competitorBatchRequestSchema,
93
93
  contentActionLabel,
94
+ dedupeReportActions,
95
+ dedupeReportOpportunities,
94
96
  deliveryFailed,
95
97
  determineAnswerMentioned,
96
98
  effectiveDomains,
@@ -135,7 +137,7 @@ import {
135
137
  visibilityStateFromAnswerMentioned,
136
138
  windowCutoff,
137
139
  wordpressEnvSchema
138
- } from "./chunk-IJEP6LB4.js";
140
+ } from "./chunk-WWU65YPN.js";
139
141
 
140
142
  // src/telemetry.ts
141
143
  import crypto from "crypto";
@@ -3214,71 +3216,6 @@ function renderHeaderLocationFragment(location) {
3214
3216
  if (!location) return " \xB7 No market set";
3215
3217
  return ` \xB7 Market: ${escapeHtml(locationDisplay(location))}`;
3216
3218
  }
3217
- var REPORT_INTENT_STOPWORDS = /* @__PURE__ */ new Set([
3218
- "a",
3219
- "an",
3220
- "and",
3221
- "for",
3222
- "from",
3223
- "in",
3224
- "near",
3225
- "of",
3226
- "on",
3227
- "or",
3228
- "the",
3229
- "to"
3230
- ]);
3231
- function reportIntentModifiers(report) {
3232
- const location = report.meta.location;
3233
- if (!location) return /* @__PURE__ */ new Set();
3234
- return new Set(
3235
- [location.label, location.city, location.region, location.country].flatMap(tokenizeReportIntent).map(normalizeReportIntentToken).filter(Boolean)
3236
- );
3237
- }
3238
- function dedupeReportActions(report, actions) {
3239
- const modifiers = reportIntentModifiers(report);
3240
- if (actions.length <= 1 || modifiers.size === 0) return [...actions];
3241
- const seen = /* @__PURE__ */ new Set();
3242
- const result = [];
3243
- for (const action of actions) {
3244
- if (action.category !== "content") {
3245
- result.push(action);
3246
- continue;
3247
- }
3248
- const key = reportIntentKey(extractActionQuery(action), modifiers);
3249
- if (!key || seen.has(key)) continue;
3250
- seen.add(key);
3251
- result.push(action);
3252
- }
3253
- return result;
3254
- }
3255
- function dedupeReportOpportunities(report) {
3256
- const modifiers = reportIntentModifiers(report);
3257
- const opportunities = report.contentOpportunities;
3258
- if (opportunities.length <= 1 || modifiers.size === 0) return opportunities;
3259
- const seen = /* @__PURE__ */ new Set();
3260
- return opportunities.filter((opportunity) => {
3261
- const key = reportIntentKey(opportunity.query, modifiers);
3262
- if (!key || seen.has(key)) return false;
3263
- seen.add(key);
3264
- return true;
3265
- });
3266
- }
3267
- function extractActionQuery(action) {
3268
- return action.title.match(/"([^"]+)"/)?.[1] ?? action.successMetric.match(/"([^"]+)"/)?.[1] ?? action.title;
3269
- }
3270
- function reportIntentKey(value, modifiers) {
3271
- const tokens = tokenizeReportIntent(value).map(normalizeReportIntentToken).filter(Boolean).filter((token) => !REPORT_INTENT_STOPWORDS.has(token)).filter((token) => !modifiers.has(token));
3272
- return [...new Set(tokens)].sort().join(" ");
3273
- }
3274
- function tokenizeReportIntent(value) {
3275
- return value.toLowerCase().match(/[a-z0-9]+/g) ?? [];
3276
- }
3277
- function normalizeReportIntentToken(token) {
3278
- if (token.length > 4 && token.endsWith("ies")) return `${token.slice(0, -3)}y`;
3279
- if (token.length > 4 && token.endsWith("s") && !token.endsWith("ss")) return token.slice(0, -1);
3280
- return token;
3281
- }
3282
3219
  function renderLocationCard(report) {
3283
3220
  const location = report.meta.location;
3284
3221
  const handling = report.meta.providerLocationHandling;
@@ -3287,7 +3224,7 @@ function renderLocationCard(report) {
3287
3224
  const weakLocationProviders = handling.filter((h) => h.treatment === "ignored" || h.treatment === "browser-geo").map((h) => h.provider);
3288
3225
  const marketValue = location ? locationDisplay(location) : "No market set";
3289
3226
  const notIncluded = otherLocations.length > 0 ? compactInlineList(otherLocations, 4) : "None";
3290
- const interpretation = location ? otherLocations.length > 0 ? `${otherLocations.length} configured ${pluralize(otherLocations.length, "market")} still ${otherLocations.length === 1 ? "needs" : "need"} a matching sweep before cross-market recommendations.` : "Single-market report; findings can be read as the current market view." : "No geographic hint was attached to this sweep; read findings as default-market or national results.";
3227
+ const interpretation = location ? otherLocations.length > 0 ? `${otherLocations.length} configured ${pluralize(otherLocations.length, "market")} still ${otherLocations.length === 1 ? "needs" : "need"} a matching check before cross-market recommendations.` : "Single-market report; findings can be read as the current market view." : "No geographic hint was attached to this check; read findings as default-market or national results.";
3291
3228
  const providerCopy = handling.length > 0 ? weakLocationProviders.length > 0 ? `${weakLocationProviders.length} ${pluralize(weakLocationProviders.length, "provider")} need a closer location check.` : `${handling.length} ${pluralize(handling.length, "provider")} received the market context.` : "No provider-level location metadata is available for this report.";
3292
3229
  const warning = weakLocationProviders.length > 0 ? `<div class="scope-warning">
3293
3230
  <strong>Location handling needs review</strong>
@@ -3297,7 +3234,7 @@ function renderLocationCard(report) {
3297
3234
  <h3>Market Scope</h3>
3298
3235
  <div class="market-scope-grid">
3299
3236
  <div class="scope-tile">
3300
- <div class="scope-label">Current sweep</div>
3237
+ <div class="scope-label">Current check</div>
3301
3238
  <div class="scope-value">${escapeHtml(marketValue)}</div>
3302
3239
  <div class="scope-copy">All findings below are scoped to this run.</div>
3303
3240
  </div>
@@ -3323,13 +3260,13 @@ function renderExecutiveSummary(report) {
3323
3260
  const citedFragment = s.totalQueryCount > 0 ? `${s.citedQueryCount}/${s.totalQueryCount} ${queryNoun} cited` : "no queries";
3324
3261
  const mentionedFragment = s.totalQueryCount > 0 ? `${s.mentionedQueryCount}/${s.totalQueryCount} ${queryNoun} mentioned` : "no queries";
3325
3262
  const headlineTitle = s.totalQueryCount > 0 ? `${s.citedQueryCount} of ${s.totalQueryCount} tracked ${queryNoun} cite ${report.meta.project.displayName}` : "No AI citation data yet";
3326
- const headlineSubtitle = s.totalQueryCount > 0 ? `${s.citationRate}% citation coverage and ${s.mentionRate}% mention coverage across ${s.providerCount} ${pluralize(s.providerCount, "provider")}.` : "Run a visibility sweep to populate the first citation and mention baseline.";
3263
+ const headlineSubtitle = s.totalQueryCount > 0 ? `${s.citationRate}% citation coverage and ${s.mentionRate}% mention coverage across ${s.providerCount} ${pluralize(s.providerCount, "provider")}.` : "Run a check to populate the first citation and mention baseline.";
3327
3264
  const priorityActions = report.agencyDiagnostics.priorities.length > 0 ? report.agencyDiagnostics.priorities : report.actionPlan;
3328
3265
  const actionCount = dedupeReportActions(report, priorityActions).length;
3329
3266
  const heroHtml = `<div class="executive-hero">
3330
3267
  <div class="headline-card">
3331
3268
  <div>
3332
- <div class="hero-kicker">Latest AI visibility sweep</div>
3269
+ <div class="hero-kicker">Latest AI visibility check</div>
3333
3270
  <div class="hero-title">${escapeHtml(headlineTitle)}</div>
3334
3271
  </div>
3335
3272
  <div class="hero-subtitle">${escapeHtml(headlineSubtitle)}</div>
@@ -3407,6 +3344,107 @@ function renderExecutiveSummary(report) {
3407
3344
  heroHtml + metricsHtml + findingsHtml + locationHtml
3408
3345
  );
3409
3346
  }
3347
+ function deltaToneClass(direction) {
3348
+ if (direction === "up") return "tone-positive";
3349
+ if (direction === "down") return "tone-negative";
3350
+ return "";
3351
+ }
3352
+ function deltaArrow(direction) {
3353
+ if (direction === "up") return "\u2191";
3354
+ if (direction === "down") return "\u2193";
3355
+ return "\u2192";
3356
+ }
3357
+ function renderRateDeltaTile(label, delta, unit) {
3358
+ if (!delta) {
3359
+ return `<div class="metric"><div class="label">${escapeHtml(label)}</div><div class="value">\u2014</div><div class="delta">No prior data</div></div>`;
3360
+ }
3361
+ const valueSuffix = unit === "%" ? "%" : "";
3362
+ const deltaSign = delta.deltaAbs > 0 ? "+" : "";
3363
+ const deltaText = `${deltaSign}${delta.deltaAbs.toFixed(unit === "%" ? 1 : 0)}${valueSuffix} vs ${delta.prior}${valueSuffix}`;
3364
+ return `<div class="metric">
3365
+ <div class="label">${escapeHtml(label)}</div>
3366
+ <div class="value ${deltaToneClass(delta.direction)}">${delta.current}${valueSuffix} <span style="font-size:14px;font-weight:500;">${deltaArrow(delta.direction)}</span></div>
3367
+ <div class="delta">${deltaText}</div>
3368
+ </div>`;
3369
+ }
3370
+ function renderTrafficDeltaTile(label, delta, countLabel) {
3371
+ if (!delta) {
3372
+ return `<div class="metric"><div class="label">${escapeHtml(label)}</div><div class="value">\u2014</div><div class="delta">Not enough trend data</div></div>`;
3373
+ }
3374
+ const deltaSign = delta.deltaAbs > 0 ? "+" : "";
3375
+ const deltaText = `${deltaSign}${formatNumber(delta.deltaAbs)} ${countLabel} vs prior ${WHATS_CHANGED_PERIOD_DAYS} days`;
3376
+ return `<div class="metric">
3377
+ <div class="label">${escapeHtml(label)}</div>
3378
+ <div class="value ${deltaToneClass(delta.direction)}">${formatNumber(delta.current)} <span style="font-size:14px;font-weight:500;">${deltaArrow(delta.direction)}</span></div>
3379
+ <div class="delta">${deltaText}</div>
3380
+ </div>`;
3381
+ }
3382
+ var WHATS_CHANGED_PERIOD_DAYS = 14;
3383
+ function renderProviderMovements(movements) {
3384
+ const meaningful = movements.filter((m) => m.direction !== "flat");
3385
+ if (meaningful.length === 0) return "";
3386
+ const rows = meaningful.map((m) => {
3387
+ const sign = m.deltaAbs > 0 ? "+" : "";
3388
+ return `<tr>
3389
+ <td>${escapeHtml(m.provider)}</td>
3390
+ <td class="numeric">${m.prior}%</td>
3391
+ <td class="numeric">${m.current}%</td>
3392
+ <td class="numeric ${deltaToneClass(m.direction)}">${sign}${m.deltaAbs.toFixed(1)}% ${deltaArrow(m.direction)}</td>
3393
+ </tr>`;
3394
+ }).join("");
3395
+ return `<div class="chart-card"><h3>AI engine movements</h3>
3396
+ <table class="report-table">
3397
+ <thead><tr><th>Engine</th><th class="numeric">Prior</th><th class="numeric">Current</th><th class="numeric">Change</th></tr></thead>
3398
+ <tbody>${rows}</tbody>
3399
+ </table>
3400
+ </div>`;
3401
+ }
3402
+ function renderWinsLosses(insights2, heading, emptyMessage) {
3403
+ if (insights2.length === 0) {
3404
+ return `<div class="chart-card"><h3>${escapeHtml(heading)}</h3>
3405
+ <p class="section-intro">${escapeHtml(emptyMessage)}</p>
3406
+ </div>`;
3407
+ }
3408
+ const rows = insights2.map((i) => {
3409
+ const tone = severityTone(i.severity);
3410
+ const countChip = i.instanceCount > 1 ? ` <span class="badge tone-neutral">\xD7 ${i.instanceCount}</span>` : "";
3411
+ return `<tr>
3412
+ <td><span class="badge tone-${tone}">${escapeHtml(reportSeverityLabel(i.severity))}</span></td>
3413
+ <td>${escapeHtml(i.title)}${countChip}</td>
3414
+ <td>${escapeHtml(i.query)}</td>
3415
+ <td>${escapeHtml(i.provider)}</td>
3416
+ </tr>`;
3417
+ }).join("");
3418
+ return `<div class="chart-card"><h3>${escapeHtml(heading)}</h3>
3419
+ <table class="report-table">
3420
+ <thead><tr><th>Severity</th><th>Title</th><th>Query</th><th>Provider</th></tr></thead>
3421
+ <tbody>${rows}</tbody>
3422
+ </table>
3423
+ </div>`;
3424
+ }
3425
+ function renderWhatsChanged(report) {
3426
+ const w = report.whatsChanged;
3427
+ if (!w.enoughHistory && !w.gscClicksDelta && !w.aiReferralsDelta && w.wins.length === 0 && w.regressions.length === 0) {
3428
+ return section(
3429
+ { id: "whats-changed", eyebrow: "Section 2", title: "What's Changed", intro: w.headline },
3430
+ renderEmpty("Trends will appear after a few more checks.")
3431
+ );
3432
+ }
3433
+ const rateTiles = `<div class="metric-grid">
3434
+ ${renderRateDeltaTile("Citation rate", w.citationRate, "%")}
3435
+ ${renderRateDeltaTile("Mention rate", w.mentionRate, "%")}
3436
+ ${renderRateDeltaTile("Cited queries", w.citedQueryCount, "count")}
3437
+ ${renderTrafficDeltaTile("GSC clicks", w.gscClicksDelta, "clicks")}
3438
+ ${renderTrafficDeltaTile("AI referral sessions", w.aiReferralsDelta, "sessions")}
3439
+ </div>`;
3440
+ const movements = renderProviderMovements(w.providerMovements);
3441
+ const wins = renderWinsLosses(w.wins, "Wins", "No new gains in the latest check.");
3442
+ const regressions = renderWinsLosses(w.regressions, "Regressions", "No new regressions in the latest check.");
3443
+ return section(
3444
+ { id: "whats-changed", eyebrow: "Section 2", title: "What's Changed", intro: w.headline },
3445
+ `${rateTiles}${movements}${wins}${regressions}`
3446
+ );
3447
+ }
3410
3448
  function renderProviderBars(rates) {
3411
3449
  if (rates.length === 0) return "";
3412
3450
  const max = Math.max(...rates.map((r) => r.citationRate), 100);
@@ -3435,7 +3473,7 @@ function renderProviderBars(rates) {
3435
3473
  }
3436
3474
  function renderCitationMatrix(scorecard) {
3437
3475
  if (scorecard.queries.length === 0 || scorecard.providers.length === 0) {
3438
- return renderEmpty("Run a visibility sweep to populate the citation matrix.");
3476
+ return renderEmpty("Run a check to populate the citation matrix.");
3439
3477
  }
3440
3478
  const headers = scorecard.providers.map((p) => `<th>${escapeHtml(p)}</th>`).join("");
3441
3479
  const rows = scorecard.queries.map((q, qi) => {
@@ -3462,7 +3500,7 @@ function renderCitationScorecard(report) {
3462
3500
  ${renderCitationMatrix(report.citationScorecard)}
3463
3501
  `;
3464
3502
  return section(
3465
- { id: "citation-scorecard", eyebrow: "Section 2", title: "Citation Scorecard", intro: "Provider-by-provider citation and mention coverage for the latest sweep." },
3503
+ { id: "citation-scorecard", eyebrow: "Section 3", title: "Citation Scorecard", intro: "Per-engine citation and mention coverage from the latest check." },
3466
3504
  body
3467
3505
  );
3468
3506
  }
@@ -3510,8 +3548,8 @@ function renderCompetitorLandscape(report) {
3510
3548
  const noMentionData = mentionLandscape.competitors.length === 0 && mentionLandscape.projectMentionCount === 0;
3511
3549
  if (noCitationData && noMentionData) {
3512
3550
  return section(
3513
- { id: "competitor-landscape", eyebrow: "Section 3", title: "Competitor Landscape" },
3514
- renderEmpty("No competitor data yet. Add competitors and run a visibility sweep.")
3551
+ { id: "competitor-landscape", eyebrow: "Section 4", title: "Competitor Landscape" },
3552
+ renderEmpty("No competitor data yet. Add competitors and run a check.")
3515
3553
  );
3516
3554
  }
3517
3555
  const mentionByDomain = new Map(mentionLandscape.competitors.map((m) => [m.domain, m]));
@@ -3542,7 +3580,7 @@ function renderCompetitorLandscape(report) {
3542
3580
  return section(
3543
3581
  {
3544
3582
  id: "competitor-landscape",
3545
- eyebrow: "Section 3",
3583
+ eyebrow: "Section 4",
3546
3584
  title: "Competitor Landscape",
3547
3585
  intro: "Who AI engines cite and mention instead of the client."
3548
3586
  },
@@ -3609,8 +3647,8 @@ function renderAiSourceOrigin(report) {
3609
3647
  const origin = report.aiSourceOrigin;
3610
3648
  if (origin.categories.length === 0 && origin.topDomains.length === 0) {
3611
3649
  return section(
3612
- { id: "ai-source-origin", eyebrow: "Section 4", title: "AI Citation Sources" },
3613
- renderEmpty("No source data yet. Run a visibility sweep first.")
3650
+ { id: "ai-source-origin", eyebrow: "Section 5", title: "AI Citation Sources" },
3651
+ renderEmpty("No source data yet. Run a check first.")
3614
3652
  );
3615
3653
  }
3616
3654
  const competitorBucket = origin.categories.find((c) => c.category === "competitor");
@@ -3630,9 +3668,9 @@ function renderAiSourceOrigin(report) {
3630
3668
  return section(
3631
3669
  {
3632
3670
  id: "ai-source-origin",
3633
- eyebrow: "Section 4",
3671
+ eyebrow: "Section 5",
3634
3672
  title: "AI Citation Sources",
3635
- intro: "External domains AI engines trusted most in the latest sweep."
3673
+ intro: "External domains AI engines cited most in the latest check."
3636
3674
  },
3637
3675
  `${headlineFragment}${table}${renderCategoryBars(origin.categories)}`
3638
3676
  );
@@ -3673,7 +3711,7 @@ function renderGsc(report) {
3673
3711
  const gsc = report.gsc;
3674
3712
  if (!gsc) {
3675
3713
  return section(
3676
- { id: "gsc", eyebrow: "Section 5", title: "GSC Performance" },
3714
+ { id: "gsc", eyebrow: "Section 6", title: "GSC Performance" },
3677
3715
  renderEmpty("Connect Google Search Console to populate this section.")
3678
3716
  );
3679
3717
  }
@@ -3716,7 +3754,7 @@ function renderGsc(report) {
3716
3754
  }
3717
3755
  const dateRange = gscDateRange(report);
3718
3756
  return section(
3719
- { id: "gsc", eyebrow: "Section 5", title: "GSC Performance", intro: `Search demand signals to compare against AI visibility${dateRange ? ` for ${dateRange}` : ""}.` },
3757
+ { id: "gsc", eyebrow: "Section 6", title: "GSC Performance", intro: `Search demand signals to compare against AI visibility${dateRange ? ` for ${dateRange}` : ""}.` },
3720
3758
  `<div class="metric-grid">
3721
3759
  <div class="metric"><div class="label">Total clicks</div><div class="value">${formatNumber(gsc.totalClicks)}</div></div>
3722
3760
  <div class="metric"><div class="label">Total impressions</div><div class="value">${formatNumber(gsc.totalImpressions)}</div></div>
@@ -3738,7 +3776,7 @@ function renderGa(report) {
3738
3776
  const ga = report.ga;
3739
3777
  if (!ga) {
3740
3778
  return section(
3741
- { id: "ga", eyebrow: "Section 6", title: "GA4 Traffic" },
3779
+ { id: "ga", eyebrow: "Section 7", title: "GA4 Traffic" },
3742
3780
  renderEmpty("Connect Google Analytics 4 to populate this section.")
3743
3781
  );
3744
3782
  }
@@ -3760,7 +3798,7 @@ function renderGa(report) {
3760
3798
  "sessions"
3761
3799
  );
3762
3800
  return section(
3763
- { id: "ga", eyebrow: "Section 6", title: "GA4 Traffic", intro: `Site traffic from ${formatDate(ga.periodStart)} to ${formatDate(ga.periodEnd)}.` },
3801
+ { id: "ga", eyebrow: "Section 7", title: "GA4 Traffic", intro: `Site traffic from ${formatDate(ga.periodStart)} to ${formatDate(ga.periodEnd)}.` },
3764
3802
  `<div class="metric-grid">
3765
3803
  <div class="metric"><div class="label">Total sessions</div><div class="value">${formatNumber(ga.totalSessions)}</div></div>
3766
3804
  <div class="metric"><div class="label">Total users</div><div class="value">${formatNumber(ga.totalUsers)}</div></div>
@@ -3779,7 +3817,7 @@ function renderSocial(report) {
3779
3817
  const social = report.socialReferrals;
3780
3818
  if (!social) {
3781
3819
  return section(
3782
- { id: "social-referrals", eyebrow: "Section 7", title: "Social Referrals" },
3820
+ { id: "social-referrals", eyebrow: "Section 8", title: "Social Referrals" },
3783
3821
  renderEmpty("No social referral data yet.")
3784
3822
  );
3785
3823
  }
@@ -3800,7 +3838,7 @@ function renderSocial(report) {
3800
3838
  <td class="numeric">${formatNumber(c.sessions)}</td>
3801
3839
  </tr>`).join("");
3802
3840
  return section(
3803
- { id: "social-referrals", eyebrow: "Section 7", title: "Social Referrals", intro: "Social traffic split by channel and campaign." },
3841
+ { id: "social-referrals", eyebrow: "Section 8", title: "Social Referrals", intro: "Social traffic split by channel and campaign." },
3804
3842
  `<div class="metric-grid">
3805
3843
  <div class="metric"><div class="label">Total sessions</div><div class="value">${formatNumber(social.totalSessions)}</div></div>
3806
3844
  <div class="metric"><div class="label">Organic social</div><div class="value">${formatNumber(social.organicSessions)}</div></div>
@@ -3819,7 +3857,7 @@ function renderAiReferrals(report) {
3819
3857
  const ai = report.aiReferrals;
3820
3858
  if (!ai) {
3821
3859
  return section(
3822
- { id: "ai-referrals", eyebrow: "Section 8", title: "AI Referral Traffic" },
3860
+ { id: "ai-referrals", eyebrow: "Section 9", title: "AI Referral Traffic" },
3823
3861
  renderEmpty("No AI referral traffic detected yet.")
3824
3862
  );
3825
3863
  }
@@ -3845,7 +3883,7 @@ function renderAiReferrals(report) {
3845
3883
  "AI referral sessions over time"
3846
3884
  );
3847
3885
  return section(
3848
- { id: "ai-referrals", eyebrow: "Section 8", title: "AI Referral Traffic", intro: "Traffic arriving from AI answer engines." },
3886
+ { id: "ai-referrals", eyebrow: "Section 9", title: "AI Referral Traffic", intro: "Traffic arriving from AI answer engines." },
3849
3887
  `<div class="metric-grid">
3850
3888
  <div class="metric"><div class="label">Total sessions</div><div class="value">${formatNumber(ai.totalSessions)}</div></div>
3851
3889
  <div class="metric"><div class="label">Total users</div><div class="value">${formatNumber(ai.totalUsers)}</div></div>
@@ -3864,7 +3902,7 @@ function renderIndexingHealth(report) {
3864
3902
  const ih = report.indexingHealth;
3865
3903
  if (!ih) {
3866
3904
  return section(
3867
- { id: "indexing-health", eyebrow: "Section 9", title: "Indexing Health" },
3905
+ { id: "indexing-health", eyebrow: "Section 10", title: "Indexing Health" },
3868
3906
  renderEmpty("Connect Google Search Console or Bing Webmaster Tools and run a sitemap inspection.")
3869
3907
  );
3870
3908
  }
@@ -3886,7 +3924,7 @@ function renderIndexingHealth(report) {
3886
3924
  }).join("");
3887
3925
  const legend = segments.map((s) => `<span><span class="legend-swatch" style="background:${s.color}"></span>${escapeHtml(s.label)}: ${s.count}</span>`).join("");
3888
3926
  return section(
3889
- { id: "indexing-health", eyebrow: "Section 9", title: "Indexing Health", intro: `Pages absent from ${ih.provider === "google" ? "Google" : "Bing"} are harder for AI engines to retrieve.` },
3927
+ { id: "indexing-health", eyebrow: "Section 10", title: "Indexing Health", intro: `Pages absent from ${ih.provider === "google" ? "Google" : "Bing"} are harder for AI engines to retrieve.` },
3890
3928
  `<div class="metric-grid">
3891
3929
  <div class="metric"><div class="label">Indexed</div><div class="value tone-positive">${formatNumber(ih.indexed)}</div></div>
3892
3930
  <div class="metric"><div class="label">Total inspected</div><div class="value">${formatNumber(ih.total)}</div></div>
@@ -3903,14 +3941,14 @@ function renderCitationsTrend(report) {
3903
3941
  const trend = report.citationsTrend;
3904
3942
  if (trend.length === 0) {
3905
3943
  return section(
3906
- { id: "citations-trend", eyebrow: "Section 10", title: "Citations Over Time" },
3907
- renderEmpty("Run multiple visibility sweeps to see a trend.")
3944
+ { id: "citations-trend", eyebrow: "Section 11", title: "Citations Over Time" },
3945
+ renderEmpty("Run multiple checks to see a trend.")
3908
3946
  );
3909
3947
  }
3910
3948
  if (isTrendBaseline(trend)) {
3911
3949
  return section(
3912
- { id: "citations-trend", eyebrow: "Section 10", title: "Citations Over Time" },
3913
- renderEmpty(`Establishing baseline (${trend.length} of ${MIN_TREND_POINTS} runs collected). Trend will appear once more sweeps are recorded.`)
3950
+ { id: "citations-trend", eyebrow: "Section 11", title: "Citations Over Time" },
3951
+ renderEmpty(`Building baseline (${trend.length} of ${MIN_TREND_POINTS} checks completed). Trend will appear once more checks are recorded.`)
3914
3952
  );
3915
3953
  }
3916
3954
  const chart = renderLineChart(
@@ -3926,11 +3964,11 @@ function renderCitationsTrend(report) {
3926
3964
  <td>${t.providerRates.map((r) => `${escapeHtml(r.provider)}: ${r.citationRate}%`).join(" \xB7 ")}</td>
3927
3965
  </tr>`).join("");
3928
3966
  return section(
3929
- { id: "citations-trend", eyebrow: "Section 10", title: "Citations Over Time", intro: "Citation coverage across completed visibility sweeps." },
3967
+ { id: "citations-trend", eyebrow: "Section 11", title: "Citations Over Time", intro: "Citation coverage across recent checks." },
3930
3968
  `${chart}
3931
- <div class="chart-card"><h3>Run-by-run breakdown</h3>
3969
+ <div class="chart-card"><h3>Check-by-check breakdown</h3>
3932
3970
  <table class="report-table">
3933
- <thead><tr><th>Run</th><th class="numeric">Cited queries</th><th>Per-provider rates</th></tr></thead>
3971
+ <thead><tr><th>Check</th><th class="numeric">Cited queries</th><th>Per-engine rates</th></tr></thead>
3934
3972
  <tbody>${rows}</tbody>
3935
3973
  </table>
3936
3974
  </div>`
@@ -3940,8 +3978,8 @@ function renderInsights(report) {
3940
3978
  const list = report.insights;
3941
3979
  if (list.length === 0) {
3942
3980
  return section(
3943
- { id: "insights", eyebrow: "Section 11", title: "Insights & Alerts" },
3944
- renderEmpty("No insights yet \u2014 run a visibility sweep to generate alerts.")
3981
+ { id: "insights", eyebrow: "Section 12", title: "Insights & Alerts" },
3982
+ renderEmpty("No insights yet \u2014 run a check to generate alerts.")
3945
3983
  );
3946
3984
  }
3947
3985
  const haveDeduped = list.every((i) => typeof i.instanceCount === "number");
@@ -3957,7 +3995,7 @@ function renderInsights(report) {
3957
3995
  </tr>`;
3958
3996
  }).join("");
3959
3997
  return section(
3960
- { id: "insights", eyebrow: "Section 11", title: "Insights & Alerts", intro: "Regressions, gains, and recurring alerts ordered by severity." },
3998
+ { id: "insights", eyebrow: "Section 12", title: "Insights & Alerts", intro: "Regressions, gains, and recurring alerts ordered by severity." },
3961
3999
  `<table class="report-table insights-table">
3962
4000
  <thead><tr>
3963
4001
  <th class="col-severity">Severity</th>
@@ -3999,7 +4037,7 @@ function renderOpportunities(report) {
3999
4037
  return section(
4000
4038
  {
4001
4039
  id: "content-opportunities",
4002
- eyebrow: "Section 12",
4040
+ eyebrow: "Section 13",
4003
4041
  title: "Content Opportunities",
4004
4042
  intro: "Queries where content work has the clearest path to more AI citations. Opportunity score is 0\u2013100, higher = stronger."
4005
4043
  },
@@ -4025,7 +4063,7 @@ function renderContentGaps(report) {
4025
4063
  return section(
4026
4064
  {
4027
4065
  id: "content-gaps",
4028
- eyebrow: "Section 13",
4066
+ eyebrow: "Section 14",
4029
4067
  title: "Content Gaps",
4030
4068
  intro: "Tracked queries where competitors are cited and the client is missing."
4031
4069
  },
@@ -4039,7 +4077,7 @@ function renderRecommendedNextSteps(report) {
4039
4077
  const steps = report.recommendedNextSteps;
4040
4078
  if (steps.length === 0) {
4041
4079
  return section(
4042
- { id: "recommended-next-steps", eyebrow: "Section 14", title: "Recommended Next Steps", intro: "Action items bucketed by timing." },
4080
+ { id: "recommended-next-steps", eyebrow: "Section 15", title: "Recommended Next Steps", intro: "Action items bucketed by timing." },
4043
4081
  renderEmpty("No outstanding actions.")
4044
4082
  );
4045
4083
  }
@@ -4050,7 +4088,7 @@ function renderRecommendedNextSteps(report) {
4050
4088
  <span class="rationale">${escapeHtml(s.rationale)}</span>
4051
4089
  </div>`).join("");
4052
4090
  return section(
4053
- { id: "recommended-next-steps", eyebrow: "Section 14", title: "Recommended Next Steps", intro: "Action items bucketed by timing." },
4091
+ { id: "recommended-next-steps", eyebrow: "Section 15", title: "Recommended Next Steps", intro: "Action items bucketed by timing." },
4054
4092
  `<div class="steps">${items}</div>`
4055
4093
  );
4056
4094
  }
@@ -4195,10 +4233,12 @@ function renderReportHtml(report, opts = {}) {
4195
4233
  const title = opts.title ?? `Canonry ${audience} report \u2014 ${report.meta.project.displayName}`;
4196
4234
  const sections = audience === "client" ? [
4197
4235
  renderClientSummary(report),
4236
+ renderWhatsChanged(report),
4198
4237
  renderAudienceActionPlan(report, "client"),
4199
4238
  renderClientEvidenceSummary(report)
4200
4239
  ].join("\n") : [
4201
4240
  renderExecutiveSummary(report),
4241
+ renderWhatsChanged(report),
4202
4242
  renderAudienceActionPlan(report, "agency"),
4203
4243
  renderAgencyDiagnostics(report),
4204
4244
  renderCitationScorecard(report),
@@ -5020,7 +5060,7 @@ function buildExecutiveFindings(citationRate, citedQueryCount, totalQueryCount,
5020
5060
  const tone = trendBaseline ? "neutral" : trend === "up" ? "positive" : trend === "down" ? "negative" : "neutral";
5021
5061
  let detail;
5022
5062
  if (trendBaseline) {
5023
- detail = `Establishing baseline (${trendsPoints.length} of ${MIN_TREND_POINTS} runs collected).`;
5063
+ detail = `Building baseline (${trendsPoints.length} of ${MIN_TREND_POINTS} checks completed).`;
5024
5064
  } else {
5025
5065
  switch (trend) {
5026
5066
  case "up":
@@ -5120,7 +5160,7 @@ function buildReportActionPlan(input) {
5120
5160
  horizon: "immediate",
5121
5161
  category: "competitors",
5122
5162
  title: "Define the competitor set Canonry should benchmark against",
5123
- action: "Review the recurring external source domains and add the true competitors before the next sweep.",
5163
+ action: "Review the recurring external source domains and add the true competitors before the next check.",
5124
5164
  why: [
5125
5165
  "The report can identify repeated external sources, but it cannot separate competitors from publishers until competitors are configured.",
5126
5166
  "A clean competitor set makes future share-of-voice and content-gap reporting easier to explain to clients."
@@ -5154,7 +5194,7 @@ function buildReportActionPlan(input) {
5154
5194
  action: opportunity.ourBestPage ? `${verb} ${target} so it directly answers the tracked query and cites the strongest supporting evidence.` : `${verb} ${target} that directly answers the query and earns citations from AI answer engines.`,
5155
5195
  why: opportunity.drivers.length > 0 ? opportunity.drivers : ["Canonry ranked this as a content opportunity from search-demand and citation evidence."],
5156
5196
  evidence,
5157
- successMetric: `A future sweep cites ${input.canonicalDomain} for "${opportunity.query}" and the matching GSC query/page improves.`,
5197
+ successMetric: `A future check cites ${input.canonicalDomain} for "${opportunity.query}" and the matching GSC query/page improves.`,
5158
5198
  confidence: opportunity.actionConfidence
5159
5199
  });
5160
5200
  }
@@ -5194,7 +5234,7 @@ function buildReportActionPlan(input) {
5194
5234
  "This points the agency toward provider-specific evidence gaps instead of a generic content recommendation."
5195
5235
  ],
5196
5236
  evidence: zeroCitationProviders.map((p) => `${p.provider}: 0/${p.totalCount} cited query-provider pairs`),
5197
- successMetric: "At least one zero-citation provider cites the client on a priority query in a later sweep.",
5237
+ successMetric: "At least one zero-citation engine cites the client on a priority query in a later check.",
5198
5238
  confidence: "high"
5199
5239
  });
5200
5240
  }
@@ -5258,13 +5298,13 @@ function buildReportActionPlan(input) {
5258
5298
  horizon: "medium-term",
5259
5299
  category: "location",
5260
5300
  title: "Keep location-scoped reporting separate by market",
5261
- action: "Run and compare separate sweeps for each configured location before making market-level recommendations.",
5301
+ action: "Run and compare separate checks for each configured location before making market-level recommendations.",
5262
5302
  why: [
5263
5303
  "A multi-location client can appear differently by market.",
5264
5304
  "Keeping each report location-scoped avoids mixing Florida and Michigan evidence in the same client story."
5265
5305
  ],
5266
5306
  evidence,
5267
- successMetric: "Each configured market has its own current sweep and trend before cross-market decisions are made.",
5307
+ successMetric: "Each configured market has its own current check and trend before cross-market decisions are made.",
5268
5308
  confidence: "high"
5269
5309
  });
5270
5310
  }
@@ -5275,10 +5315,10 @@ function buildReportActionPlan(input) {
5275
5315
  horizon: "short-term",
5276
5316
  category: "monitoring",
5277
5317
  title: "Keep monitoring citation and mention coverage",
5278
- action: "Run the next scheduled visibility sweep and watch for citation gains, losses, and provider-specific misses.",
5318
+ action: "Run the next scheduled check and watch for citation gains, losses, and engine-specific misses.",
5279
5319
  why: [
5280
5320
  "No urgent corrective action surfaced from the current evidence.",
5281
- "AEO performance is directional; repeated sweeps are needed before overreacting to a single sample."
5321
+ "AEO performance is directional; repeated checks are needed before overreacting to a single sample."
5282
5322
  ],
5283
5323
  evidence: ["No critical insights, content gaps, indexing blockers, or provider-zero issues were detected in this report."],
5284
5324
  successMetric: "Coverage stays stable or improves across the next trend window.",
@@ -5290,11 +5330,11 @@ function buildReportActionPlan(input) {
5290
5330
  function trendSentence(trend) {
5291
5331
  switch (trend) {
5292
5332
  case "up":
5293
- return "Citation coverage improved versus the prior comparable sweep.";
5333
+ return "Citation coverage improved versus the prior comparable check.";
5294
5334
  case "down":
5295
- return "Citation coverage declined versus the prior comparable sweep.";
5335
+ return "Citation coverage declined versus the prior comparable check.";
5296
5336
  case "flat":
5297
- return "Citation coverage is flat versus the prior comparable sweep.";
5337
+ return "Citation coverage is flat versus the prior comparable check.";
5298
5338
  case "unknown":
5299
5339
  return "There is not enough comparable run history yet to call a trend.";
5300
5340
  }
@@ -5302,16 +5342,16 @@ function trendSentence(trend) {
5302
5342
  function buildClientSummary(reportLike) {
5303
5343
  const s = reportLike.executiveSummary;
5304
5344
  const queryNoun = s.totalQueryCount === 1 ? "query" : "queries";
5305
- const headline = s.totalQueryCount > 0 ? `${s.citedQueryCount} of ${s.totalQueryCount} tracked ${queryNoun} are cited by AI engines` : "No tracked queries have completed a visibility sweep yet";
5306
- const overview = s.totalQueryCount > 0 ? `${reportLike.canonicalDomain} is cited on ${s.citationRate}% of tracked queries and mentioned on ${s.mentionRate}% of tracked queries. ${trendSentence(s.trend)}` : "Canonry needs at least one completed visibility sweep before it can summarize how the brand appears in AI answers.";
5345
+ const headline = s.totalQueryCount > 0 ? `${s.citedQueryCount} of ${s.totalQueryCount} tracked ${queryNoun} are cited by AI engines` : "No tracked queries have completed a check yet";
5346
+ const overview = s.totalQueryCount > 0 ? `${reportLike.canonicalDomain} is cited on ${s.citationRate}% of tracked queries and mentioned on ${s.mentionRate}% of tracked queries. ${trendSentence(s.trend)}` : "At least one completed check is needed before this can summarize how the brand appears in AI answers.";
5307
5347
  const confidenceNotes = [];
5308
5348
  if (s.totalQueryCount === 0) {
5309
- confidenceNotes.push("Confidence is low until the first tracked query sweep completes.");
5349
+ confidenceNotes.push("Confidence is low until the first tracked query check completes.");
5310
5350
  } else if (s.totalQueryCount < 5) {
5311
5351
  confidenceNotes.push("Directional read: the tracked query set is still small, so each query has outsized impact on the percentage.");
5312
5352
  }
5313
5353
  if (isTrendBaseline(reportLike.citationsTrend)) {
5314
- confidenceNotes.push(`Trend confidence is still developing; ${MIN_TREND_POINTS} comparable sweeps are needed for a stable trend.`);
5354
+ confidenceNotes.push(`Trend confidence is still developing; ${MIN_TREND_POINTS} comparable checks are needed for a stable trend.`);
5315
5355
  }
5316
5356
  if (!reportLike.gsc) {
5317
5357
  confidenceNotes.push("Search Console is not connected, so content recommendations lean more heavily on citation and competitor evidence.");
@@ -5331,13 +5371,13 @@ function buildAgencyDiagnostics(input) {
5331
5371
  const zeroCitationProviders = input.citationScorecard.providerRates.filter((p) => p.totalCount > 0 && p.citedCount === 0);
5332
5372
  diagnostics.push({
5333
5373
  title: "Provider citation coverage",
5334
- detail: zeroCitationProviders.length > 0 ? `${zeroCitationProviders.length} provider${zeroCitationProviders.length === 1 ? "" : "s"} returned zero client citations in the latest sweep.` : "Every provider with completed snapshots produced at least one client citation or no provider data is available yet.",
5374
+ detail: zeroCitationProviders.length > 0 ? `${zeroCitationProviders.length} engine${zeroCitationProviders.length === 1 ? "" : "s"} returned zero client citations in the latest check.` : "Every provider with completed snapshots produced at least one client citation or no provider data is available yet.",
5335
5375
  severity: zeroCitationProviders.length > 0 ? "negative" : "positive",
5336
5376
  evidence: zeroCitationProviders.length > 0 ? zeroCitationProviders.map((p) => `${p.provider}: 0/${p.totalCount}`) : input.citationScorecard.providerRates.map((p) => `${p.provider}: ${p.citedCount}/${p.totalCount}`)
5337
5377
  });
5338
5378
  diagnostics.push({
5339
5379
  title: "AI source domains",
5340
- detail: input.aiSourceOrigin.topDomains.length > 0 ? "Repeated external source domains show what AI engines are currently trusting for this topic set." : "No external source-domain evidence is available from the latest sweep yet.",
5380
+ detail: input.aiSourceOrigin.topDomains.length > 0 ? "Repeated external source domains show what AI engines are currently trusting for this topic set." : "No external source-domain evidence is available from the latest check yet.",
5341
5381
  severity: input.aiSourceOrigin.topDomains.length > 0 ? "neutral" : "caution",
5342
5382
  evidence: input.aiSourceOrigin.topDomains.slice(0, 5).map((d) => `${d.domain}: ${d.count}`)
5343
5383
  });
@@ -5374,6 +5414,112 @@ function buildAgencyDiagnostics(input) {
5374
5414
  diagnostics
5375
5415
  };
5376
5416
  }
5417
+ var WHATS_CHANGED_PERIOD_DAYS2 = 14;
5418
+ var WHATS_CHANGED_MIN_TREND_POINTS = WHATS_CHANGED_PERIOD_DAYS2 * 2;
5419
+ var WIN_REGRESSION_LIMIT = 5;
5420
+ function rateDirection(delta, threshold = 0.5) {
5421
+ if (delta > threshold) return "up";
5422
+ if (delta < -threshold) return "down";
5423
+ return "flat";
5424
+ }
5425
+ function periodOverPeriodDelta(trend) {
5426
+ if (trend.length < WHATS_CHANGED_MIN_TREND_POINTS) return null;
5427
+ const tail = trend.slice(-WHATS_CHANGED_PERIOD_DAYS2);
5428
+ const prior = trend.slice(-WHATS_CHANGED_PERIOD_DAYS2 * 2, -WHATS_CHANGED_PERIOD_DAYS2);
5429
+ const current = tail.reduce((s, p) => s + p.value, 0);
5430
+ const priorTotal = prior.reduce((s, p) => s + p.value, 0);
5431
+ const deltaAbs = current - priorTotal;
5432
+ return {
5433
+ current,
5434
+ prior: priorTotal,
5435
+ deltaAbs,
5436
+ direction: rateDirection(deltaAbs, 0)
5437
+ };
5438
+ }
5439
+ function buildWhatsChangedHeadline(citation, gscClicks, aiReferrals, enoughHistory, trendLength) {
5440
+ if (!enoughHistory) {
5441
+ return `Building baseline (${trendLength} of ${MIN_TREND_POINTS} checks completed). Trends appear after a few more checks.`;
5442
+ }
5443
+ const parts = [];
5444
+ if (citation) {
5445
+ const arrow = citation.direction === "up" ? "\u2191" : citation.direction === "down" ? "\u2193" : "\u2192";
5446
+ const verb = citation.direction === "up" ? "rose" : citation.direction === "down" ? "fell" : "held";
5447
+ parts.push(`Citation rate ${verb} ${citation.prior}% ${arrow} ${citation.current}%`);
5448
+ }
5449
+ if (aiReferrals && aiReferrals.direction !== "flat") {
5450
+ const arrow = aiReferrals.direction === "up" ? "\u2191" : "\u2193";
5451
+ parts.push(`AI referrals ${arrow}${Math.abs(aiReferrals.deltaAbs)} sessions vs prior 14 days`);
5452
+ } else if (gscClicks && gscClicks.direction !== "flat") {
5453
+ const arrow = gscClicks.direction === "up" ? "\u2191" : "\u2193";
5454
+ parts.push(`GSC clicks ${arrow}${Math.abs(gscClicks.deltaAbs)} vs prior 14 days`);
5455
+ }
5456
+ return parts.length > 0 ? `${parts.join(" \xB7 ")}.` : "No meaningful movement vs the prior period.";
5457
+ }
5458
+ function buildWhatsChanged(input) {
5459
+ const { citationsTrend, gsc, aiReferrals, insights: insightList } = input;
5460
+ const baseline = isTrendBaseline(citationsTrend);
5461
+ const latest = citationsTrend.at(-1);
5462
+ const prior = citationsTrend.length >= 2 ? citationsTrend.at(-2) : null;
5463
+ const enoughHistory = !baseline && latest !== void 0 && prior !== void 0;
5464
+ const citationRate = enoughHistory ? {
5465
+ current: latest.citationRate,
5466
+ prior: prior.citationRate,
5467
+ deltaAbs: latest.citationRate - prior.citationRate,
5468
+ direction: rateDirection(latest.citationRate - prior.citationRate)
5469
+ } : null;
5470
+ const mentionRate = enoughHistory ? {
5471
+ current: latest.mentionRate,
5472
+ prior: prior.mentionRate,
5473
+ deltaAbs: latest.mentionRate - prior.mentionRate,
5474
+ direction: rateDirection(latest.mentionRate - prior.mentionRate)
5475
+ } : null;
5476
+ const citedQueryCount = enoughHistory ? {
5477
+ current: latest.citedQueryCount,
5478
+ prior: prior.citedQueryCount,
5479
+ deltaAbs: latest.citedQueryCount - prior.citedQueryCount,
5480
+ direction: rateDirection(latest.citedQueryCount - prior.citedQueryCount, 0)
5481
+ } : null;
5482
+ const providerMovements = [];
5483
+ if (enoughHistory) {
5484
+ const priorByProvider = new Map(prior.providerRates.map((p) => [p.provider, p.citationRate]));
5485
+ for (const cur of latest.providerRates) {
5486
+ const priorRate = priorByProvider.get(cur.provider);
5487
+ if (priorRate === void 0) continue;
5488
+ const deltaAbs = cur.citationRate - priorRate;
5489
+ providerMovements.push({
5490
+ provider: cur.provider,
5491
+ current: cur.citationRate,
5492
+ prior: priorRate,
5493
+ deltaAbs,
5494
+ direction: rateDirection(deltaAbs)
5495
+ });
5496
+ }
5497
+ providerMovements.sort((a, b) => Math.abs(b.deltaAbs) - Math.abs(a.deltaAbs));
5498
+ }
5499
+ const gscClicksDelta = gsc ? periodOverPeriodDelta(gsc.trend.map((t) => ({ date: t.date, value: t.clicks }))) : null;
5500
+ const aiReferralsDelta = aiReferrals ? periodOverPeriodDelta(aiReferrals.trend.map((t) => ({ date: t.date, value: t.sessions }))) : null;
5501
+ const wins = insightList.filter((i) => i.type === "gain").slice(0, WIN_REGRESSION_LIMIT);
5502
+ const regressions = insightList.filter((i) => i.type === "regression").slice(0, WIN_REGRESSION_LIMIT);
5503
+ const headline = buildWhatsChangedHeadline(
5504
+ citationRate,
5505
+ gscClicksDelta,
5506
+ aiReferralsDelta,
5507
+ enoughHistory,
5508
+ citationsTrend.length
5509
+ );
5510
+ return {
5511
+ enoughHistory,
5512
+ headline,
5513
+ citationRate,
5514
+ mentionRate,
5515
+ citedQueryCount,
5516
+ gscClicksDelta,
5517
+ aiReferralsDelta,
5518
+ providerMovements,
5519
+ wins,
5520
+ regressions
5521
+ };
5522
+ }
5377
5523
  function buildProjectReport(db, projectName) {
5378
5524
  const project = resolveProject(db, projectName);
5379
5525
  const queryLookup = loadQueryLookup(db, project.id);
@@ -5426,6 +5572,12 @@ function buildProjectReport(db, projectName) {
5426
5572
  contentOpportunities,
5427
5573
  insightDerivedSteps
5428
5574
  );
5575
+ const whatsChanged = buildWhatsChanged({
5576
+ citationsTrend,
5577
+ gsc: gscSection,
5578
+ aiReferrals: aiReferralsSection,
5579
+ insights: insightList
5580
+ });
5429
5581
  const totalQueryCount = queryLookup.byId.size;
5430
5582
  const citedQueryIds = /* @__PURE__ */ new Set();
5431
5583
  const mentionedQueryIds = /* @__PURE__ */ new Set();
@@ -5553,6 +5705,7 @@ function buildProjectReport(db, projectName) {
5553
5705
  aiReferrals: aiReferralsSection,
5554
5706
  indexingHealth: indexingHealthSection,
5555
5707
  citationsTrend,
5708
+ whatsChanged,
5556
5709
  insights: insightList,
5557
5710
  recommendedNextSteps,
5558
5711
  actionPlan,