@canonry/canonry 4.125.0 → 4.127.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.
Files changed (25) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +5 -0
  2. package/assets/assets/{AuditHistoryPanel-C-KSU-dr.js → AuditHistoryPanel-DwJpVWme.js} +1 -1
  3. package/assets/assets/{BacklinksPage-BH4njwCh.js → BacklinksPage-K8FQUBxI.js} +1 -1
  4. package/assets/assets/{ChartPrimitives-BhB9Kxzc.js → ChartPrimitives-6oFKrJFk.js} +1 -1
  5. package/assets/assets/{HistoryPage-BQVNx0eu.js → HistoryPage-DwOAoN1Z.js} +1 -1
  6. package/assets/assets/{ProjectPage-DnkNE1AW.js → ProjectPage-CdgFUk7h.js} +1 -1
  7. package/assets/assets/{RunRow-70N3oGz8.js → RunRow-lt9IS99U.js} +1 -1
  8. package/assets/assets/{RunsPage-CtuKRP0o.js → RunsPage-DitYcIdx.js} +1 -1
  9. package/assets/assets/{SettingsPage-B1ZGnDz8.js → SettingsPage-CAOrh65Q.js} +1 -1
  10. package/assets/assets/{TrafficPage-BOGbXHd9.js → TrafficPage-BqzNvx75.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-5jj7n7W1.js → TrafficSourceDetailPage-DXbygZUA.js} +1 -1
  12. package/assets/assets/{arrow-left-gOf0lyu3.js → arrow-left-AmG4gtUR.js} +1 -1
  13. package/assets/assets/{extract-error-message-CZKcwJhv.js → extract-error-message-DlVtjoPz.js} +1 -1
  14. package/assets/assets/{index-B_CDPjOg.js → index-BiCDkOH4.js} +3 -3
  15. package/assets/assets/{trash-2-BnFgG3Wq.js → trash-2-gFHWd1A1.js} +1 -1
  16. package/assets/index.html +1 -1
  17. package/dist/{chunk-DN5H2ZN7.js → chunk-A4YBV4Y4.js} +35 -4
  18. package/dist/{chunk-RFLNTI7Y.js → chunk-BLSHHRRU.js} +21 -5
  19. package/dist/{chunk-YPRUBNUK.js → chunk-HQRUFTLN.js} +272 -61
  20. package/dist/{chunk-WA57WG6W.js → chunk-IHRW6WWV.js} +1 -1
  21. package/dist/cli.js +4 -4
  22. package/dist/index.js +4 -4
  23. package/dist/{intelligence-service-WJ2ZR7GF.js → intelligence-service-2WKS3EWJ.js} +2 -2
  24. package/dist/mcp.js +2 -2
  25. package/package.json +8 -8
@@ -336,7 +336,7 @@ import {
336
336
  wordpressSchemaDeployResultDtoSchema,
337
337
  wordpressSchemaStatusResultDtoSchema,
338
338
  wordpressStatusDtoSchema
339
- } from "./chunk-RFLNTI7Y.js";
339
+ } from "./chunk-BLSHHRRU.js";
340
340
 
341
341
  // src/intelligence-service.ts
342
342
  import { eq as eq41, desc as desc18, asc as asc9, and as and32, ne as ne6, or as or8, inArray as inArray15, gte as gte10, lte as lte6 } from "drizzle-orm";
@@ -396,6 +396,7 @@ __export(schema_exports, {
396
396
  googleConnections: () => googleConnections,
397
397
  gscCoverageSnapshots: () => gscCoverageSnapshots,
398
398
  gscDailyTotals: () => gscDailyTotals,
399
+ gscQueryDailyTotals: () => gscQueryDailyTotals,
399
400
  gscSearchData: () => gscSearchData,
400
401
  gscUrlInspections: () => gscUrlInspections,
401
402
  healthSnapshots: () => healthSnapshots,
@@ -656,6 +657,27 @@ var gscDailyTotals = sqliteTable("gsc_daily_totals", {
656
657
  uniqueIndex("idx_gsc_daily_totals_project_date").on(table.projectId, table.date),
657
658
  index("idx_gsc_daily_totals_project").on(table.projectId)
658
659
  ]);
660
+ var gscQueryDailyTotals = sqliteTable("gsc_query_daily_totals", {
661
+ id: text("id").primaryKey(),
662
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
663
+ date: text("date").notNull(),
664
+ query: text("query").notNull(),
665
+ clicks: integer("clicks").notNull().default(0),
666
+ impressions: integer("impressions").notNull().default(0),
667
+ /**
668
+ * Stored as a string like `gscSearchData.position` (parsed on read). CTR is
669
+ * derived (clicks / impressions) and intentionally not stored.
670
+ */
671
+ position: text("position").notNull().default("0"),
672
+ syncedAt: text("synced_at").notNull(),
673
+ syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "cascade" }),
674
+ createdAt: text("created_at").notNull()
675
+ }, (table) => [
676
+ uniqueIndex("idx_gsc_query_daily_totals_project_date_query").on(table.projectId, table.date, table.query),
677
+ index("idx_gsc_query_daily_totals_project_date").on(table.projectId, table.date),
678
+ index("idx_gsc_query_daily_totals_query").on(table.query),
679
+ index("idx_gsc_query_daily_totals_run").on(table.syncRunId)
680
+ ]);
659
681
  var gscUrlInspections = sqliteTable("gsc_url_inspections", {
660
682
  id: text("id").primaryKey(),
661
683
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
@@ -3971,6 +3993,34 @@ var MIGRATION_VERSIONS = [
3971
3993
  `CREATE INDEX IF NOT EXISTS idx_ga_daily_totals_project ON ga_daily_totals(project_id)`,
3972
3994
  `CREATE INDEX IF NOT EXISTS idx_ga_daily_totals_run ON ga_daily_totals(sync_run_id)`
3973
3995
  ]
3996
+ },
3997
+ {
3998
+ // Per-query daily totals fetched WITHOUT the `page` dimension. Summing
3999
+ // `gsc_search_data` by query multiplies impressions by how many of the
4000
+ // site's pages ranked on the same SERP — ~0% for single-page queries but
4001
+ // ~500% for brand+category terms, which reorders a top-queries table.
4002
+ // Dropping `page` makes Google do the dedup. Per-query counterpart to
4003
+ // `gsc_daily_totals`.
4004
+ version: 107,
4005
+ name: "gsc-query-daily-totals",
4006
+ statements: [
4007
+ `CREATE TABLE IF NOT EXISTS gsc_query_daily_totals (
4008
+ id TEXT PRIMARY KEY,
4009
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
4010
+ date TEXT NOT NULL,
4011
+ query TEXT NOT NULL,
4012
+ clicks INTEGER NOT NULL DEFAULT 0,
4013
+ impressions INTEGER NOT NULL DEFAULT 0,
4014
+ position TEXT NOT NULL DEFAULT '0',
4015
+ synced_at TEXT NOT NULL,
4016
+ sync_run_id TEXT REFERENCES runs(id) ON DELETE CASCADE,
4017
+ created_at TEXT NOT NULL
4018
+ )`,
4019
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_gsc_query_daily_totals_project_date_query ON gsc_query_daily_totals(project_id, date, query)`,
4020
+ `CREATE INDEX IF NOT EXISTS idx_gsc_query_daily_totals_project_date ON gsc_query_daily_totals(project_id, date)`,
4021
+ `CREATE INDEX IF NOT EXISTS idx_gsc_query_daily_totals_query ON gsc_query_daily_totals(query)`,
4022
+ `CREATE INDEX IF NOT EXISTS idx_gsc_query_daily_totals_run ON gsc_query_daily_totals(sync_run_id)`
4023
+ ]
3974
4024
  }
3975
4025
  ];
3976
4026
  function rebuildBacklinkTableWithSource(tx, table) {
@@ -9584,14 +9634,24 @@ function computeQueryChanges(projectQueries, cutoff) {
9584
9634
  label: `+${count2} kp`
9585
9635
  }));
9586
9636
  }
9637
+ function pooledRate(buckets, rateKey) {
9638
+ const countKey = rateKey === "citationRate" ? "cited" : "mentionedCount";
9639
+ let numerator = 0;
9640
+ let denominator = 0;
9641
+ for (const bucket of buckets) {
9642
+ numerator += bucket[countKey];
9643
+ denominator += bucket.total;
9644
+ }
9645
+ return denominator > 0 ? numerator / denominator : 0;
9646
+ }
9587
9647
  function computeTrend(buckets, rateKey) {
9588
9648
  const nonEmpty = buckets.filter((b) => b.total > 0);
9589
9649
  if (nonEmpty.length < 2) return "stable";
9590
9650
  const mid = Math.floor(nonEmpty.length / 2);
9591
9651
  const firstHalf = nonEmpty.slice(0, mid);
9592
9652
  const secondHalf = nonEmpty.slice(mid);
9593
- const avgFirst = firstHalf.reduce((s, b) => s + b[rateKey], 0) / firstHalf.length;
9594
- const avgSecond = secondHalf.reduce((s, b) => s + b[rateKey], 0) / secondHalf.length;
9653
+ const avgFirst = pooledRate(firstHalf, rateKey);
9654
+ const avgSecond = pooledRate(secondHalf, rateKey);
9595
9655
  const diff = avgSecond - avgFirst;
9596
9656
  if (diff > 0.05) return "improving";
9597
9657
  if (diff < -0.05) return "declining";
@@ -10048,7 +10108,12 @@ function buildGaTrafficByPage(db, projectId) {
10048
10108
  return map;
10049
10109
  }
10050
10110
  function sumAiReferralSessions(db, projectId) {
10051
- const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(eq13(gaAiReferrals.projectId, projectId)).all();
10111
+ const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(
10112
+ and9(
10113
+ eq13(gaAiReferrals.projectId, projectId),
10114
+ eq13(gaAiReferrals.sourceDimension, "session")
10115
+ )
10116
+ ).all();
10052
10117
  return rows.reduce((acc, r) => acc + (r.sessions ?? 0), 0);
10053
10118
  }
10054
10119
  function buildCandidateQueries(opts) {
@@ -10636,6 +10701,64 @@ function mergeGscDailyTotalsWithFallback(propertyTotals, dimensionedFallback) {
10636
10701
  for (const row of propertyTotals) byDate.set(row.date, row);
10637
10702
  return [...byDate.values()].sort((a, b) => a.date.localeCompare(b.date));
10638
10703
  }
10704
+ function readGscQueryDailyRows(db, projectId, startDate, endDate) {
10705
+ const rows = db.select({
10706
+ date: gscQueryDailyTotals.date,
10707
+ query: gscQueryDailyTotals.query,
10708
+ clicks: gscQueryDailyTotals.clicks,
10709
+ impressions: gscQueryDailyTotals.impressions,
10710
+ position: gscQueryDailyTotals.position
10711
+ }).from(gscQueryDailyTotals).where(
10712
+ and11(
10713
+ eq15(gscQueryDailyTotals.projectId, projectId),
10714
+ sql6`${gscQueryDailyTotals.date} >= ${startDate}`,
10715
+ sql6`${gscQueryDailyTotals.date} <= ${endDate}`
10716
+ )
10717
+ ).all();
10718
+ return rows.map((r) => {
10719
+ const position = Number(r.position);
10720
+ return {
10721
+ date: r.date,
10722
+ query: r.query,
10723
+ clicks: r.clicks,
10724
+ impressions: r.impressions,
10725
+ position: Number.isFinite(position) ? position : 0
10726
+ };
10727
+ });
10728
+ }
10729
+ function mergeGscQueryTotalsWithFallback(accurateDays, fallbackDays) {
10730
+ const dayKey = (r) => `${r.date}\0${r.query}`;
10731
+ const byDay = /* @__PURE__ */ new Map();
10732
+ for (const row of fallbackDays) byDay.set(dayKey(row), { row, accurate: false });
10733
+ for (const row of accurateDays) byDay.set(dayKey(row), { row, accurate: true });
10734
+ const byQuery = /* @__PURE__ */ new Map();
10735
+ for (const { row, accurate } of byDay.values()) {
10736
+ const acc = byQuery.get(row.query) ?? {
10737
+ clicks: 0,
10738
+ impressions: 0,
10739
+ weightedPositionSum: 0,
10740
+ positionSum: 0,
10741
+ positionDays: 0,
10742
+ sawAccurate: false,
10743
+ sawLegacy: false
10744
+ };
10745
+ acc.clicks += row.clicks;
10746
+ acc.impressions += row.impressions;
10747
+ acc.weightedPositionSum += row.position * row.impressions;
10748
+ acc.positionSum += row.position;
10749
+ acc.positionDays += 1;
10750
+ if (accurate) acc.sawAccurate = true;
10751
+ else acc.sawLegacy = true;
10752
+ byQuery.set(row.query, acc);
10753
+ }
10754
+ return [...byQuery.entries()].map(([query, acc]) => ({
10755
+ query,
10756
+ clicks: acc.clicks,
10757
+ impressions: acc.impressions,
10758
+ position: acc.impressions > 0 ? acc.weightedPositionSum / acc.impressions : acc.positionDays > 0 ? acc.positionSum / acc.positionDays : 0,
10759
+ source: acc.sawAccurate && acc.sawLegacy ? "mixed" : acc.sawAccurate ? "google" : "page-summed"
10760
+ }));
10761
+ }
10639
10762
 
10640
10763
  // ../api-routes/src/report-renderer.ts
10641
10764
  var COLORS = {
@@ -12235,7 +12358,6 @@ function renderAiReferrals(report) {
12235
12358
  <tr>
12236
12359
  <td class="page-cell">${formatLandingPageHtml(p.page)}</td>
12237
12360
  <td class="numeric">${formatNumber(p.sessions)}</td>
12238
- <td class="numeric">${formatNumber(p.users)}</td>
12239
12361
  </tr>`).join("");
12240
12362
  const trendChart = renderLineChart(
12241
12363
  ai.trend.map((t) => ({ x: t.date, y: t.sessions, label: t.date.slice(5) })),
@@ -12246,13 +12368,12 @@ function renderAiReferrals(report) {
12246
12368
  { id: "ai-referrals", eyebrow: "Section 9", title: "AI Referral Traffic", intro: "Traffic arriving from AI answer engines." },
12247
12369
  `<div class="metric-grid">
12248
12370
  <div class="metric"><div class="label">Total sessions</div><div class="value">${formatNumber(ai.totalSessions)}</div></div>
12249
- <div class="metric"><div class="label">Total users</div><div class="value">${formatNumber(ai.totalUsers)}</div></div>
12250
12371
  </div>
12251
12372
  ${trendChart}
12252
12373
  ${sourceBars}
12253
12374
  <div class="chart-card"><h3>Top AI landing pages</h3>
12254
12375
  <table class="report-table">
12255
- <thead><tr><th>Page</th><th class="numeric">Sessions</th><th class="numeric">Users</th></tr></thead>
12376
+ <thead><tr><th>Page</th><th class="numeric">Sessions</th></tr></thead>
12256
12377
  <tbody>${pageRows}</tbody>
12257
12378
  </table>
12258
12379
  </div>`
@@ -12969,15 +13090,16 @@ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trac
12969
13090
  const dailyTotals = allDailyTotals.filter((r) => r.date >= startDate && r.date <= maxDate);
12970
13091
  if (rows.length === 0 && dailyTotals.length === 0) return null;
12971
13092
  let dimensionedClicks = 0;
12972
- const queryAgg = /* @__PURE__ */ new Map();
13093
+ const queryDayAgg = /* @__PURE__ */ new Map();
12973
13094
  const dimensionedTrendAgg = /* @__PURE__ */ new Map();
12974
13095
  for (const r of rows) {
12975
13096
  dimensionedClicks += r.clicks;
12976
- const q = queryAgg.get(r.query) ?? { clicks: 0, impressions: 0, weightedPositionSum: 0 };
13097
+ const dayKey = `${r.date} ${r.query}`;
13098
+ const q = queryDayAgg.get(dayKey) ?? { date: r.date, query: r.query, clicks: 0, impressions: 0, weightedPositionSum: 0 };
12977
13099
  q.clicks += r.clicks;
12978
13100
  q.impressions += r.impressions;
12979
13101
  q.weightedPositionSum += safeNum(r.position) * r.impressions;
12980
- queryAgg.set(r.query, q);
13102
+ queryDayAgg.set(dayKey, q);
12981
13103
  const t = dimensionedTrendAgg.get(r.date) ?? { clicks: 0, impressions: 0, weightedPositionSum: 0 };
12982
13104
  t.clicks += r.clicks;
12983
13105
  t.impressions += r.impressions;
@@ -13004,17 +13126,27 @@ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trac
13004
13126
  const trend = dailySeries.map((d) => ({ date: d.date, clicks: d.clicks, impressions: d.impressions }));
13005
13127
  const ctr = totalImpressions > 0 ? totalClicks / totalImpressions : 0;
13006
13128
  const avgPosition = totalImpressions > 0 ? weightedPositionSum / totalImpressions : 0;
13007
- const topQueries = [...queryAgg.entries()].map(([query, agg]) => ({
13008
- query,
13129
+ const queryTotals = mergeGscQueryTotalsWithFallback(
13130
+ readGscQueryDailyRows(db, projectId, startDate, maxDate),
13131
+ [...queryDayAgg.values()].map((agg) => ({
13132
+ date: agg.date,
13133
+ query: agg.query,
13134
+ clicks: agg.clicks,
13135
+ impressions: agg.impressions,
13136
+ position: agg.impressions > 0 ? agg.weightedPositionSum / agg.impressions : 0
13137
+ }))
13138
+ );
13139
+ const topQueries = queryTotals.map((agg) => ({
13140
+ query: agg.query,
13009
13141
  clicks: agg.clicks,
13010
13142
  impressions: agg.impressions,
13011
13143
  ctr: agg.impressions > 0 ? agg.clicks / agg.impressions : 0,
13012
- avgPosition: agg.impressions > 0 ? agg.weightedPositionSum / agg.impressions : 0,
13013
- category: categorizeQuery(query, projectBrandNames, canonicalDomain)
13144
+ avgPosition: agg.position,
13145
+ category: categorizeQuery(agg.query, projectBrandNames, canonicalDomain)
13014
13146
  })).sort((a, b) => b.clicks - a.clicks).slice(0, TOP_QUERIES_LIMIT);
13015
13147
  const categoryAgg = /* @__PURE__ */ new Map();
13016
- for (const [query, agg] of queryAgg) {
13017
- const cat = categorizeQuery(query, projectBrandNames, canonicalDomain);
13148
+ for (const agg of queryTotals) {
13149
+ const cat = categorizeQuery(agg.query, projectBrandNames, canonicalDomain);
13018
13150
  const bucket = categoryAgg.get(cat) ?? { clicks: 0, impressions: 0 };
13019
13151
  bucket.clicks += agg.clicks;
13020
13152
  bucket.impressions += agg.impressions;
@@ -13029,9 +13161,9 @@ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trac
13029
13161
  const periodStart = trend[0]?.date ?? "";
13030
13162
  const periodEnd = trend.at(-1)?.date ?? "";
13031
13163
  const trackedSet = new Set(trackedQueries.map((q) => q.toLowerCase()));
13032
- const gscQuerySet = new Set([...queryAgg.keys()].map((q) => q.toLowerCase()));
13164
+ const gscQuerySet = new Set(queryTotals.map((q) => q.query.toLowerCase()));
13033
13165
  const trackedButNoGsc = trackedQueries.filter((q) => !gscQuerySet.has(q.toLowerCase())).sort();
13034
- const gscButNotTracked = [...queryAgg.entries()].filter(([q]) => !trackedSet.has(q.toLowerCase())).filter(([q]) => categorizeQuery(q, projectBrandNames, canonicalDomain) !== "brand").sort((a, b) => b[1].impressions - a[1].impressions).map(([q]) => q).slice(0, TOP_QUERIES_LIMIT);
13166
+ const gscButNotTracked = [...queryTotals].filter((agg) => !trackedSet.has(agg.query.toLowerCase())).filter((agg) => categorizeQuery(agg.query, projectBrandNames, canonicalDomain) !== "brand").sort((a, b) => b.impressions - a.impressions).map((agg) => agg.query).slice(0, TOP_QUERIES_LIMIT);
13035
13167
  return {
13036
13168
  periodStart,
13037
13169
  periodEnd,
@@ -13171,8 +13303,24 @@ function buildSocialReferrals(db, projectId) {
13171
13303
  topCampaigns
13172
13304
  };
13173
13305
  }
13174
- function buildAiReferrals(db, projectId) {
13175
- const rows = db.select().from(gaAiReferrals).where(eq16(gaAiReferrals.projectId, projectId)).all();
13306
+ function resolveAiReferralWindow(db, projectId, windowDays) {
13307
+ const windowKey = GA_WINDOW_SUMMARY_KEYS[windowDays];
13308
+ const windowSummary = windowKey ? db.select({ periodStart: gaTrafficWindowSummaries.periodStart, periodEnd: gaTrafficWindowSummaries.periodEnd }).from(gaTrafficWindowSummaries).where(and12(
13309
+ eq16(gaTrafficWindowSummaries.projectId, projectId),
13310
+ eq16(gaTrafficWindowSummaries.windowKey, windowKey)
13311
+ )).get() : void 0;
13312
+ if (windowSummary) return { start: windowSummary.periodStart, end: windowSummary.periodEnd };
13313
+ const latestSummary = db.select({ periodEnd: gaTrafficSummaries.periodEnd }).from(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, projectId)).orderBy(desc7(gaTrafficSummaries.syncedAt)).get();
13314
+ const end = latestSummary?.periodEnd ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
13315
+ return { start: windowStartDate(end, windowDays), end };
13316
+ }
13317
+ function buildAiReferrals(db, projectId, windowDays) {
13318
+ const window = resolveAiReferralWindow(db, projectId, windowDays);
13319
+ const rows = db.select().from(gaAiReferrals).where(and12(
13320
+ eq16(gaAiReferrals.projectId, projectId),
13321
+ sql7`${gaAiReferrals.date} >= ${window.start}`,
13322
+ sql7`${gaAiReferrals.date} <= ${window.end}`
13323
+ )).all();
13176
13324
  if (rows.length === 0) return null;
13177
13325
  const dimSessionsByTuple = /* @__PURE__ */ new Map();
13178
13326
  for (const r of rows) {
@@ -13200,60 +13348,47 @@ function buildAiReferrals(db, projectId) {
13200
13348
  (r) => winningDimension.get(`${r.date}::${r.source}::${r.medium}`) === r.sourceDimension
13201
13349
  );
13202
13350
  let total = 0;
13203
- let totalUsers = 0;
13204
13351
  let paidSessions = 0;
13205
- let paidUsers = 0;
13206
13352
  let organicSessions = 0;
13207
- let organicUsers = 0;
13208
13353
  const sourceAgg = /* @__PURE__ */ new Map();
13209
13354
  const trendAgg = /* @__PURE__ */ new Map();
13210
13355
  const pageAgg = /* @__PURE__ */ new Map();
13211
13356
  for (const r of dedupedRows) {
13212
13357
  total += r.sessions;
13213
- totalUsers += r.users;
13214
13358
  const paid = isPaidAiTrafficClass(r.trafficClass);
13215
13359
  if (paid) {
13216
13360
  paidSessions += r.sessions;
13217
- paidUsers += r.users;
13218
13361
  } else {
13219
13362
  organicSessions += r.sessions;
13220
- organicUsers += r.users;
13221
13363
  }
13222
13364
  const s = sourceAgg.get(r.source) ?? {
13223
13365
  sessions: 0,
13224
- users: 0,
13225
13366
  paidSessions: 0,
13226
13367
  organicSessions: 0
13227
13368
  };
13228
13369
  s.sessions += r.sessions;
13229
- s.users += r.users;
13230
13370
  if (paid) s.paidSessions += r.sessions;
13231
13371
  else s.organicSessions += r.sessions;
13232
13372
  sourceAgg.set(r.source, s);
13233
13373
  trendAgg.set(r.date, (trendAgg.get(r.date) ?? 0) + r.sessions);
13234
13374
  const page = r.landingPageNormalized ?? r.landingPage;
13235
- const p = pageAgg.get(page) ?? { sessions: 0, users: 0 };
13375
+ const p = pageAgg.get(page) ?? { sessions: 0 };
13236
13376
  p.sessions += r.sessions;
13237
- p.users += r.users;
13238
13377
  pageAgg.set(page, p);
13239
13378
  }
13240
13379
  const bySource = [...sourceAgg.entries()].map(([source, data]) => ({
13241
13380
  source,
13242
13381
  sessions: data.sessions,
13243
- users: data.users,
13244
13382
  paidSessions: data.paidSessions,
13245
13383
  organicSessions: data.organicSessions,
13246
13384
  sharePct: total > 0 ? Math.round(data.sessions / total * 100) : 0
13247
13385
  })).sort((a, b) => b.sessions - a.sessions);
13248
13386
  const trend = [...trendAgg.entries()].map(([date, sessions]) => ({ date, sessions })).sort((a, b) => a.date.localeCompare(b.date));
13249
- const topLandingPages = [...pageAgg.entries()].map(([page, data]) => ({ page, sessions: data.sessions, users: data.users })).sort((a, b) => b.sessions - a.sessions).slice(0, TOP_AI_REFERRAL_PAGES_LIMIT);
13387
+ const topLandingPages = [...pageAgg.entries()].map(([page, data]) => ({ page, sessions: data.sessions })).sort((a, b) => b.sessions - a.sessions).slice(0, TOP_AI_REFERRAL_PAGES_LIMIT);
13250
13388
  return {
13251
13389
  totalSessions: total,
13252
- totalUsers,
13253
13390
  paidSessions,
13254
- paidUsers,
13255
13391
  organicSessions,
13256
- organicUsers,
13257
13392
  bySource,
13258
13393
  trend,
13259
13394
  topLandingPages
@@ -14258,7 +14393,7 @@ function buildProjectReport(db, projectName, periodDays) {
14258
14393
  );
14259
14394
  const gaSection = buildGaSection(db, project.id, periodDays);
14260
14395
  const socialSection = buildSocialReferrals(db, project.id);
14261
- const aiReferralsSection = buildAiReferrals(db, project.id);
14396
+ const aiReferralsSection = buildAiReferrals(db, project.id, periodDays);
14262
14397
  const serverActivitySection = buildServerActivity(db, project.id, periodDays);
14263
14398
  const indexingHealthSection = buildIndexingHealth(db, project.id);
14264
14399
  const citationsTrend = buildCitationsTrend(db, project.id, queryLookup, latestRunLocation);
@@ -15754,7 +15889,8 @@ function summarizeTransitionsFromSnapshots(latest, previous, since) {
15754
15889
  }
15755
15890
  function buildSuggestedQueriesFromGsc(app, projectId, trackedQueries) {
15756
15891
  const cutoff = new Date(Date.now() - 28 * 24 * 60 * 60 * 1e3).toISOString().slice(0, 10);
15757
- const rows = app.db.select({
15892
+ const dimensionedRows = app.db.select({
15893
+ date: gscSearchData.date,
15758
15894
  query: gscSearchData.query,
15759
15895
  impressions: sql8`COALESCE(SUM(${gscSearchData.impressions}), 0)`,
15760
15896
  clicks: sql8`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
@@ -15766,12 +15902,23 @@ function buildSuggestedQueriesFromGsc(app, projectId, trackedQueries) {
15766
15902
  eq20(gscSearchData.projectId, projectId),
15767
15903
  sql8`${gscSearchData.date} >= ${cutoff}`,
15768
15904
  sql8`${gscSearchData.impressions} > 0`
15769
- )).groupBy(gscSearchData.query).orderBy(sql8`SUM(${gscSearchData.impressions}) DESC`).limit(100).all();
15770
- const gscRows = rows.map((r) => ({
15905
+ )).groupBy(gscSearchData.date, gscSearchData.query).all();
15906
+ const todayIso = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
15907
+ const merged = mergeGscQueryTotalsWithFallback(
15908
+ readGscQueryDailyRows(app.db, projectId, cutoff, todayIso),
15909
+ dimensionedRows.map((r) => ({
15910
+ date: r.date,
15911
+ query: r.query,
15912
+ impressions: Number(r.impressions),
15913
+ clicks: Number(r.clicks),
15914
+ position: Number(r.avgPosition)
15915
+ }))
15916
+ );
15917
+ const gscRows = merged.filter((r) => r.impressions > 0).sort((a, b) => b.impressions - a.impressions).slice(0, 100).map((r) => ({
15771
15918
  query: r.query,
15772
- impressions: Number(r.impressions),
15773
- clicks: Number(r.clicks),
15774
- avgPosition: Number(r.avgPosition)
15919
+ impressions: r.impressions,
15920
+ clicks: r.clicks,
15921
+ avgPosition: r.position
15775
15922
  }));
15776
15923
  return buildSuggestedQueries(gscRows, { trackedQueries });
15777
15924
  }
@@ -24545,6 +24692,7 @@ var AdsActivationError = class extends Error {
24545
24692
  entity;
24546
24693
  };
24547
24694
  var DEFAULT_LEASE_MS = 5 * 6e4;
24695
+ var DEFAULT_ACTIVATION_VERIFICATION_RETRY_DELAYS_MS = [100, 250, 500, 1e3, 2e3, 2e3];
24548
24696
  var PROVIDER_PAUSED = "paused";
24549
24697
  var PROVIDER_ACTIVE = "active";
24550
24698
  function assertNever(value) {
@@ -25479,6 +25627,66 @@ async function validateActiveDescendants(ctx, parent) {
25479
25627
  );
25480
25628
  }
25481
25629
  }
25630
+ async function verifyActiveAfterMutation(ctx, target) {
25631
+ let lastUpdatedAt = null;
25632
+ for (let attempt = 0; ; attempt++) {
25633
+ let entity = null;
25634
+ try {
25635
+ entity = await readTarget(ctx.deps.provider, target);
25636
+ lastUpdatedAt = entity.updatedAt;
25637
+ } catch {
25638
+ }
25639
+ if (entity) {
25640
+ try {
25641
+ validateIdentity(target, entity);
25642
+ validateAdApproval(target, entity);
25643
+ } catch (cause) {
25644
+ return {
25645
+ ok: false,
25646
+ error: cause instanceof AdsActivationError ? cause : targetError(
25647
+ AdsActivationErrorCodes.providerMutationFailed,
25648
+ "Provider activation validation failed",
25649
+ 502,
25650
+ target
25651
+ ),
25652
+ providerUpdatedAt: entity.updatedAt
25653
+ };
25654
+ }
25655
+ if (entity.status === PROVIDER_ACTIVE && entity.updatedAt !== null) {
25656
+ return { ok: true, entity };
25657
+ }
25658
+ const exactPreMutationSnapshot = entity.status === PROVIDER_PAUSED && entity.updatedAt === target.expectedUpdatedAt;
25659
+ if (!exactPreMutationSnapshot) {
25660
+ return {
25661
+ ok: false,
25662
+ error: targetError(
25663
+ AdsActivationErrorCodes.providerMutationFailed,
25664
+ "Provider did not confirm the required active state",
25665
+ 502,
25666
+ target
25667
+ ),
25668
+ providerUpdatedAt: entity.updatedAt
25669
+ };
25670
+ }
25671
+ }
25672
+ if (attempt >= ctx.deps.activationVerificationRetryDelaysMs.length) {
25673
+ return {
25674
+ ok: false,
25675
+ error: targetError(
25676
+ AdsActivationErrorCodes.providerMutationFailed,
25677
+ "Provider activation outcome could not be verified",
25678
+ 502,
25679
+ target
25680
+ ),
25681
+ providerUpdatedAt: lastUpdatedAt
25682
+ };
25683
+ }
25684
+ const delayMs = ctx.deps.activationVerificationRetryDelaysMs[attempt];
25685
+ await renewLease(ctx);
25686
+ await ctx.deps.sleep(delayMs);
25687
+ await renewLease(ctx);
25688
+ }
25689
+ }
25482
25690
  async function activateOne(ctx, original) {
25483
25691
  let step = currentStep(ctx, original.id);
25484
25692
  if (step.state === AdsOperationStepStates.active) {
@@ -25530,25 +25738,16 @@ async function activateOne(ctx, original) {
25530
25738
  } catch {
25531
25739
  }
25532
25740
  await renewLease(ctx);
25533
- let entity;
25534
- try {
25535
- entity = await readTarget(ctx.deps.provider, target);
25536
- } catch {
25537
- const error = targetError(AdsActivationErrorCodes.providerMutationFailed, "Provider activation outcome could not be verified", 502, target);
25538
- await transitionStep(ctx, step, unknownStep(step, error, null, ctx.deps.now().toISOString()));
25539
- throw error;
25540
- }
25541
- try {
25542
- validateIdentity(target, entity);
25543
- validateAdApproval(target, entity);
25544
- if (entity.status !== PROVIDER_ACTIVE || entity.updatedAt === null) {
25545
- throw targetError(AdsActivationErrorCodes.providerMutationFailed, "Provider did not confirm the required active state", 502, target);
25546
- }
25547
- } catch (cause) {
25548
- const error = cause instanceof AdsActivationError ? cause : targetError(AdsActivationErrorCodes.providerMutationFailed, "Provider activation validation failed", 502, target);
25549
- await transitionStep(ctx, step, unknownStep(step, error, entity.updatedAt, ctx.deps.now().toISOString()));
25550
- throw error;
25741
+ const verification = await verifyActiveAfterMutation(ctx, target);
25742
+ if (!verification.ok) {
25743
+ await transitionStep(
25744
+ ctx,
25745
+ step,
25746
+ unknownStep(step, verification.error, verification.providerUpdatedAt, ctx.deps.now().toISOString())
25747
+ );
25748
+ throw verification.error;
25551
25749
  }
25750
+ const { entity } = verification;
25552
25751
  await validateExactDescendants(
25553
25752
  ctx.deps.provider,
25554
25753
  ctx.manifest,
@@ -25732,11 +25931,22 @@ async function executeApprovedAdsActivation(dependencies, request) {
25732
25931
  provider: dependencies.provider,
25733
25932
  now: dependencies.now ?? (() => /* @__PURE__ */ new Date()),
25734
25933
  randomId: dependencies.randomId ?? (() => crypto20.randomUUID()),
25735
- leaseMs: dependencies.leaseMs ?? DEFAULT_LEASE_MS
25934
+ leaseMs: dependencies.leaseMs ?? DEFAULT_LEASE_MS,
25935
+ sleep: dependencies.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
25936
+ activationVerificationRetryDelaysMs: dependencies.activationVerificationRetryDelaysMs ?? DEFAULT_ACTIVATION_VERIFICATION_RETRY_DELAYS_MS
25736
25937
  };
25737
25938
  if (!Number.isSafeInteger(deps.leaseMs) || deps.leaseMs <= 0) {
25738
25939
  throw new AdsActivationError(AdsActivationErrorCodes.invalidManifest, "Activation lease duration is invalid", 400);
25739
25940
  }
25941
+ if (deps.activationVerificationRetryDelaysMs.some(
25942
+ (delay) => !Number.isSafeInteger(delay) || delay < 0
25943
+ )) {
25944
+ throw new AdsActivationError(
25945
+ AdsActivationErrorCodes.invalidManifest,
25946
+ "Activation verification retry delays are invalid",
25947
+ 400
25948
+ );
25949
+ }
25740
25950
  const now = deps.now();
25741
25951
  const operationId = deps.randomId();
25742
25952
  const leaseOwner = deps.randomId();
@@ -42265,6 +42475,7 @@ export {
42265
42475
  notifications,
42266
42476
  gscSearchData,
42267
42477
  gscDailyTotals,
42478
+ gscQueryDailyTotals,
42268
42479
  gscUrlInspections,
42269
42480
  gscCoverageSnapshots,
42270
42481
  siteAuditSnapshots,
@@ -37,7 +37,7 @@ import {
37
37
  trafficConnectVercelRequestSchema,
38
38
  trafficConnectWordpressRequestSchema,
39
39
  trafficEventKindSchema
40
- } from "./chunk-RFLNTI7Y.js";
40
+ } from "./chunk-BLSHHRRU.js";
41
41
 
42
42
  // src/config.ts
43
43
  import fs from "fs";
package/dist/cli.js CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  setTelemetrySource,
30
30
  showFirstRunNotice,
31
31
  trackEvent
32
- } from "./chunk-DN5H2ZN7.js";
32
+ } from "./chunk-A4YBV4Y4.js";
33
33
  import {
34
34
  CliError,
35
35
  EXIT_SYSTEM_ERROR,
@@ -46,7 +46,7 @@ import {
46
46
  saveConfig,
47
47
  saveConfigPatch,
48
48
  usageError
49
- } from "./chunk-WA57WG6W.js";
49
+ } from "./chunk-IHRW6WWV.js";
50
50
  import {
51
51
  apiKeys,
52
52
  createClient,
@@ -54,7 +54,7 @@ import {
54
54
  projects,
55
55
  queries,
56
56
  renderReportHtml
57
- } from "./chunk-YPRUBNUK.js";
57
+ } from "./chunk-HQRUFTLN.js";
58
58
  import {
59
59
  AdsOperationKinds,
60
60
  AdsOperationStates,
@@ -95,7 +95,7 @@ import {
95
95
  providerQuotaPolicySchema,
96
96
  resolveProviderInput,
97
97
  winnabilityClassSchema
98
- } from "./chunk-RFLNTI7Y.js";
98
+ } from "./chunk-BLSHHRRU.js";
99
99
 
100
100
  // src/cli.ts
101
101
  import { pathToFileURL } from "url";
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-DN5H2ZN7.js";
3
+ } from "./chunk-A4YBV4Y4.js";
4
4
  import {
5
5
  loadConfig
6
- } from "./chunk-WA57WG6W.js";
7
- import "./chunk-YPRUBNUK.js";
8
- import "./chunk-RFLNTI7Y.js";
6
+ } from "./chunk-IHRW6WWV.js";
7
+ import "./chunk-HQRUFTLN.js";
8
+ import "./chunk-BLSHHRRU.js";
9
9
  export {
10
10
  createServer,
11
11
  loadConfig
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-YPRUBNUK.js";
4
- import "./chunk-RFLNTI7Y.js";
3
+ } from "./chunk-HQRUFTLN.js";
4
+ import "./chunk-BLSHHRRU.js";
5
5
  export {
6
6
  IntelligenceService
7
7
  };
package/dist/mcp.js CHANGED
@@ -3,10 +3,10 @@ import {
3
3
  PACKAGE_VERSION,
4
4
  canonryMcpTools,
5
5
  createApiClient
6
- } from "./chunk-WA57WG6W.js";
6
+ } from "./chunk-IHRW6WWV.js";
7
7
  import {
8
8
  isReadOnlyKey
9
- } from "./chunk-RFLNTI7Y.js";
9
+ } from "./chunk-BLSHHRRU.js";
10
10
 
11
11
  // src/mcp/cli.ts
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonry/canonry",
3
- "version": "4.125.0",
3
+ "version": "4.127.0",
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",
@@ -71,21 +71,21 @@
71
71
  "@ainyc/canonry-contracts": "0.0.0",
72
72
  "@ainyc/canonry-db": "0.0.0",
73
73
  "@ainyc/canonry-integration-bing": "0.0.0",
74
- "@ainyc/canonry-integration-cloud-run": "0.0.0",
75
- "@ainyc/canonry-integration-google": "0.0.0",
76
74
  "@ainyc/canonry-integration-openai-ads": "0.0.0",
75
+ "@ainyc/canonry-integration-cloud-run": "0.0.0",
77
76
  "@ainyc/canonry-integration-commoncrawl": "0.0.0",
78
- "@ainyc/canonry-integration-google-business-profile": "0.0.0",
77
+ "@ainyc/canonry-integration-google": "0.0.0",
79
78
  "@ainyc/canonry-integration-google-places": "0.0.0",
80
79
  "@ainyc/canonry-intelligence": "0.0.0",
80
+ "@ainyc/canonry-integration-traffic": "0.0.0",
81
+ "@ainyc/canonry-integration-wordpress": "0.0.0",
81
82
  "@ainyc/canonry-provider-cdp": "0.0.0",
82
83
  "@ainyc/canonry-provider-claude": "0.0.0",
83
- "@ainyc/canonry-provider-gemini": "0.0.0",
84
- "@ainyc/canonry-integration-wordpress": "0.0.0",
85
- "@ainyc/canonry-integration-traffic": "0.0.0",
84
+ "@ainyc/canonry-integration-google-business-profile": "0.0.0",
86
85
  "@ainyc/canonry-provider-local": "0.0.0",
87
86
  "@ainyc/canonry-provider-openai": "0.0.0",
88
- "@ainyc/canonry-provider-perplexity": "0.0.0"
87
+ "@ainyc/canonry-provider-perplexity": "0.0.0",
88
+ "@ainyc/canonry-provider-gemini": "0.0.0"
89
89
  },
90
90
  "scripts": {
91
91
  "build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",