@canonry/canonry 4.124.0 → 4.126.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-fPBADGsr.js → AuditHistoryPanel-C-KSU-dr.js} +1 -1
  3. package/assets/assets/{BacklinksPage-DKer7s8M.js → BacklinksPage-BH4njwCh.js} +1 -1
  4. package/assets/assets/{ChartPrimitives-0DFDFhCZ.js → ChartPrimitives-BhB9Kxzc.js} +1 -1
  5. package/assets/assets/{HistoryPage-BTkTlXg0.js → HistoryPage-BQVNx0eu.js} +1 -1
  6. package/assets/assets/{ProjectPage-BzqDCkp2.js → ProjectPage-DnkNE1AW.js} +1 -1
  7. package/assets/assets/{RunRow-DyIeH4_d.js → RunRow-70N3oGz8.js} +1 -1
  8. package/assets/assets/{RunsPage-KVu_37ds.js → RunsPage-CtuKRP0o.js} +1 -1
  9. package/assets/assets/{SettingsPage-CCLXSBpq.js → SettingsPage-B1ZGnDz8.js} +1 -1
  10. package/assets/assets/{TrafficPage-BH7a0sS2.js → TrafficPage-BOGbXHd9.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-kIlf0kCe.js → TrafficSourceDetailPage-5jj7n7W1.js} +1 -1
  12. package/assets/assets/{arrow-left-jhivZs4g.js → arrow-left-gOf0lyu3.js} +1 -1
  13. package/assets/assets/{extract-error-message-XU_cbcLD.js → extract-error-message-CZKcwJhv.js} +1 -1
  14. package/assets/assets/{index-CqFSTLK5.js → index-B_CDPjOg.js} +59 -59
  15. package/assets/assets/{trash-2-CcmHLt5u.js → trash-2-BnFgG3Wq.js} +1 -1
  16. package/assets/index.html +1 -1
  17. package/dist/{chunk-BGLBWFMV.js → chunk-FCGRCCX6.js} +304 -23
  18. package/dist/{chunk-4D2XUMMV.js → chunk-M6GKSLOJ.js} +73 -7
  19. package/dist/{chunk-SLCGTOKT.js → chunk-RFLNTI7Y.js} +11 -1
  20. package/dist/{chunk-WGGB7QBD.js → chunk-WA57WG6W.js} +1 -1
  21. package/dist/cli.js +4 -4
  22. package/dist/index.js +4 -4
  23. package/dist/{intelligence-service-SACHNNCT.js → intelligence-service-K6B3MTDS.js} +2 -2
  24. package/dist/mcp.js +2 -2
  25. package/package.json +8 -8
@@ -1 +1 @@
1
- import{c}from"./index-CqFSTLK5.js";const a=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],h=c("circle-check",a);const e=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],n=c("circle-question-mark",e);const o=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],s=c("download",o);const t=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],r=c("trash-2",t);export{h as C,s as D,r as T,n as a};
1
+ import{c}from"./index-B_CDPjOg.js";const a=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],h=c("circle-check",a);const e=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],n=c("circle-question-mark",e);const o=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],s=c("download",o);const t=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],r=c("trash-2",t);export{h as C,s as D,r as T,n as a};
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-CqFSTLK5.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-B_CDPjOg.js"></script>
16
16
  <link rel="modulepreload" crossorigin href="./assets/vendor-tanstack-Be8JdV5M.js">
17
17
  <link rel="modulepreload" crossorigin href="./assets/vendor-radix-CQ77EO2y.js">
18
18
  <link rel="modulepreload" crossorigin href="./assets/vendor-recharts-DSxTZhaH.js">
@@ -7,6 +7,7 @@ import {
7
7
  AI_PROVIDER_INFRA_DOMAINS,
8
8
  AdsActivationEntityTypes,
9
9
  AdsActivationGrantStates,
10
+ AdsActivationVersionPolicies,
10
11
  AdsAdGroupBillingEventTypes,
11
12
  AdsCampaignBiddingTypes,
12
13
  AdsEntityStatuses,
@@ -335,7 +336,7 @@ import {
335
336
  wordpressSchemaDeployResultDtoSchema,
336
337
  wordpressSchemaStatusResultDtoSchema,
337
338
  wordpressStatusDtoSchema
338
- } from "./chunk-SLCGTOKT.js";
339
+ } from "./chunk-RFLNTI7Y.js";
339
340
 
340
341
  // src/intelligence-service.ts
341
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";
@@ -395,6 +396,7 @@ __export(schema_exports, {
395
396
  googleConnections: () => googleConnections,
396
397
  gscCoverageSnapshots: () => gscCoverageSnapshots,
397
398
  gscDailyTotals: () => gscDailyTotals,
399
+ gscQueryDailyTotals: () => gscQueryDailyTotals,
398
400
  gscSearchData: () => gscSearchData,
399
401
  gscUrlInspections: () => gscUrlInspections,
400
402
  healthSnapshots: () => healthSnapshots,
@@ -655,6 +657,27 @@ var gscDailyTotals = sqliteTable("gsc_daily_totals", {
655
657
  uniqueIndex("idx_gsc_daily_totals_project_date").on(table.projectId, table.date),
656
658
  index("idx_gsc_daily_totals_project").on(table.projectId)
657
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
+ ]);
658
681
  var gscUrlInspections = sqliteTable("gsc_url_inspections", {
659
682
  id: text("id").primaryKey(),
660
683
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
@@ -3970,6 +3993,34 @@ var MIGRATION_VERSIONS = [
3970
3993
  `CREATE INDEX IF NOT EXISTS idx_ga_daily_totals_project ON ga_daily_totals(project_id)`,
3971
3994
  `CREATE INDEX IF NOT EXISTS idx_ga_daily_totals_run ON ga_daily_totals(sync_run_id)`
3972
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
+ ]
3973
4024
  }
3974
4025
  ];
3975
4026
  function rebuildBacklinkTableWithSource(tx, table) {
@@ -9583,14 +9634,24 @@ function computeQueryChanges(projectQueries, cutoff) {
9583
9634
  label: `+${count2} kp`
9584
9635
  }));
9585
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
+ }
9586
9647
  function computeTrend(buckets, rateKey) {
9587
9648
  const nonEmpty = buckets.filter((b) => b.total > 0);
9588
9649
  if (nonEmpty.length < 2) return "stable";
9589
9650
  const mid = Math.floor(nonEmpty.length / 2);
9590
9651
  const firstHalf = nonEmpty.slice(0, mid);
9591
9652
  const secondHalf = nonEmpty.slice(mid);
9592
- const avgFirst = firstHalf.reduce((s, b) => s + b[rateKey], 0) / firstHalf.length;
9593
- const avgSecond = secondHalf.reduce((s, b) => s + b[rateKey], 0) / secondHalf.length;
9653
+ const avgFirst = pooledRate(firstHalf, rateKey);
9654
+ const avgSecond = pooledRate(secondHalf, rateKey);
9594
9655
  const diff = avgSecond - avgFirst;
9595
9656
  if (diff > 0.05) return "improving";
9596
9657
  if (diff < -0.05) return "declining";
@@ -10047,7 +10108,12 @@ function buildGaTrafficByPage(db, projectId) {
10047
10108
  return map;
10048
10109
  }
10049
10110
  function sumAiReferralSessions(db, projectId) {
10050
- 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();
10051
10117
  return rows.reduce((acc, r) => acc + (r.sessions ?? 0), 0);
10052
10118
  }
10053
10119
  function buildCandidateQueries(opts) {
@@ -10635,6 +10701,64 @@ function mergeGscDailyTotalsWithFallback(propertyTotals, dimensionedFallback) {
10635
10701
  for (const row of propertyTotals) byDate.set(row.date, row);
10636
10702
  return [...byDate.values()].sort((a, b) => a.date.localeCompare(b.date));
10637
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
+ }
10638
10762
 
10639
10763
  // ../api-routes/src/report-renderer.ts
10640
10764
  var COLORS = {
@@ -12968,15 +13092,16 @@ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trac
12968
13092
  const dailyTotals = allDailyTotals.filter((r) => r.date >= startDate && r.date <= maxDate);
12969
13093
  if (rows.length === 0 && dailyTotals.length === 0) return null;
12970
13094
  let dimensionedClicks = 0;
12971
- const queryAgg = /* @__PURE__ */ new Map();
13095
+ const queryDayAgg = /* @__PURE__ */ new Map();
12972
13096
  const dimensionedTrendAgg = /* @__PURE__ */ new Map();
12973
13097
  for (const r of rows) {
12974
13098
  dimensionedClicks += r.clicks;
12975
- const q = queryAgg.get(r.query) ?? { clicks: 0, impressions: 0, weightedPositionSum: 0 };
13099
+ const dayKey = `${r.date} ${r.query}`;
13100
+ const q = queryDayAgg.get(dayKey) ?? { date: r.date, query: r.query, clicks: 0, impressions: 0, weightedPositionSum: 0 };
12976
13101
  q.clicks += r.clicks;
12977
13102
  q.impressions += r.impressions;
12978
13103
  q.weightedPositionSum += safeNum(r.position) * r.impressions;
12979
- queryAgg.set(r.query, q);
13104
+ queryDayAgg.set(dayKey, q);
12980
13105
  const t = dimensionedTrendAgg.get(r.date) ?? { clicks: 0, impressions: 0, weightedPositionSum: 0 };
12981
13106
  t.clicks += r.clicks;
12982
13107
  t.impressions += r.impressions;
@@ -13003,17 +13128,27 @@ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trac
13003
13128
  const trend = dailySeries.map((d) => ({ date: d.date, clicks: d.clicks, impressions: d.impressions }));
13004
13129
  const ctr = totalImpressions > 0 ? totalClicks / totalImpressions : 0;
13005
13130
  const avgPosition = totalImpressions > 0 ? weightedPositionSum / totalImpressions : 0;
13006
- const topQueries = [...queryAgg.entries()].map(([query, agg]) => ({
13007
- query,
13131
+ const queryTotals = mergeGscQueryTotalsWithFallback(
13132
+ readGscQueryDailyRows(db, projectId, startDate, maxDate),
13133
+ [...queryDayAgg.values()].map((agg) => ({
13134
+ date: agg.date,
13135
+ query: agg.query,
13136
+ clicks: agg.clicks,
13137
+ impressions: agg.impressions,
13138
+ position: agg.impressions > 0 ? agg.weightedPositionSum / agg.impressions : 0
13139
+ }))
13140
+ );
13141
+ const topQueries = queryTotals.map((agg) => ({
13142
+ query: agg.query,
13008
13143
  clicks: agg.clicks,
13009
13144
  impressions: agg.impressions,
13010
13145
  ctr: agg.impressions > 0 ? agg.clicks / agg.impressions : 0,
13011
- avgPosition: agg.impressions > 0 ? agg.weightedPositionSum / agg.impressions : 0,
13012
- category: categorizeQuery(query, projectBrandNames, canonicalDomain)
13146
+ avgPosition: agg.position,
13147
+ category: categorizeQuery(agg.query, projectBrandNames, canonicalDomain)
13013
13148
  })).sort((a, b) => b.clicks - a.clicks).slice(0, TOP_QUERIES_LIMIT);
13014
13149
  const categoryAgg = /* @__PURE__ */ new Map();
13015
- for (const [query, agg] of queryAgg) {
13016
- const cat = categorizeQuery(query, projectBrandNames, canonicalDomain);
13150
+ for (const agg of queryTotals) {
13151
+ const cat = categorizeQuery(agg.query, projectBrandNames, canonicalDomain);
13017
13152
  const bucket = categoryAgg.get(cat) ?? { clicks: 0, impressions: 0 };
13018
13153
  bucket.clicks += agg.clicks;
13019
13154
  bucket.impressions += agg.impressions;
@@ -13028,9 +13163,9 @@ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trac
13028
13163
  const periodStart = trend[0]?.date ?? "";
13029
13164
  const periodEnd = trend.at(-1)?.date ?? "";
13030
13165
  const trackedSet = new Set(trackedQueries.map((q) => q.toLowerCase()));
13031
- const gscQuerySet = new Set([...queryAgg.keys()].map((q) => q.toLowerCase()));
13166
+ const gscQuerySet = new Set(queryTotals.map((q) => q.query.toLowerCase()));
13032
13167
  const trackedButNoGsc = trackedQueries.filter((q) => !gscQuerySet.has(q.toLowerCase())).sort();
13033
- 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);
13168
+ 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);
13034
13169
  return {
13035
13170
  periodStart,
13036
13171
  periodEnd,
@@ -15753,7 +15888,8 @@ function summarizeTransitionsFromSnapshots(latest, previous, since) {
15753
15888
  }
15754
15889
  function buildSuggestedQueriesFromGsc(app, projectId, trackedQueries) {
15755
15890
  const cutoff = new Date(Date.now() - 28 * 24 * 60 * 60 * 1e3).toISOString().slice(0, 10);
15756
- const rows = app.db.select({
15891
+ const dimensionedRows = app.db.select({
15892
+ date: gscSearchData.date,
15757
15893
  query: gscSearchData.query,
15758
15894
  impressions: sql8`COALESCE(SUM(${gscSearchData.impressions}), 0)`,
15759
15895
  clicks: sql8`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
@@ -15765,12 +15901,23 @@ function buildSuggestedQueriesFromGsc(app, projectId, trackedQueries) {
15765
15901
  eq20(gscSearchData.projectId, projectId),
15766
15902
  sql8`${gscSearchData.date} >= ${cutoff}`,
15767
15903
  sql8`${gscSearchData.impressions} > 0`
15768
- )).groupBy(gscSearchData.query).orderBy(sql8`SUM(${gscSearchData.impressions}) DESC`).limit(100).all();
15769
- const gscRows = rows.map((r) => ({
15904
+ )).groupBy(gscSearchData.date, gscSearchData.query).all();
15905
+ const todayIso = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
15906
+ const merged = mergeGscQueryTotalsWithFallback(
15907
+ readGscQueryDailyRows(app.db, projectId, cutoff, todayIso),
15908
+ dimensionedRows.map((r) => ({
15909
+ date: r.date,
15910
+ query: r.query,
15911
+ impressions: Number(r.impressions),
15912
+ clicks: Number(r.clicks),
15913
+ position: Number(r.avgPosition)
15914
+ }))
15915
+ );
15916
+ const gscRows = merged.filter((r) => r.impressions > 0).sort((a, b) => b.impressions - a.impressions).slice(0, 100).map((r) => ({
15770
15917
  query: r.query,
15771
- impressions: Number(r.impressions),
15772
- clicks: Number(r.clicks),
15773
- avgPosition: Number(r.avgPosition)
15918
+ impressions: r.impressions,
15919
+ clicks: r.clicks,
15920
+ avgPosition: r.position
15774
15921
  }));
15775
15922
  return buildSuggestedQueries(gscRows, { trackedQueries });
15776
15923
  }
@@ -18623,7 +18770,13 @@ var routeCatalog = [
18623
18770
  properties: {
18624
18771
  manifest: adsActivationManifestSchema2,
18625
18772
  executorApiKeyId: adsEntityIdSchema,
18626
- expiresAt: { type: "string", format: "date-time" }
18773
+ expiresAt: { type: "string", format: "date-time" },
18774
+ versionPolicy: {
18775
+ type: "string",
18776
+ enum: ["exact", "refresh_semantically_unchanged"],
18777
+ default: "exact",
18778
+ description: "Exact rejects any provider version drift. refresh_semantically_unchanged rebinds versions only when the live paused tree still matches its durable create receipts field-for-field."
18779
+ }
18627
18780
  }
18628
18781
  }
18629
18782
  }
@@ -24948,6 +25101,81 @@ async function preflightAdsActivationApproval(provider, request) {
24948
25101
  }
24949
25102
  return { manifest, manifestHash: hashAdsActivationManifest(manifest) };
24950
25103
  }
25104
+ async function refreshSemanticallyUnchangedAdsActivationManifest(provider, request) {
25105
+ const manifest = canonicalManifest(request.manifest);
25106
+ await validateAccount(provider, request.adAccountId);
25107
+ await validateExactDescendants(provider, manifest, {
25108
+ entityType: AdsActivationEntityTypes.campaign,
25109
+ entityId: manifest.campaign.id,
25110
+ parentId: null,
25111
+ expectedUpdatedAt: manifest.campaign.expectedUpdatedAt
25112
+ });
25113
+ const versions = /* @__PURE__ */ new Map();
25114
+ for (const target of activationTargets(manifest)) {
25115
+ let entity;
25116
+ try {
25117
+ entity = await readTarget(provider, target);
25118
+ } catch {
25119
+ throw targetError(
25120
+ AdsActivationErrorCodes.providerReadFailed,
25121
+ "An ads entity could not be verified for version refresh",
25122
+ 502,
25123
+ target
25124
+ );
25125
+ }
25126
+ validateIdentity(target, entity);
25127
+ validateAdApproval(target, entity);
25128
+ if (entity.status !== PROVIDER_PAUSED) {
25129
+ throw targetError(AdsActivationErrorCodes.entityNotPaused, "Every approved ads entity must still be paused", 409, target);
25130
+ }
25131
+ if (!Number.isInteger(entity.updatedAt) || entity.updatedAt === null || entity.updatedAt < 0) {
25132
+ throw targetError(
25133
+ AdsActivationErrorCodes.providerReadFailed,
25134
+ "An ads entity did not report a usable provider version",
25135
+ 502,
25136
+ target
25137
+ );
25138
+ }
25139
+ if (!request.semanticallyMatchesMaterialization(target.entityType, target.entityId, entity)) {
25140
+ throw targetError(
25141
+ AdsActivationErrorCodes.entityMismatch,
25142
+ "An ads entity no longer matches its materialized approved fields",
25143
+ 409,
25144
+ target
25145
+ );
25146
+ }
25147
+ versions.set(`${target.entityType}:${target.entityId}`, entity.updatedAt);
25148
+ }
25149
+ const version = (entityType, entityId) => {
25150
+ const current = versions.get(`${entityType}:${entityId}`);
25151
+ if (current === void 0) {
25152
+ throw new AdsActivationError(
25153
+ AdsActivationErrorCodes.persistenceFailed,
25154
+ "A refreshed ads entity version is missing",
25155
+ 500
25156
+ );
25157
+ }
25158
+ return current;
25159
+ };
25160
+ const refreshed = {
25161
+ campaign: {
25162
+ id: manifest.campaign.id,
25163
+ expectedUpdatedAt: version(AdsActivationEntityTypes.campaign, manifest.campaign.id),
25164
+ adGroups: manifest.campaign.adGroups.map((group) => ({
25165
+ id: group.id,
25166
+ expectedUpdatedAt: version(AdsActivationEntityTypes.ad_group, group.id),
25167
+ ads: group.ads.map((ad) => ({
25168
+ id: ad.id,
25169
+ expectedUpdatedAt: version(AdsActivationEntityTypes.ad, ad.id)
25170
+ }))
25171
+ }))
25172
+ }
25173
+ };
25174
+ return preflightAdsActivationApproval(provider, {
25175
+ adAccountId: request.adAccountId,
25176
+ manifest: refreshed
25177
+ });
25178
+ }
24951
25179
  function stepTarget(manifest, step) {
24952
25180
  const target = activationTargets(manifest).find(
24953
25181
  (candidate) => candidate.entityType === step.entityType && candidate.entityId === step.entityId
@@ -26519,7 +26747,11 @@ function registerAdsActivationRoutes(app, opts) {
26519
26747
  const runtime = await opts.resolveRuntime(project);
26520
26748
  let preflight;
26521
26749
  try {
26522
- preflight = await preflightAdsActivationApproval(runtime.provider, {
26750
+ preflight = body.versionPolicy === AdsActivationVersionPolicies.refreshSemanticallyUnchanged ? await refreshSemanticallyUnchangedAdsActivationManifest(runtime.provider, {
26751
+ adAccountId: runtime.adAccountId,
26752
+ manifest: body.manifest,
26753
+ semanticallyMatchesMaterialization: runtime.semanticallyMatchesMaterialization ?? (() => false)
26754
+ }) : await preflightAdsActivationApproval(runtime.provider, {
26523
26755
  adAccountId: runtime.adAccountId,
26524
26756
  manifest: body.manifest
26525
26757
  });
@@ -26960,6 +27192,44 @@ function entityMatchesReconcileFields(entity, desired, fingerprint) {
26960
27192
  const candidateFingerprint = requestHash(selected);
26961
27193
  return fingerprint === null ? candidateFingerprint === requestHash(desired) : candidateFingerprint === fingerprint;
26962
27194
  }
27195
+ function semanticallyMatchesMaterializedEntity(app, input) {
27196
+ const createKind = (() => {
27197
+ switch (input.entityType) {
27198
+ case AdsActivationEntityTypes.campaign:
27199
+ return AdsOperationKinds.campaign_create;
27200
+ case AdsActivationEntityTypes.ad_group:
27201
+ return AdsOperationKinds.ad_group_create;
27202
+ case AdsActivationEntityTypes.ad:
27203
+ return AdsOperationKinds.ad_create;
27204
+ }
27205
+ })();
27206
+ const updateKind = (() => {
27207
+ switch (input.entityType) {
27208
+ case AdsActivationEntityTypes.campaign:
27209
+ return AdsOperationKinds.campaign_update;
27210
+ case AdsActivationEntityTypes.ad_group:
27211
+ return AdsOperationKinds.ad_group_update;
27212
+ case AdsActivationEntityTypes.ad:
27213
+ return AdsOperationKinds.ad_update;
27214
+ }
27215
+ })();
27216
+ const rows = app.db.select().from(adsOperations).where(and20(
27217
+ eq26(adsOperations.projectId, input.projectId),
27218
+ eq26(adsOperations.entityType, input.entityType),
27219
+ eq26(adsOperations.entityId, input.entityId),
27220
+ inArray12(adsOperations.kind, [createKind, updateKind])
27221
+ )).orderBy(asc6(adsOperations.createdAt), asc6(adsOperations.id)).all();
27222
+ if (rows.some((row) => row.state === AdsOperationStates.pending || row.state === AdsOperationStates.reconciling || row.state === AdsOperationStates.unknown || row.kind === updateKind && row.state === AdsOperationStates.succeeded)) {
27223
+ return false;
27224
+ }
27225
+ const creates = rows.filter((row) => row.kind === createKind && row.state === AdsOperationStates.succeeded);
27226
+ if (creates.length !== 1) return false;
27227
+ const create = creates[0];
27228
+ if (create.adAccountId !== input.adAccountId || create.reconcileStrategy !== AdsReconcileStrategies.create_fingerprint || !create.reconcileFields) {
27229
+ return false;
27230
+ }
27231
+ return entityMatchesReconcileFields(input.entity, create.reconcileFields, null);
27232
+ }
26963
27233
  function reconcileFieldsWithoutParent(fields) {
26964
27234
  const { campaignId: _campaignId, adGroupId: _adGroupId, ...withoutParent } = fields;
26965
27235
  return withoutParent;
@@ -27743,6 +28013,16 @@ async function adsRoutes(app, opts) {
27743
28013
  }
27744
28014
  return {
27745
28015
  adAccountId,
28016
+ semanticallyMatchesMaterialization: (entityType, entityId, entity) => {
28017
+ if (entity.status === null || entity.updatedAt === null) return false;
28018
+ return semanticallyMatchesMaterializedEntity(app, {
28019
+ projectId: project.id,
28020
+ adAccountId,
28021
+ entityType,
28022
+ entityId,
28023
+ entity: { ...entity, status: entity.status, updatedAt: entity.updatedAt }
28024
+ });
28025
+ },
27746
28026
  provider: {
27747
28027
  getAccount: refreshAccount,
27748
28028
  getCampaign: (id) => operator.getCampaign(apiKey, id),
@@ -42131,6 +42411,7 @@ export {
42131
42411
  notifications,
42132
42412
  gscSearchData,
42133
42413
  gscDailyTotals,
42414
+ gscQueryDailyTotals,
42134
42415
  gscUrlInspections,
42135
42416
  gscCoverageSnapshots,
42136
42417
  siteAuditSnapshots,
@@ -10,7 +10,7 @@ import {
10
10
  loadConfig,
11
11
  loadConfigRaw,
12
12
  saveConfigPatch
13
- } from "./chunk-WGGB7QBD.js";
13
+ } from "./chunk-WA57WG6W.js";
14
14
  import {
15
15
  CC_CACHE_DIR,
16
16
  DUCKDB_SPEC,
@@ -77,6 +77,7 @@ import {
77
77
  groupRunsByCreatedAt,
78
78
  gscCoverageSnapshots,
79
79
  gscDailyTotals,
80
+ gscQueryDailyTotals,
80
81
  gscSearchData,
81
82
  gscUrlInspections,
82
83
  hashAttributes,
@@ -115,7 +116,7 @@ import {
115
116
  siteAuditPages,
116
117
  siteAuditSnapshots,
117
118
  usageCounters
118
- } from "./chunk-BGLBWFMV.js";
119
+ } from "./chunk-FCGRCCX6.js";
119
120
  import {
120
121
  AGENT_MEMORY_VALUE_MAX_BYTES,
121
122
  AGENT_PROVIDER_IDS,
@@ -185,7 +186,7 @@ import {
185
186
  validationError,
186
187
  winnabilityClassLabel,
187
188
  withRetry
188
- } from "./chunk-SLCGTOKT.js";
189
+ } from "./chunk-RFLNTI7Y.js";
189
190
 
190
191
  // src/telemetry.ts
191
192
  import crypto from "crypto";
@@ -3869,6 +3870,36 @@ async function executeGscSync(db, runId, projectId, opts) {
3869
3870
  }).run();
3870
3871
  }
3871
3872
  log2.info("daily-totals.complete", { runId, projectId, rowCount: totalRows.length });
3873
+ const queryTotalRows = await fetchSearchAnalytics(accessToken, propertyId, {
3874
+ startDate,
3875
+ endDate,
3876
+ dimensions: ["date", "query"]
3877
+ });
3878
+ db.delete(gscQueryDailyTotals).where(
3879
+ and2(
3880
+ eq2(gscQueryDailyTotals.projectId, projectId),
3881
+ sql2`${gscQueryDailyTotals.date} >= ${startDate}`,
3882
+ sql2`${gscQueryDailyTotals.date} <= ${endDate}`
3883
+ )
3884
+ ).run();
3885
+ const queryTotalsNow = (/* @__PURE__ */ new Date()).toISOString();
3886
+ for (const row of queryTotalRows) {
3887
+ const [date, query] = row.keys;
3888
+ if (!date || !query) continue;
3889
+ db.insert(gscQueryDailyTotals).values({
3890
+ id: crypto4.randomUUID(),
3891
+ projectId,
3892
+ date,
3893
+ query,
3894
+ clicks: row.clicks,
3895
+ impressions: row.impressions,
3896
+ position: String(row.position),
3897
+ syncedAt: queryTotalsNow,
3898
+ syncRunId: runId,
3899
+ createdAt: queryTotalsNow
3900
+ }).run();
3901
+ }
3902
+ log2.info("query-totals.complete", { runId, projectId, rowCount: queryTotalRows.length });
3872
3903
  const pageClicks = /* @__PURE__ */ new Map();
3873
3904
  for (const row of rows) {
3874
3905
  const page = row.keys[1];
@@ -4612,7 +4643,10 @@ async function fetchAllPages(apiKey, path10, queryPairs) {
4612
4643
  );
4613
4644
  }
4614
4645
  function insightsPairs(opts) {
4615
- return (opts?.fields ?? []).map((field) => `fields[]=${encodeURIComponent(field)}`);
4646
+ return [
4647
+ ...(opts?.fields ?? []).map((field) => `fields[]=${encodeURIComponent(field)}`),
4648
+ ...(opts?.timeRanges ?? []).map((range) => `time_ranges[]=${encodeURIComponent(JSON.stringify(range))}`)
4649
+ ];
4616
4650
  }
4617
4651
  async function getAdAccount(apiKey) {
4618
4652
  validateApiKey(apiKey);
@@ -4793,6 +4827,31 @@ function removeOpenAiAdsConnection(config, projectName) {
4793
4827
  var log4 = createLogger("AdsSync");
4794
4828
  var CAMPAIGN_INSIGHT_FIELDS = ["campaign.impressions", "campaign.clicks", "campaign.spend", "campaign.conversions", "metadata.readable_time"];
4795
4829
  var AD_GROUP_INSIGHT_FIELDS = ["ad_group.impressions", "ad_group.clicks", "ad_group.spend", "ad_group.conversions", "metadata.readable_time"];
4830
+ var INSIGHTS_LOOKBACK_MS = 90 * 24 * 60 * 60 * 1e3;
4831
+ function accountHour(date, timezone) {
4832
+ const parts = new Intl.DateTimeFormat("en-CA", {
4833
+ timeZone: timezone,
4834
+ year: "numeric",
4835
+ month: "2-digit",
4836
+ day: "2-digit",
4837
+ hour: "2-digit",
4838
+ hourCycle: "h23"
4839
+ }).formatToParts(date);
4840
+ const value = (type) => {
4841
+ const part = parts.find((candidate) => candidate.type === type)?.value;
4842
+ if (!part) throw new Error(`OpenAI Ads account timezone did not produce a ${type}`);
4843
+ return part;
4844
+ };
4845
+ return `${value("year")}-${value("month")}-${value("day")}T${value("hour")}`;
4846
+ }
4847
+ function trailingAdsInsightHourRange(now, timezone) {
4848
+ return {
4849
+ type: "hour_range",
4850
+ since: accountHour(new Date(now.getTime() - INSIGHTS_LOOKBACK_MS), timezone),
4851
+ until: accountHour(now, timezone),
4852
+ timezone
4853
+ };
4854
+ }
4796
4855
  function toInsightUpserts(level, entityId, rows) {
4797
4856
  const upserts = [];
4798
4857
  for (const row of rows) {
@@ -4830,6 +4889,7 @@ async function executeAdsSync(db, runId, projectId, opts) {
4830
4889
  log4.info("sync.start", { runId, projectId, adAccountId: connRow.adAccountId });
4831
4890
  const account = await getAdAccount(apiKey);
4832
4891
  const campaigns = await listCampaigns(apiKey);
4892
+ const insightTimeRanges = [trailingAdsInsightHourRange(/* @__PURE__ */ new Date(), account.timezone)];
4833
4893
  const errors = /* @__PURE__ */ new Map();
4834
4894
  const adGroupsByCampaign = /* @__PURE__ */ new Map();
4835
4895
  const adsByGroup = /* @__PURE__ */ new Map();
@@ -4839,12 +4899,18 @@ async function executeAdsSync(db, runId, projectId, opts) {
4839
4899
  try {
4840
4900
  const [adGroups, campaignInsights] = await Promise.all([
4841
4901
  listAdGroups(apiKey, campaign.id),
4842
- getCampaignInsights(apiKey, campaign.id, { fields: CAMPAIGN_INSIGHT_FIELDS })
4902
+ getCampaignInsights(apiKey, campaign.id, {
4903
+ fields: CAMPAIGN_INSIGHT_FIELDS,
4904
+ timeRanges: insightTimeRanges
4905
+ })
4843
4906
  ]);
4844
4907
  const groupResults = await Promise.all(adGroups.map(async (group) => ({
4845
4908
  group,
4846
4909
  ads: await listAds(apiKey, group.id),
4847
- insights: await getAdGroupInsights(apiKey, group.id, { fields: AD_GROUP_INSIGHT_FIELDS })
4910
+ insights: await getAdGroupInsights(apiKey, group.id, {
4911
+ fields: AD_GROUP_INSIGHT_FIELDS,
4912
+ timeRanges: insightTimeRanges
4913
+ })
4848
4914
  })));
4849
4915
  syncedCampaigns.push(campaign);
4850
4916
  adGroupsByCampaign.set(campaign.id, adGroups);
@@ -6901,7 +6967,7 @@ function readStoredGroundingSources(rawResponse) {
6901
6967
  return result;
6902
6968
  }
6903
6969
  async function backfillInsightsCommand(project, opts) {
6904
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-SACHNNCT.js");
6970
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-K6B3MTDS.js");
6905
6971
  const config = loadConfig();
6906
6972
  const db = createClient(config.database);
6907
6973
  migrate(db);
@@ -6848,10 +6848,19 @@ var adsOperationStepDtoSchema = z35.discriminatedUnion("state", [
6848
6848
  adsOperationStepRollbackFailedDtoSchema,
6849
6849
  adsOperationStepUnknownDtoSchema
6850
6850
  ]);
6851
+ var AdsActivationVersionPolicies = {
6852
+ exact: "exact",
6853
+ refreshSemanticallyUnchanged: "refresh_semantically_unchanged"
6854
+ };
6855
+ var adsActivationVersionPolicySchema = z35.enum([
6856
+ AdsActivationVersionPolicies.exact,
6857
+ AdsActivationVersionPolicies.refreshSemanticallyUnchanged
6858
+ ]);
6851
6859
  var adsActivationGrantCreateRequestSchema = z35.object({
6852
6860
  manifest: adsActivationManifestSchema,
6853
6861
  executorApiKeyId: adsEntityIdSchema,
6854
- expiresAt: adsActivationIsoTimestampSchema
6862
+ expiresAt: adsActivationIsoTimestampSchema,
6863
+ versionPolicy: adsActivationVersionPolicySchema.default(AdsActivationVersionPolicies.exact)
6855
6864
  }).strict();
6856
6865
  var adsActivationGrantResponseSchema = z35.object({
6857
6866
  grant: adsActivationGrantDtoSchema
@@ -7441,6 +7450,7 @@ export {
7441
7450
  adsOperationResponseSchema,
7442
7451
  adsActivationGrantDtoSchema,
7443
7452
  adsOperationStepDtoSchema,
7453
+ AdsActivationVersionPolicies,
7444
7454
  adsActivationGrantCreateRequestSchema,
7445
7455
  adsActivationGrantResponseSchema,
7446
7456
  adsActivationGrantRevokeRequestSchema,