@ainyc/canonry 3.5.1 → 3.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/index.html CHANGED
@@ -12,7 +12,7 @@
12
12
  <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
13
13
  <link rel="apple-touch-icon" href="./apple-touch-icon.png" />
14
14
  <title>Canonry</title>
15
- <script type="module" crossorigin src="./assets/index-CFtdvSnQ.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-B5VNd16q.js"></script>
16
16
  <link rel="stylesheet" crossorigin href="./assets/index-BfwQqd05.css">
17
17
  </head>
18
18
  <body>
@@ -4,7 +4,7 @@ import {
4
4
  configExists,
5
5
  loadConfig,
6
6
  saveConfigPatch
7
- } from "./chunk-ZYESHCMF.js";
7
+ } from "./chunk-GLPZ5NVP.js";
8
8
  import {
9
9
  IntelligenceService,
10
10
  MIN_TREND_POINTS,
@@ -49,7 +49,7 @@ import {
49
49
  runs,
50
50
  schedules,
51
51
  usageCounters
52
- } from "./chunk-M4KG7RJT.js";
52
+ } from "./chunk-W463NVVC.js";
53
53
  import {
54
54
  AGENT_MEMORY_VALUE_MAX_BYTES,
55
55
  AGENT_PROVIDER_IDS,
@@ -64,6 +64,7 @@ import {
64
64
  RunKinds,
65
65
  RunStatuses,
66
66
  RunTriggers,
67
+ absolutizeProjectUrl,
67
68
  agentBusy,
68
69
  agentMemoryDeleteRequestSchema,
69
70
  agentMemoryUpsertRequestSchema,
@@ -111,7 +112,7 @@ import {
111
112
  visibilityStateFromAnswerMentioned,
112
113
  windowCutoff,
113
114
  wordpressEnvSchema
114
- } from "./chunk-VIUWGDDU.js";
115
+ } from "./chunk-RDX6GBWM.js";
115
116
 
116
117
  // src/telemetry.ts
117
118
  import crypto from "crypto";
@@ -2783,7 +2784,12 @@ function renderExecutiveSummary(report) {
2783
2784
  <span>${escapeHtml(f.detail)}</span>
2784
2785
  </div>`).join("")}</div>` : "";
2785
2786
  return section(
2786
- { id: "executive-summary", eyebrow: "Section 1", title: "Executive Summary" },
2787
+ {
2788
+ id: "executive-summary",
2789
+ eyebrow: "Section 1",
2790
+ title: "Executive Summary",
2791
+ intro: "Top-line citation rate with trend versus the prior run, plus the most actionable findings from the latest visibility sweep."
2792
+ },
2787
2793
  metricsHtml + findingsHtml
2788
2794
  );
2789
2795
  }
@@ -2842,15 +2848,11 @@ function renderCitationScorecard(report) {
2842
2848
  ${renderCitationMatrix(report.citationScorecard)}
2843
2849
  `;
2844
2850
  return section(
2845
- { id: "citation-scorecard", eyebrow: "Section 2", title: "Citation Scorecard", intro: "Per-keyword \xD7 per-provider citation matrix from the latest visibility sweep." },
2851
+ { id: "citation-scorecard", eyebrow: "Section 2", title: "Citation Scorecard", intro: "Whether your domain appeared in each AI engine\u2019s source list for every tracked keyword in the latest sweep \u2014 a cell turns green when your domain was cited, red when it was not, and gray when no snapshot exists for that pair." },
2846
2852
  body
2847
2853
  );
2848
2854
  }
2849
- function renderCompetitorBars(landscape, canonical) {
2850
- const data = [
2851
- { label: canonical, count: landscape.projectCitationCount, isProject: true },
2852
- ...landscape.competitors.map((c) => ({ label: c.domain, count: c.citationCount, isProject: false }))
2853
- ];
2855
+ function renderLandscapeBars(data, heading, ariaLabel) {
2854
2856
  if (data.length <= 1) return "";
2855
2857
  const max = Math.max(...data.map((d) => d.count), 1);
2856
2858
  const width = 600;
@@ -2867,22 +2869,43 @@ function renderCompetitorBars(landscape, canonical) {
2867
2869
  <text x="${labelWidth + w + 6}" y="${y + 13}" fill="${COLORS.text}" font-size="11">${d.count}</text>`;
2868
2870
  }).join("");
2869
2871
  return `<div class="chart-card">
2870
- <h3>Citations per domain</h3>
2871
- <svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="Citations per domain bar chart">
2872
+ <h3>${escapeHtml(heading)}</h3>
2873
+ <svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="${escapeHtml(ariaLabel)}">
2872
2874
  ${bars}
2873
2875
  </svg>
2874
2876
  </div>`;
2875
2877
  }
2878
+ function renderCompetitorBars(landscape, canonical) {
2879
+ const data = [
2880
+ { label: canonical, count: landscape.projectCitationCount, isProject: true },
2881
+ ...landscape.competitors.map((c) => ({ label: c.domain, count: c.citationCount, isProject: false }))
2882
+ ];
2883
+ return renderLandscapeBars(data, "Citations per domain", "Citations per domain bar chart");
2884
+ }
2885
+ function renderMentionBars(landscape, canonical) {
2886
+ const data = [
2887
+ { label: canonical, count: landscape.projectMentionCount, isProject: true },
2888
+ ...landscape.competitors.map((c) => ({ label: c.domain, count: c.mentionCount, isProject: false }))
2889
+ ];
2890
+ return renderLandscapeBars(data, "Mentions per domain", "Mentions per domain bar chart");
2891
+ }
2876
2892
  function renderCompetitorLandscape(report) {
2877
2893
  const competitors2 = report.competitorLandscape.competitors;
2878
- if (competitors2.length === 0 && report.competitorLandscape.projectCitationCount === 0) {
2894
+ const mentionLandscape = report.mentionLandscape;
2895
+ const noCitationData = competitors2.length === 0 && report.competitorLandscape.projectCitationCount === 0;
2896
+ const noMentionData = mentionLandscape.competitors.length === 0 && mentionLandscape.projectMentionCount === 0;
2897
+ if (noCitationData && noMentionData) {
2879
2898
  return section(
2880
2899
  { id: "competitor-landscape", eyebrow: "Section 3", title: "Competitor Landscape" },
2881
2900
  renderEmpty("No competitor data yet. Add competitors and run a visibility sweep.")
2882
2901
  );
2883
2902
  }
2903
+ const mentionByDomain = new Map(mentionLandscape.competitors.map((m) => [m.domain, m]));
2884
2904
  const rows = competitors2.map((c) => {
2885
2905
  const tone = pressureTone(c.pressureLabel);
2906
+ const mention = mentionByDomain.get(c.domain);
2907
+ const mentionCount = mention?.mentionCount ?? 0;
2908
+ const mentionTotal = mention?.totalCount ?? mentionLandscape.totalAnswerSnapshots;
2886
2909
  const pagesDisclosure = c.theirCitedPages.length > 0 ? `<details class="cited-pages"><summary>${c.theirCitedPages.length} cited URL${c.theirCitedPages.length > 1 ? "s" : ""}</summary>
2887
2910
  <ul>${c.theirCitedPages.map((p) => `<li><a href="${escapeHtml(p.url)}">${escapeHtml(p.url)}</a> <span class="cited-for">${escapeHtml(p.citedFor.join(", "))}</span></li>`).join("")}</ul>
2888
2911
  </details>` : "";
@@ -2890,17 +2913,26 @@ function renderCompetitorLandscape(report) {
2890
2913
  <td>${escapeHtml(c.domain)}</td>
2891
2914
  <td><span class="badge tone-${tone}">${escapeHtml(c.pressureLabel)}</span></td>
2892
2915
  <td class="numeric">${c.citationCount} / ${c.totalCount}</td>
2916
+ <td class="numeric">${mentionCount} / ${mentionTotal}</td>
2893
2917
  <td class="numeric">${c.sharePct}%</td>
2894
2918
  <td>${escapeHtml(c.citedKeywords.slice(0, 5).join(", "))}${c.citedKeywords.length > 5 ? "\u2026" : ""}${pagesDisclosure}</td>
2895
2919
  </tr>`;
2896
2920
  }).join("");
2897
2921
  const table = competitors2.length > 0 ? `<table class="report-table">
2898
- <thead><tr><th>Domain</th><th>Pressure</th><th>Citations</th><th class="numeric">SOV</th><th>Cited keywords</th></tr></thead>
2922
+ <thead><tr><th>Domain</th><th>Pressure</th><th>Citations</th><th class="numeric">Mentions</th><th class="numeric">SOV</th><th>Cited keywords</th></tr></thead>
2899
2923
  <tbody>${rows}</tbody>
2900
2924
  </table>` : renderEmpty("No competitors configured.");
2925
+ const citationBars = renderCompetitorBars(report.competitorLandscape, report.meta.project.canonicalDomain);
2926
+ const mentionBars = renderMentionBars(mentionLandscape, report.meta.project.canonicalDomain);
2927
+ const charts = citationBars && mentionBars ? `<div class="chart-grid">${citationBars}${mentionBars}</div>` : `${citationBars}${mentionBars}`;
2901
2928
  return section(
2902
- { id: "competitor-landscape", eyebrow: "Section 3", title: "Competitor Landscape", intro: "Where tracked competitors appear in AI answers compared to your domain." },
2903
- `${renderCompetitorBars(report.competitorLandscape, report.meta.project.canonicalDomain)}${table}`
2929
+ {
2930
+ id: "competitor-landscape",
2931
+ eyebrow: "Section 3",
2932
+ title: "Competitor Landscape",
2933
+ intro: "Where tracked competitors appear in AI answers compared to your domain \u2014 both in source citations and in the answer text itself."
2934
+ },
2935
+ `${charts}${table}`
2904
2936
  );
2905
2937
  }
2906
2938
  function renderDonut(buckets) {
@@ -2947,7 +2979,7 @@ function renderAiSourceOrigin(report) {
2947
2979
  const origin = report.aiSourceOrigin;
2948
2980
  if (origin.categories.length === 0 && origin.topDomains.length === 0) {
2949
2981
  return section(
2950
- { id: "ai-source-origin", eyebrow: "Section 4", title: "AI Source Origin" },
2982
+ { id: "ai-source-origin", eyebrow: "Section 4", title: "AI Citation Sources" },
2951
2983
  renderEmpty("No source data yet. Run a visibility sweep first.")
2952
2984
  );
2953
2985
  }
@@ -2962,7 +2994,12 @@ function renderAiSourceOrigin(report) {
2962
2994
  <tbody>${rows}</tbody>
2963
2995
  </table>` : "";
2964
2996
  return section(
2965
- { id: "ai-source-origin", eyebrow: "Section 4", title: "AI Source Origin", intro: "Where AI answers pull from, aggregated across the latest sweep." },
2997
+ {
2998
+ id: "ai-source-origin",
2999
+ eyebrow: "Section 4",
3000
+ title: "AI Citation Sources",
3001
+ intro: "Every external website AI engines cited as a source for your tracked keywords in the latest sweep \u2014 categorized by site type (Reddit, YouTube, news, etc.) on the left and ranked by citation count on the right. Your own domains are excluded; tracked competitors are flagged."
3002
+ },
2966
3003
  `${renderDonut(origin.categories)}${table}`
2967
3004
  );
2968
3005
  }
@@ -3041,7 +3078,7 @@ function renderGsc(report) {
3041
3078
  </div>`);
3042
3079
  }
3043
3080
  return section(
3044
- { id: "gsc", eyebrow: "Section 5", title: "GSC Performance", intro: "Top queries, category breakdown, and traffic trend from Google Search Console." },
3081
+ { id: "gsc", eyebrow: "Section 5", title: "GSC Performance", intro: "Your site\u2019s performance in Google\u2019s regular (non-AI) search results \u2014 top queries that drove impressions, intent breakdown, and the click trend, sourced from Google Search Console for the most recent sync window." },
3045
3082
  `<div class="metric-grid">
3046
3083
  <div class="metric"><div class="label">Total clicks</div><div class="value">${formatNumber(gsc.totalClicks)}</div></div>
3047
3084
  <div class="metric"><div class="label">Total impressions</div><div class="value">${formatNumber(gsc.totalImpressions)}</div></div>
@@ -3086,7 +3123,7 @@ function renderGa(report) {
3086
3123
  <td class="numeric">${c.sharePct}%</td>
3087
3124
  </tr>`).join("");
3088
3125
  return section(
3089
- { id: "ga", eyebrow: "Section 6", title: "GA4 Traffic", intro: `Sessions and users for ${formatDate(ga.periodStart)} \u2192 ${formatDate(ga.periodEnd)}.` },
3126
+ { id: "ga", eyebrow: "Section 6", title: "GA4 Traffic", intro: `Total sessions and users on your site between ${formatDate(ga.periodStart)} and ${formatDate(ga.periodEnd)}, with the top landing pages and channel breakdown \u2014 sourced from Google Analytics 4.` },
3090
3127
  `<div class="metric-grid">
3091
3128
  <div class="metric"><div class="label">Total sessions</div><div class="value">${formatNumber(ga.totalSessions)}</div></div>
3092
3129
  <div class="metric"><div class="label">Total users</div><div class="value">${formatNumber(ga.totalUsers)}</div></div>
@@ -3127,7 +3164,7 @@ function renderSocial(report) {
3127
3164
  <td class="numeric">${formatNumber(c.sessions)}</td>
3128
3165
  </tr>`).join("");
3129
3166
  return section(
3130
- { id: "social-referrals", eyebrow: "Section 7", title: "Social Referrals", intro: "Paid vs organic split with top campaigns." },
3167
+ { id: "social-referrals", eyebrow: "Section 7", title: "Social Referrals", intro: "Sessions on your site sent by social platforms (LinkedIn, Facebook, X, etc.) \u2014 paid versus organic split and the top campaigns that drove them. Sourced from Google Analytics 4." },
3131
3168
  `<div class="metric-grid">
3132
3169
  <div class="metric"><div class="label">Total sessions</div><div class="value">${formatNumber(social.totalSessions)}</div></div>
3133
3170
  <div class="metric"><div class="label">Organic social</div><div class="value">${formatNumber(social.organicSessions)}</div></div>
@@ -3174,7 +3211,7 @@ function renderAiReferrals(report) {
3174
3211
  "AI referral sessions over time"
3175
3212
  );
3176
3213
  return section(
3177
- { id: "ai-referrals", eyebrow: "Section 8", title: "AI Referral Traffic", intro: "Sessions sent from AI answer engines." },
3214
+ { id: "ai-referrals", eyebrow: "Section 8", title: "AI Referral Traffic", intro: "Sessions on your site referred by AI answer engines (ChatGPT, Perplexity, Claude, Copilot, Gemini, etc.) \u2014 broken down by referrer with a daily trend and the top landing pages. Sourced from Google Analytics 4." },
3178
3215
  `<div class="metric-grid">
3179
3216
  <div class="metric"><div class="label">Total sessions</div><div class="value">${formatNumber(ai.totalSessions)}</div></div>
3180
3217
  <div class="metric"><div class="label">Total users</div><div class="value">${formatNumber(ai.totalUsers)}</div></div>
@@ -3220,7 +3257,7 @@ function renderIndexingHealth(report) {
3220
3257
  }).join("");
3221
3258
  const legend = segments.map((s) => `<span><span class="legend-swatch" style="background:${s.color}"></span>${escapeHtml(s.label)}: ${s.count}</span>`).join("");
3222
3259
  return section(
3223
- { id: "indexing-health", eyebrow: "Section 9", title: "Indexing Health", intro: `Source: ${ih.provider === "google" ? "Google Search Console" : "Bing Webmaster Tools"}.` },
3260
+ { id: "indexing-health", eyebrow: "Section 9", title: "Indexing Health", intro: `What share of your tracked URLs are currently indexed in ${ih.provider === "google" ? "Google" : "Bing"} \u2014 sourced from ${ih.provider === "google" ? "Google Search Console URL Inspection" : "Bing Webmaster Tools URL Inspection"}. Pages absent from the index can\u2019t be retrieved by AI engines either.` },
3224
3261
  `<div class="metric-grid">
3225
3262
  <div class="metric"><div class="label">Indexed</div><div class="value tone-positive">${formatNumber(ih.indexed)}</div></div>
3226
3263
  <div class="metric"><div class="label">Total inspected</div><div class="value">${formatNumber(ih.total)}</div></div>
@@ -3260,7 +3297,7 @@ function renderCitationsTrend(report) {
3260
3297
  <td>${t.providerRates.map((r) => `${escapeHtml(r.provider)}: ${r.citationRate}%`).join(" \xB7 ")}</td>
3261
3298
  </tr>`).join("");
3262
3299
  return section(
3263
- { id: "citations-trend", eyebrow: "Section 10", title: "Citations Over Time", intro: "Per-run citation rate across the project history." },
3300
+ { id: "citations-trend", eyebrow: "Section 10", title: "Citations Over Time", intro: "Citation rate across every visibility sweep \u2014 the share of (keyword \xD7 provider) pairs in each run where your domain appeared in the source list, with a per-provider breakdown beneath." },
3264
3301
  `${chart}
3265
3302
  <div class="chart-card"><h3>Run-by-run breakdown</h3>
3266
3303
  <table class="report-table">
@@ -3291,7 +3328,7 @@ function renderInsights(report) {
3291
3328
  </tr>`;
3292
3329
  }).join("");
3293
3330
  return section(
3294
- { id: "insights", eyebrow: "Section 11", title: "Insights & Alerts", intro: "Priority-ordered findings from the most recent runs." },
3331
+ { id: "insights", eyebrow: "Section 11", title: "Insights & Alerts", intro: "Regressions (citations lost), gains (citations won), and opportunities surfaced by the intelligence engine across the most recent sweeps \u2014 ordered by severity and recurrence." },
3295
3332
  `<table class="report-table">
3296
3333
  <thead><tr><th>Severity</th><th>Title</th><th>Keyword</th><th>Provider</th><th>Recommendation</th></tr></thead>
3297
3334
  <tbody>${rows}</tbody>
@@ -3301,8 +3338,9 @@ function renderInsights(report) {
3301
3338
  function renderOpportunities(report) {
3302
3339
  const opps = report.contentOpportunities;
3303
3340
  if (opps.length === 0) return "";
3341
+ const canonical = report.meta.project.canonicalDomain;
3304
3342
  const rows = opps.slice(0, 10).map((o) => {
3305
- const ourPage = o.ourBestPage ? `<a href="${escapeHtml(o.ourBestPage.url)}">${escapeHtml(o.ourBestPage.url)}</a>` : '<span class="cell-not-cited">\u2014</span>';
3343
+ const ourPage = o.ourBestPage ? `<a href="${escapeHtml(absolutizeProjectUrl(o.ourBestPage.url, canonical))}">${escapeHtml(o.ourBestPage.url)}</a>` : '<span class="cell-not-cited">\u2014</span>';
3306
3344
  const winning = o.winningCompetitor ? `<a href="${escapeHtml(o.winningCompetitor.url)}">${escapeHtml(o.winningCompetitor.domain)}</a>` : '<span class="cell-not-cited">\u2014</span>';
3307
3345
  return `<tr>
3308
3346
  <td>${escapeHtml(o.query)}</td>
@@ -3319,7 +3357,7 @@ function renderOpportunities(report) {
3319
3357
  id: "content-opportunities",
3320
3358
  eyebrow: "Section 12",
3321
3359
  title: "Content Opportunities",
3322
- intro: "Ranked, action-typed targets from the content recommendation engine. Top 10 shown."
3360
+ intro: "Queries where you have search demand or competitor citation pressure but aren\u2019t winning AI citations. Each row carries a suggested action (create / refresh / expand / add-schema). Top 10 shown."
3323
3361
  },
3324
3362
  `<table class="report-table">
3325
3363
  <thead><tr><th>Query</th><th>Action</th><th class="numeric">Score</th><th>Our page</th><th>Winning competitor</th><th>Demand</th><th>Confidence</th></tr></thead>
@@ -3331,7 +3369,7 @@ function renderRecommendedNextSteps(report) {
3331
3369
  const steps = report.recommendedNextSteps;
3332
3370
  if (steps.length === 0) {
3333
3371
  return section(
3334
- { id: "recommended-next-steps", eyebrow: "Section 13", title: "Recommended Next Steps" },
3372
+ { id: "recommended-next-steps", eyebrow: "Section 13", title: "Recommended Next Steps", intro: "Action items bucketed by horizon (immediate, short-term, medium-term), drawn from open insights and the highest-ranked content opportunities." },
3335
3373
  renderEmpty("No outstanding actions.")
3336
3374
  );
3337
3375
  }
@@ -3342,7 +3380,7 @@ function renderRecommendedNextSteps(report) {
3342
3380
  <span class="rationale">${escapeHtml(s.rationale)}</span>
3343
3381
  </div>`).join("");
3344
3382
  return section(
3345
- { id: "recommended-next-steps", eyebrow: "Section 13", title: "Recommended Next Steps" },
3383
+ { id: "recommended-next-steps", eyebrow: "Section 13", title: "Recommended Next Steps", intro: "Action items bucketed by horizon (immediate, short-term, medium-term), drawn from open insights and the highest-ranked content opportunities." },
3346
3384
  `<div class="steps">${items}</div>`
3347
3385
  );
3348
3386
  }
@@ -3818,6 +3856,56 @@ function buildCompetitorLandscape(snapshots, competitorDomains, projectDomains,
3818
3856
  competitorRows.sort((a, b) => b.citationCount - a.citationCount);
3819
3857
  return { projectCitationCount, competitors: competitorRows };
3820
3858
  }
3859
+ function buildMentionLandscape(snapshots, competitorDomains, projectDisplayName, projectDomains, keywordLookup) {
3860
+ let projectMentionCount = 0;
3861
+ let totalAnswerSnapshots = 0;
3862
+ const competitorMap = /* @__PURE__ */ new Map();
3863
+ for (const c of competitorDomains) {
3864
+ competitorMap.set(c, { count: 0, keywords: /* @__PURE__ */ new Set() });
3865
+ }
3866
+ for (const snap of snapshots) {
3867
+ const text = snap.answerText;
3868
+ if (!text) continue;
3869
+ totalAnswerSnapshots++;
3870
+ const kw = keywordLookup.byId.get(snap.keywordId);
3871
+ const projectMentioned = snap.answerMentioned ?? determineAnswerMentioned(
3872
+ text,
3873
+ projectDisplayName,
3874
+ projectDomains
3875
+ );
3876
+ if (projectMentioned) projectMentionCount++;
3877
+ for (const competitor of competitorDomains) {
3878
+ const brand = brandLabelFromDomain(competitor);
3879
+ const mentioned = determineAnswerMentioned(text, brand, [competitor]);
3880
+ if (mentioned) {
3881
+ const entry = competitorMap.get(competitor);
3882
+ entry.count++;
3883
+ if (kw) entry.keywords.add(kw);
3884
+ }
3885
+ }
3886
+ }
3887
+ const totalMentionedSlots = projectMentionCount + [...competitorMap.values()].reduce((sum, v) => sum + v.count, 0);
3888
+ const competitorRows = [...competitorMap.entries()].map(([domain, data]) => {
3889
+ const ratio = totalAnswerSnapshots > 0 ? data.count / totalAnswerSnapshots : 0;
3890
+ let pressureLabel = "None";
3891
+ if (data.count > 0) {
3892
+ if (ratio >= 0.5) pressureLabel = "High";
3893
+ else if (ratio >= 0.2) pressureLabel = "Moderate";
3894
+ else pressureLabel = "Low";
3895
+ }
3896
+ const sharePct = totalMentionedSlots > 0 ? Math.round(data.count / totalMentionedSlots * 100) : 0;
3897
+ return {
3898
+ domain,
3899
+ mentionCount: data.count,
3900
+ totalCount: totalAnswerSnapshots,
3901
+ pressureLabel,
3902
+ mentionedKeywords: [...data.keywords].sort(),
3903
+ sharePct
3904
+ };
3905
+ });
3906
+ competitorRows.sort((a, b) => b.mentionCount - a.mentionCount);
3907
+ return { projectMentionCount, totalAnswerSnapshots, competitors: competitorRows };
3908
+ }
3821
3909
  function buildAiSourceOrigin(snapshots, projectDomains, competitorDomains) {
3822
3910
  const categoryCounts = /* @__PURE__ */ new Map();
3823
3911
  const domainCounts = /* @__PURE__ */ new Map();
@@ -4262,6 +4350,13 @@ function buildProjectReport(db, projectName) {
4262
4350
  projectDomains,
4263
4351
  keywordLookup
4264
4352
  );
4353
+ const mentionLandscape = buildMentionLandscape(
4354
+ latestSnapshots,
4355
+ competitorDomains,
4356
+ project.displayName,
4357
+ projectDomains,
4358
+ keywordLookup
4359
+ );
4265
4360
  const aiSourceOrigin = buildAiSourceOrigin(latestSnapshots, projectDomains, competitorDomains);
4266
4361
  const trackedKeywords = [...keywordLookup.byId.values()];
4267
4362
  const gscSection = buildGscSection(
@@ -4354,6 +4449,7 @@ function buildProjectReport(db, projectName) {
4354
4449
  },
4355
4450
  citationScorecard,
4356
4451
  competitorLandscape,
4452
+ mentionLandscape,
4357
4453
  aiSourceOrigin,
4358
4454
  gsc: gscSection,
4359
4455
  ga: gaSection,
@@ -7132,7 +7228,7 @@ var routeCatalog = [
7132
7228
  path: "/api/v1/projects/{name}/report",
7133
7229
  summary: "Aggregated client-facing AEO report",
7134
7230
  tags: ["report"],
7135
- description: "Bundles every section the canonry-report HTML output needs (executive summary, citation scorecard, competitor landscape, AI source origin, GSC, GA4, social/AI referrals, indexing health, citations trend, insights, and recommended next steps) into a single JSON payload. Backs `canonry report <project>`.",
7231
+ description: "Bundles every section the canonry-report HTML output needs (executive summary, citation scorecard, competitor landscape \u2014 citation + mention landscapes, AI citation sources, GSC, GA4, social/AI referrals, indexing health, citations trend, insights, and recommended next steps) into a single JSON payload. Backs `canonry report <project>`.",
7136
7232
  parameters: [nameParameter],
7137
7233
  responses: {
7138
7234
  200: { description: "Report returned." },
@@ -10,7 +10,7 @@ import {
10
10
  projectUpsertRequestSchema,
11
11
  runTriggerRequestSchema,
12
12
  scheduleUpsertRequestSchema
13
- } from "./chunk-VIUWGDDU.js";
13
+ } from "./chunk-RDX6GBWM.js";
14
14
 
15
15
  // src/config.ts
16
16
  import fs from "fs";
@@ -1135,7 +1135,7 @@ var canonryMcpTools = [
1135
1135
  defineTool({
1136
1136
  name: "canonry_report",
1137
1137
  title: "Get aggregated AEO report",
1138
- description: "Returns the full client-facing AEO report bundle for a project \u2014 executive summary, per-keyword \xD7 per-provider citation matrix, competitor landscape, AI source origin, GSC/GA4 performance, social and AI referrals, indexing health, citations trend, prioritized insights, and recommended next steps. Same payload `canonry report <project>` consumes to render the self-contained HTML.",
1138
+ description: "Returns the full client-facing AEO report bundle for a project \u2014 executive summary, per-keyword \xD7 per-provider citation matrix, competitor landscape, AI citation sources, GSC/GA4 performance, social and AI referrals, indexing health, citations trend, prioritized insights, and recommended next steps. Same payload `canonry report <project>` consumes to render the self-contained HTML.",
1139
1139
  access: "read",
1140
1140
  tier: "monitoring",
1141
1141
  inputSchema: projectInputSchema,
@@ -1711,6 +1711,17 @@ function dropTrailingSlash(path) {
1711
1711
  }
1712
1712
  return path;
1713
1713
  }
1714
+ function absolutizeProjectUrl(url, canonicalDomain) {
1715
+ if (!url) return "";
1716
+ const trimmed = url.trim();
1717
+ if (!trimmed) return "";
1718
+ if (/^https?:\/\//i.test(trimmed)) return trimmed;
1719
+ if (trimmed.startsWith("//")) return `https:${trimmed}`;
1720
+ const host = canonicalDomain.trim().replace(/^https?:\/\//i, "").replace(/\/+$/, "");
1721
+ if (!host) return trimmed;
1722
+ if (trimmed.startsWith("/")) return `https://${host}${trimmed}`;
1723
+ return `https://${host}/${trimmed}`;
1724
+ }
1714
1725
  function normalizeUrlPath(input) {
1715
1726
  if (input == null) return null;
1716
1727
  let trimmed = input.trim();
@@ -1910,6 +1921,7 @@ export {
1910
1921
  CheckScopes,
1911
1922
  CheckCategories,
1912
1923
  summarizeCheckResults,
1924
+ absolutizeProjectUrl,
1913
1925
  normalizeUrlPath,
1914
1926
  emptyCitationVisibility,
1915
1927
  citationStateToCited,
@@ -2,7 +2,7 @@ import {
2
2
  ContentActions,
3
3
  RunKinds,
4
4
  __export
5
- } from "./chunk-VIUWGDDU.js";
5
+ } from "./chunk-RDX6GBWM.js";
6
6
 
7
7
  // src/intelligence-service.ts
8
8
  import { eq, desc, asc, and, or, inArray } from "drizzle-orm";
package/dist/cli.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  setGoogleAuthConfig,
19
19
  showFirstRunNotice,
20
20
  trackEvent
21
- } from "./chunk-R6YCYS2C.js";
21
+ } from "./chunk-CG4HEQAK.js";
22
22
  import {
23
23
  CliError,
24
24
  EXIT_SYSTEM_ERROR,
@@ -33,7 +33,7 @@ import {
33
33
  saveConfig,
34
34
  saveConfigPatch,
35
35
  usageError
36
- } from "./chunk-ZYESHCMF.js";
36
+ } from "./chunk-GLPZ5NVP.js";
37
37
  import {
38
38
  apiKeys,
39
39
  competitors,
@@ -45,7 +45,7 @@ import {
45
45
  projects,
46
46
  querySnapshots,
47
47
  runs
48
- } from "./chunk-M4KG7RJT.js";
48
+ } from "./chunk-W463NVVC.js";
49
49
  import {
50
50
  CcReleaseSyncStatuses,
51
51
  CheckScopes,
@@ -63,7 +63,7 @@ import {
63
63
  providerQuotaPolicySchema,
64
64
  resolveProviderInput,
65
65
  skillsClientSchema
66
- } from "./chunk-VIUWGDDU.js";
66
+ } from "./chunk-RDX6GBWM.js";
67
67
 
68
68
  // src/cli.ts
69
69
  import { pathToFileURL } from "url";
@@ -579,7 +579,7 @@ function readStoredGroundingSources(rawResponse) {
579
579
  return result;
580
580
  }
581
581
  async function backfillInsightsCommand(project, opts) {
582
- const { IntelligenceService } = await import("./intelligence-service-MY2EO4WD.js");
582
+ const { IntelligenceService } = await import("./intelligence-service-WZUM3AX6.js");
583
583
  const config = loadConfig();
584
584
  const db = createClient(config.database);
585
585
  migrate(db);
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-R6YCYS2C.js";
3
+ } from "./chunk-CG4HEQAK.js";
4
4
  import {
5
5
  loadConfig
6
- } from "./chunk-ZYESHCMF.js";
7
- import "./chunk-M4KG7RJT.js";
8
- import "./chunk-VIUWGDDU.js";
6
+ } from "./chunk-GLPZ5NVP.js";
7
+ import "./chunk-W463NVVC.js";
8
+ import "./chunk-RDX6GBWM.js";
9
9
  export {
10
10
  createServer,
11
11
  loadConfig
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-M4KG7RJT.js";
4
- import "./chunk-VIUWGDDU.js";
3
+ } from "./chunk-W463NVVC.js";
4
+ import "./chunk-RDX6GBWM.js";
5
5
  export {
6
6
  IntelligenceService
7
7
  };
package/dist/mcp.js CHANGED
@@ -2,8 +2,8 @@ import {
2
2
  CliError,
3
3
  canonryMcpTools,
4
4
  createApiClient
5
- } from "./chunk-ZYESHCMF.js";
6
- import "./chunk-VIUWGDDU.js";
5
+ } from "./chunk-GLPZ5NVP.js";
6
+ import "./chunk-RDX6GBWM.js";
7
7
 
8
8
  // src/mcp/cli.ts
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "3.5.1",
3
+ "version": "3.6.2",
4
4
  "type": "module",
5
5
  "description": "Agent-first open-source AEO operating platform - track how answer engines cite your domain",
6
6
  "license": "FSL-1.1-ALv2",
@@ -60,20 +60,20 @@
60
60
  "tsup": "^8.5.1",
61
61
  "tsx": "^4.19.0",
62
62
  "@ainyc/canonry-config": "0.0.0",
63
- "@ainyc/canonry-db": "0.0.0",
64
- "@ainyc/canonry-intelligence": "0.0.0",
65
63
  "@ainyc/canonry-api-routes": "0.0.0",
64
+ "@ainyc/canonry-db": "0.0.0",
66
65
  "@ainyc/canonry-contracts": "0.0.0",
66
+ "@ainyc/canonry-intelligence": "0.0.0",
67
+ "@ainyc/canonry-integration-bing": "0.0.0",
67
68
  "@ainyc/canonry-integration-commoncrawl": "0.0.0",
68
- "@ainyc/canonry-integration-wordpress": "0.0.0",
69
69
  "@ainyc/canonry-integration-google": "0.0.0",
70
- "@ainyc/canonry-integration-bing": "0.0.0",
70
+ "@ainyc/canonry-integration-wordpress": "0.0.0",
71
+ "@ainyc/canonry-provider-gemini": "0.0.0",
71
72
  "@ainyc/canonry-provider-cdp": "0.0.0",
72
- "@ainyc/canonry-provider-local": "0.0.0",
73
- "@ainyc/canonry-provider-perplexity": "0.0.0",
74
- "@ainyc/canonry-provider-openai": "0.0.0",
75
73
  "@ainyc/canonry-provider-claude": "0.0.0",
76
- "@ainyc/canonry-provider-gemini": "0.0.0"
74
+ "@ainyc/canonry-provider-openai": "0.0.0",
75
+ "@ainyc/canonry-provider-perplexity": "0.0.0",
76
+ "@ainyc/canonry-provider-local": "0.0.0"
77
77
  },
78
78
  "scripts": {
79
79
  "build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",