@canonry/canonry 7.86.0 → 7.87.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +21 -1
  2. package/assets/agent-workspace/skills/canonry/references/server-side-traffic.md +10 -6
  3. package/assets/assets/{AgentSetupPage-0f7Ca4IZ.js → AgentSetupPage-_ZP3F_zA.js} +4 -4
  4. package/assets/assets/{AuditHistoryPanel-BAQuFTNl.js → AuditHistoryPanel-9B0XpYsN.js} +1 -1
  5. package/assets/assets/{BacklinksPage-BK2bn9Xg.js → BacklinksPage-CsvVgXTZ.js} +1 -1
  6. package/assets/assets/{ChartPrimitives-B5L9FUj3.js → ChartPrimitives-4D5wgAn5.js} +1 -1
  7. package/assets/assets/{HistoryPage-ZBfL9nGw.js → HistoryPage-BCxpCul0.js} +1 -1
  8. package/assets/assets/{MeasurementPropertyPage-DDSOq4aU.js → MeasurementPropertyPage-BObvWjVx.js} +4 -4
  9. package/assets/assets/ProjectPage-y378tOaT.js +11 -0
  10. package/assets/assets/{RunsPage-ke0wMYLg.js → RunsPage-CfQ_kqlu.js} +1 -1
  11. package/assets/assets/{SettingsPage-Bbi-sTBk.js → SettingsPage-PIgRvqJc.js} +1 -1
  12. package/assets/assets/SiteHealthSection-DuUE1cB0.js +2 -0
  13. package/assets/assets/ToneBadge-BgTLSTKp.js +60 -0
  14. package/assets/assets/{TrafficPage-nXixENx9.js → TrafficPage-B_YunwjH.js} +1 -1
  15. package/assets/assets/TrafficSourceDetailPage-C51W87fU.js +1 -0
  16. package/assets/assets/{account-context-BmN431xn.js → account-context-CVjb3ZSn.js} +1 -1
  17. package/assets/assets/agent-trust-copy-BlpC8jo5.js +1 -0
  18. package/assets/assets/{card-DUTMWeHB.js → card-BnVez9nG.js} +1 -1
  19. package/assets/assets/{extract-error-message-BFaIHLqb.js → extract-error-message-Dqirbrl0.js} +1 -1
  20. package/assets/assets/{format-helpers-D1w2yznw.js → format-helpers-DVXZD864.js} +1 -1
  21. package/assets/assets/{index-DIHdWY7b.css → index-BOUrrC9J.css} +1 -1
  22. package/assets/assets/index-CsvEqhK4.js +60 -0
  23. package/assets/assets/query-client-CVc7wMKJ.js +1 -0
  24. package/assets/assets/react-query-CSlTSOjz.js +1 -0
  25. package/assets/assets/{react-sigma_core.esm.min-cvMDhRNS.js → react-sigma_core.esm.min-CVfGIEIE.js} +3 -3
  26. package/assets/assets/{service-CvCKbPnf.js → service-BGqeyqhZ.js} +1 -1
  27. package/assets/index.html +9 -9
  28. package/dist/{ads-7IJTHW6T.js → ads-ZUZWSIBV.js} +3 -3
  29. package/dist/{agent-4UC2X7O2.js → agent-YDS3WY7B.js} +7 -7
  30. package/dist/{agent-ask-SVPU2WQU.js → agent-ask-7VUPXTXL.js} +3 -3
  31. package/dist/{agent-conversations-PJQRY5RX.js → agent-conversations-WPEBK3BS.js} +3 -3
  32. package/dist/{agent-journeys-JOD5DQ6G.js → agent-journeys-H5CCYLNB.js} +3 -3
  33. package/dist/{agent-memory-KTMZMUYF.js → agent-memory-CNPKPIWD.js} +3 -3
  34. package/dist/{agent-providers-VEBB3WEW.js → agent-providers-U7FLJO5I.js} +3 -3
  35. package/dist/{agent-transcript-RRZJCMK2.js → agent-transcript-VY3BQNPU.js} +3 -3
  36. package/dist/{analytics-EZV4XJKR.js → analytics-PYUNO7C2.js} +3 -3
  37. package/dist/{apply-NP7ORQS4.js → apply-YNBEYC2G.js} +3 -3
  38. package/dist/{backfill-7T6VFPWT.js → backfill-PNHBAV5N.js} +10 -10
  39. package/dist/{backlinks-KTEJM2IP.js → backlinks-LVHZ2MQ7.js} +3 -3
  40. package/dist/{bing-TC54HJUB.js → bing-A2MUG6AV.js} +3 -3
  41. package/dist/{bootstrap-UDSN22NT.js → bootstrap-AZYQXBHN.js} +10 -10
  42. package/dist/{cdp-IZSHF3MS.js → cdp-HCYY4SXR.js} +7 -7
  43. package/dist/{chunk-IILPHGRC.js → chunk-2AXFKYJU.js} +2 -2
  44. package/dist/{chunk-U3QNTJBP.js → chunk-2FJB3CSS.js} +1 -1
  45. package/dist/{chunk-HHMOUS26.js → chunk-2I22IUA3.js} +5 -5
  46. package/dist/{chunk-KEIKD6JQ.js → chunk-3KK6FWDF.js} +1 -1
  47. package/dist/{chunk-STHWLBYT.js → chunk-4FWDITQG.js} +1 -1
  48. package/dist/{chunk-3K3QBKNC.js → chunk-4LNZTL7M.js} +3 -3
  49. package/dist/{chunk-Z5LJI5XK.js → chunk-6F5ULQDY.js} +4 -4
  50. package/dist/{chunk-OXI45LZP.js → chunk-BJHHD7VE.js} +2 -2
  51. package/dist/{chunk-TTXBQCE6.js → chunk-BKZ6PQAE.js} +21 -21
  52. package/dist/{chunk-EX6GVZAE.js → chunk-CBWGUGME.js} +2 -1
  53. package/dist/{chunk-LJ6VNMCY.js → chunk-CR54BCUP.js} +7 -7
  54. package/dist/{chunk-CBNFWSWU.js → chunk-DOPUEXMC.js} +2 -2
  55. package/dist/{chunk-RI2ZT3OR.js → chunk-ECZGTJIQ.js} +6 -6
  56. package/dist/{chunk-KUVTXIFX.js → chunk-F643WELJ.js} +1913 -1743
  57. package/dist/{chunk-5WEGX3H7.js → chunk-FAMCDCVK.js} +1 -1
  58. package/dist/{chunk-5ZAQVRFL.js → chunk-G44S34CE.js} +2 -2
  59. package/dist/{chunk-UTG6UUGK.js → chunk-GBWCEOZR.js} +1 -1
  60. package/dist/{chunk-E6IZVSRY.js → chunk-GDU6IMGB.js} +2 -2
  61. package/dist/{chunk-RR76O744.js → chunk-GXTCDNX7.js} +1 -1
  62. package/dist/{chunk-JAK37YKH.js → chunk-IBGZ6D6R.js} +2 -2
  63. package/dist/{chunk-WVMWY4SE.js → chunk-ICS2WJ7H.js} +3 -3
  64. package/dist/{chunk-2J5EUXO2.js → chunk-IK7QEKKD.js} +1 -1
  65. package/dist/{chunk-X3EU4UPS.js → chunk-INH773FB.js} +3 -3
  66. package/dist/{chunk-5KHF5FV5.js → chunk-J7ITGXCS.js} +4 -4
  67. package/dist/{chunk-BLGYC2K7.js → chunk-JAYUWQAV.js} +1 -1
  68. package/dist/{chunk-GO4RP6XE.js → chunk-JDTKG7KJ.js} +7 -3
  69. package/dist/{chunk-F5SQITTJ.js → chunk-MNLAIB67.js} +1 -1
  70. package/dist/{chunk-SUSQEH2D.js → chunk-O26FQ7RI.js} +1 -1
  71. package/dist/{chunk-4EKPEVJ2.js → chunk-RHOIR5QY.js} +5 -5
  72. package/dist/{chunk-UPFWF6TF.js → chunk-RSPOFH66.js} +4 -4
  73. package/dist/{chunk-VBW33U4E.js → chunk-TEQF4ZJA.js} +22 -2
  74. package/dist/{chunk-AZP37FMA.js → chunk-TN53B5AB.js} +28 -3
  75. package/dist/{chunk-ZFL5DOGJ.js → chunk-UXSWQAVZ.js} +395 -342
  76. package/dist/{chunk-MJE3SGFW.js → chunk-VDYXB6SV.js} +1 -1
  77. package/dist/{chunk-DU4QEC65.js → chunk-W7DHXSXN.js} +281 -53
  78. package/dist/{citations-TOZK6OFP.js → citations-3EZTHA65.js} +3 -3
  79. package/dist/{cli-catalog-LNWBJNOD.js → cli-catalog-CBTM3JKP.js} +3 -3
  80. package/dist/{cli-commands-3HX4JSDX.js → cli-commands-62QECRBV.js} +7 -7
  81. package/dist/cli.js +13 -13
  82. package/dist/{competitor-54HLL354.js → competitor-VH4TXJLT.js} +3 -3
  83. package/dist/{content-ZIGSMH5U.js → content-TSMNHXJZ.js} +3 -3
  84. package/dist/{dashboard-YJH6IQGK.js → dashboard-4HOXCUQE.js} +6 -6
  85. package/dist/{demo-ZYK5XCP3.js → demo-IY7Y7KR3.js} +1 -1
  86. package/dist/{demo-server-LZ6MWJ2S.js → demo-server-BLLSDILQ.js} +13 -12
  87. package/dist/{discover-22QKOFIM.js → discover-XTJXTKCI.js} +3 -3
  88. package/dist/{discover-eval-6IFCCH2S.js → discover-eval-IVQI5YEW.js} +3 -3
  89. package/dist/{doctor-WLLBFOJU.js → doctor-SJ4SHSQO.js} +3 -3
  90. package/dist/{evidence-7VHTWNZW.js → evidence-C6T556YH.js} +3 -3
  91. package/dist/{export-cmd-5RZ3QDFH.js → export-cmd-JQZ5ZRSI.js} +3 -3
  92. package/dist/{ga-LPI3CWY4.js → ga-W2WHVRHN.js} +3 -3
  93. package/dist/{gbp-YBOMWR75.js → gbp-3AZIV5WF.js} +3 -3
  94. package/dist/{google-N42X5FBH.js → google-3CKWPZT7.js} +4 -4
  95. package/dist/{google-marketing-SO4B7Z7L.js → google-marketing-PJBGQYYW.js} +1 -1
  96. package/dist/{health-cmd-W7RQWCEB.js → health-cmd-4JTQMCXM.js} +3 -3
  97. package/dist/{history-SDRTOIDZ.js → history-TXUJFEO2.js} +3 -3
  98. package/dist/index.js +25 -25
  99. package/dist/{init-DPXOGP3P.js → init-JY76QAOC.js} +12 -12
  100. package/dist/{insights-636LLPVG.js → insights-XZQO4B45.js} +3 -3
  101. package/dist/intelligence-service-GJYBEHJE.js +16 -0
  102. package/dist/{journeys-NIXKVYA4.js → journeys-VFIV5JYV.js} +3 -3
  103. package/dist/{keys-U2JR56TX.js → keys-PV23G4LW.js} +3 -3
  104. package/dist/{keyword-GLRK2ZHR.js → keyword-D76KBIV5.js} +3 -3
  105. package/dist/{logs-ENACGWGS.js → logs-DYDZ6IEJ.js} +3 -3
  106. package/dist/{mcp-DST2XTP2.js → mcp-GAR5LDQD.js} +4 -4
  107. package/dist/mcp.js +9 -9
  108. package/dist/{measurement-plan-Y3SA4ZQC.js → measurement-plan-K24EOGMI.js} +5 -5
  109. package/dist/{notify-ZYXJQUHU.js → notify-XQHMPDQQ.js} +3 -3
  110. package/dist/{organic-evidence-NSX7VCNW.js → organic-evidence-UHHE7AO4.js} +3 -3
  111. package/dist/{overview-UO6Z64IT.js → overview-AUZ3XRS5.js} +4 -4
  112. package/dist/{project-BHXUPHQA.js → project-FUCPU2BD.js} +3 -3
  113. package/dist/{query-GDRAKEN5.js → query-HRMSXDKL.js} +3 -3
  114. package/dist/{research-YIOQ7WVP.js → research-R23XLZJ3.js} +3 -3
  115. package/dist/{results-export-KXMCAEHJ.js → results-export-N6IV3KG4.js} +3 -3
  116. package/dist/{run-6PN5GOVE.js → run-CJLG5EXH.js} +4 -4
  117. package/dist/{schedule-5B67GLAM.js → schedule-IPDCQ2I5.js} +3 -3
  118. package/dist/{search-BMVPCN2V.js → search-ZU4AKJ76.js} +3 -3
  119. package/dist/{serve-HB7DI7OC.js → serve-ZQXMTDE7.js} +26 -26
  120. package/dist/{session-3RTDTW6Z.js → session-MGOUDLXN.js} +13 -13
  121. package/dist/{settings-FJEYHYRH.js → settings-4FS5NRKP.js} +3 -3
  122. package/dist/{skills-6E7IUQUO.js → skills-GSFHACGN.js} +2 -2
  123. package/dist/{snapshot-BJQ7WPJZ.js → snapshot-4NUVPDK3.js} +3 -3
  124. package/dist/{src-QU75IDHI.js → src-SJYGA7NH.js} +2 -2
  125. package/dist/{status-3GHFPN54.js → status-ASBLMFP2.js} +4 -4
  126. package/dist/{technical-aeo-76JRC66W.js → technical-aeo-QXHRBGYY.js} +3 -3
  127. package/dist/{telemetry-KB73JBDZ.js → telemetry-J2I577I3.js} +4 -4
  128. package/dist/{tools-6VWDHNPZ.js → tools-ZWFLE5UR.js} +11 -11
  129. package/dist/{traffic-XB7MZJ23.js → traffic-B7Q4WPOV.js} +5 -5
  130. package/dist/trends-YOLCJBNM.js +56 -0
  131. package/dist/{users-HYBG3J72.js → users-LPSCZV3M.js} +3 -3
  132. package/dist/{visibility-stats-ZDB2LMRJ.js → visibility-stats-JJQTZSSK.js} +3 -3
  133. package/dist/{wordpress-SXKR6XNT.js → wordpress-4IKDT5KT.js} +3 -3
  134. package/package.json +1 -1
  135. package/assets/assets/ProjectPage-8cVgOm5o.js +0 -11
  136. package/assets/assets/SiteHealthSection-eKxTDUE2.js +0 -2
  137. package/assets/assets/ToneBadge-WAMyKadz.js +0 -60
  138. package/assets/assets/TrafficSourceDetailPage-ir5zJU_l.js +0 -1
  139. package/assets/assets/agent-trust-copy-zfx8m-cx.js +0 -1
  140. package/assets/assets/index-C5r0VqgO.js +0 -60
  141. package/assets/assets/query-client-DrIawr39.js +0 -1
  142. package/assets/assets/react-query-DQuOVjDi.js +0 -1
  143. package/dist/intelligence-service-JRVESWU2.js +0 -16
@@ -12,7 +12,7 @@ import {
12
12
  classifyTrafficPath,
13
13
  hostMatchesDomain,
14
14
  hostOf
15
- } from "./chunk-KUVTXIFX.js";
15
+ } from "./chunk-F643WELJ.js";
16
16
 
17
17
  // ../integration-traffic/src/agent-trust.ts
18
18
  var AGENT_TRUST_RULES_VERSION = 2;
@@ -13,14 +13,14 @@ import {
13
13
  normalizeCloudflareWorkerEvent,
14
14
  sessionizeJourneyRequests,
15
15
  verifyRequestSignature
16
- } from "./chunk-MJE3SGFW.js";
16
+ } from "./chunk-VDYXB6SV.js";
17
17
  import {
18
18
  currentOutcomeAttribution,
19
19
  getRequestContext,
20
20
  registerRequestContext,
21
21
  requestAuditIdentity,
22
22
  sanitizeRequestContext
23
- } from "./chunk-UTG6UUGK.js";
23
+ } from "./chunk-GBWCEOZR.js";
24
24
  import {
25
25
  RawEventSampleEventTypes,
26
26
  SentimentIdempotencyConflict,
@@ -168,7 +168,7 @@ import {
168
168
  userSessions,
169
169
  users,
170
170
  writeNegativeReviewMaxStars
171
- } from "./chunk-SUSQEH2D.js";
171
+ } from "./chunk-O26FQ7RI.js";
172
172
  import {
173
173
  ADS_ACTIVATE_SCOPE,
174
174
  ADS_ACTIVATION_ABSOLUTE_MAX_ENTITIES,
@@ -403,6 +403,7 @@ import {
403
403
  SiteCrawlUrlVersionFilters,
404
404
  SiteHealthLinkKinds,
405
405
  SiteHealthTemplateDetections,
406
+ TREND_SURFACE_DEFAULT_WINDOW,
406
407
  TrafficEventConfidences,
407
408
  TrafficEventKinds,
408
409
  TrafficEvidenceKinds,
@@ -412,6 +413,9 @@ import {
412
413
  TrafficSourceAuthModes,
413
414
  TrafficSourceStatuses,
414
415
  TrafficSourceTypes,
416
+ TrendBetter,
417
+ TrendSurfaces,
418
+ TrendUnits,
415
419
  USAGE_TELEMETRY_HEADERS,
416
420
  UserRoles,
417
421
  VerificationStatuses,
@@ -514,6 +518,7 @@ import {
514
518
  breakdownShares,
515
519
  bucketDuration,
516
520
  buildHarvestAnchorTerms,
521
+ buildLevelTrendFigure,
517
522
  buildMeasurementExecutionIdentity,
518
523
  buildMeasurementRunManifestV1,
519
524
  buildRedactedNotificationUrl,
@@ -772,6 +777,7 @@ import {
772
777
  internalError,
773
778
  intersectScopes,
774
779
  isBrowserProvider,
780
+ isCalendarDate,
775
781
  isEligibleFor,
776
782
  isFetchTransportError,
777
783
  isGa4AiAssistantChannel,
@@ -1203,6 +1209,9 @@ import {
1203
1209
  trafficSyncResponseSchema,
1204
1210
  trafficUnrecognizedAgentsQuerySchema,
1205
1211
  trafficUnrecognizedAgentsResponseSchema,
1212
+ trendSurfaceSchema,
1213
+ trendWindowSpan,
1214
+ trendsResponseSchema,
1206
1215
  truncateUtf16,
1207
1216
  uiTelemetryEventSchema,
1208
1217
  unsupportedKind,
@@ -1235,7 +1244,7 @@ import {
1235
1244
  wordpressSchemaDeployResultDtoSchema,
1236
1245
  wordpressSchemaStatusResultDtoSchema,
1237
1246
  wordpressStatusDtoSchema
1238
- } from "./chunk-KUVTXIFX.js";
1247
+ } from "./chunk-F643WELJ.js";
1239
1248
 
1240
1249
  // ../api-routes/src/user-session.ts
1241
1250
  import crypto2 from "crypto";
@@ -6336,6 +6345,7 @@ function isTechnicalAeoRead(request, url) {
6336
6345
  if (rest && /^technical-aeo\/runs\/[^/]+\/(?:progress|page-health-preview)$/.test(rest)) {
6337
6346
  return true;
6338
6347
  }
6348
+ if (rest === "trends") return queryValue(request, "surface") === "site-health";
6339
6349
  return rest === "technical-aeo" || rest === "technical-aeo/pages" || rest === "technical-aeo/trend" || rest === "technical-aeo/crawl" || rest === "technical-aeo/graph" || rest === "technical-aeo/subgraph" || rest === "technical-aeo/path" || rest === "technical-aeo/changes" || rest === "technical-aeo/crawl/pages/audit" || rest === "technical-aeo/crawl/pages" || rest === "technical-aeo/runs" || rest === "technical-aeo/structure" || rest === "technical-aeo/internal-links" || rest === "technical-aeo/internal-links/neighbors" || rest === "technical-aeo/dead-links";
6340
6350
  }
6341
6351
  function isEmbedQueryWrite(request) {
@@ -31509,9 +31519,9 @@ function buildReferralAssessment(db, projectName, input) {
31509
31519
  const { rows: observedRows, counts: raw } = sumCounts(sql23`1 = 1`);
31510
31520
  const landed = sumCounts(referralLandedCondition()).counts;
31511
31521
  const countable = sumCounts(countableReferralCondition()).counts;
31512
- const difference2 = (whole, part) => aiReferralClassCounts(whole.total - part.total, whole.paid - part.paid, whole.organic - part.organic);
31513
- const redirects = difference2(raw, landed);
31514
- const subresources = difference2(landed, countable);
31522
+ const difference3 = (whole, part) => aiReferralClassCounts(whole.total - part.total, whole.paid - part.paid, whole.organic - part.organic);
31523
+ const redirects = difference3(raw, landed);
31524
+ const subresources = difference3(landed, countable);
31515
31525
  const candidates = db.select({
31516
31526
  sourceId: aiReferralEventsHourly.sourceId,
31517
31527
  product: aiReferralEventsHourly.product,
@@ -35736,6 +35746,7 @@ var SCHEMA_TABLE = {
35736
35746
  SiteAuditRunResponseDto: siteAuditRunResponseSchema,
35737
35747
  SiteAuditScoreDto: siteAuditScoreSchema,
35738
35748
  SiteAuditTrendResponseDto: siteAuditTrendResponseSchema,
35749
+ TrendsResponseDto: trendsResponseSchema,
35739
35750
  SiteCrawlDeadLinksResponseDto: siteCrawlDeadLinksResponseSchema,
35740
35751
  SiteCrawlGraphResponseDto: siteCrawlGraphResponseSchema,
35741
35752
  SiteCrawlInternalLinksResponseDto: siteCrawlInternalLinksResponseSchema,
@@ -43673,6 +43684,25 @@ var routeCatalog = [
43673
43684
  404: errorResponse("Project not found.")
43674
43685
  }
43675
43686
  },
43687
+ {
43688
+ method: "get",
43689
+ path: "/api/v1/projects/{name}/trends",
43690
+ summary: "Get a dashboard trend card: figures, their readings over a window, and each change",
43691
+ description: "Stored readings only; nothing calls a provider. `index-coverage` returns Google and Bing indexed and not-indexed pages, `site-health` the site score and pages checked per scan, `backlinks` referring domains, total linking hosts and top-10 concentration per Common Crawl release, with crawler and proxy hosts left out as on the Backlinks tab. Every figure is a level: its value is the latest reading in the window and its change runs from the first reading in the window (`change.basis`). The window ends on the surface's latest reading; pass `startDate`/`endDate` instead to read your own calendar range. A Common Crawl release reads at its window's last month, in crawl order, and carries its window as `label`. Readings before a figure's `recordedFrom` were never recorded. Groups never recorded (for example Bing not connected) are left out.",
43692
+ tags: ["trends"],
43693
+ parameters: [
43694
+ nameParameter,
43695
+ { name: "surface", in: "query", required: true, description: "Which card to read.", schema: { type: "string", enum: ["index-coverage", "site-health", "backlinks"] } },
43696
+ { name: "window", in: "query", description: "Window ending on the latest reading. Defaults to 90d for index-coverage and all for site-health and backlinks. Any other value is rejected with 400. Not allowed with startDate or endDate.", schema: { type: "string", enum: ["7d", "30d", "90d", "all"] } },
43697
+ { name: "startDate", in: "query", description: "First calendar day (YYYY-MM-DD) of your own range instead of a window. Left out, the range starts at the earliest reading.", schema: { type: "string", format: "date" } },
43698
+ { name: "endDate", in: "query", description: "Last calendar day (YYYY-MM-DD) of your own range instead of a window. Left out, the range ends at the latest reading.", schema: { type: "string", format: "date" } }
43699
+ ],
43700
+ responses: {
43701
+ 200: jsonResponse("Trend card returned. `window` is null for a date range.", "TrendsResponseDto"),
43702
+ 400: errorResponse("Missing or unknown surface, an unknown window, an invalid date, a start after the end, or a window and a date range together."),
43703
+ 404: errorResponse("Project not found.")
43704
+ }
43705
+ },
43676
43706
  {
43677
43707
  method: "get",
43678
43708
  path: "/api/v1/projects/{name}/technical-aeo/crawl",
@@ -47103,9 +47133,22 @@ function computeGscPeriodComparison(daily, bounds) {
47103
47133
  impressions: comparable ? relativeChange(trailing.impressions, prior.impressions) : null,
47104
47134
  ctr: comparable ? relativeChange(trailing.ctr, prior.ctr) : null,
47105
47135
  position: comparable ? relativeChange(trailing.position, prior.position) : null
47136
+ },
47137
+ difference: {
47138
+ clicks: comparable ? trailing.clicks - prior.clicks : null,
47139
+ impressions: comparable ? trailing.impressions - prior.impressions : null,
47140
+ // A rate's difference is itself a fraction, so it goes out at wire precision.
47141
+ ctr: comparable ? roundedFraction(difference2(trailing.ctr, prior.ctr)) : null,
47142
+ position: comparable ? difference2(trailing.position, prior.position) : null
47106
47143
  }
47107
47144
  };
47108
47145
  }
47146
+ function difference2(trailing, prior) {
47147
+ return trailing === null || prior === null ? null : trailing - prior;
47148
+ }
47149
+ function roundedFraction(value) {
47150
+ return value === null ? null : roundRatio(value, RatioUnits.fraction);
47151
+ }
47109
47152
 
47110
47153
  // ../api-routes/src/ga-property-data.ts
47111
47154
  import { eq as eq73 } from "drizzle-orm";
@@ -61697,6 +61740,35 @@ function isValidReleaseId(id) {
61697
61740
  function formatReleaseId(year, window2) {
61698
61741
  return `cc-main-${year}-${window2}`;
61699
61742
  }
61743
+ var MONTH_NAMES = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
61744
+ var MONTH_LABELS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
61745
+ function releaseWindow(id) {
61746
+ const match = RELEASE_ID_REGEX.exec(id);
61747
+ if (!match) return null;
61748
+ const first = MONTH_NAMES.indexOf(match[2].slice(0, 3));
61749
+ return { first: Number(match[1]) * 12 + first, last: Number(match[1]) * 12 + first + 2 };
61750
+ }
61751
+ var monthLabel = (absolute) => `${MONTH_LABELS[absolute % 12]} ${Math.floor(absolute / 12)}`;
61752
+ function compareReleaseIds(a, b) {
61753
+ const left = releaseWindow(a);
61754
+ const right = releaseWindow(b);
61755
+ if (left && right) return left.first - right.first;
61756
+ if (left) return 1;
61757
+ if (right) return -1;
61758
+ return a < b ? -1 : a > b ? 1 : 0;
61759
+ }
61760
+ function releaseWindowEndMonth(id) {
61761
+ const window2 = releaseWindow(id);
61762
+ if (!window2) return null;
61763
+ return `${Math.floor(window2.last / 12)}-${String(window2.last % 12 + 1).padStart(2, "0")}-01`;
61764
+ }
61765
+ function formatReleaseWindow(id) {
61766
+ const window2 = releaseWindow(id);
61767
+ if (!window2) return null;
61768
+ const firstYear = Math.floor(window2.first / 12);
61769
+ const lastYear = Math.floor(window2.last / 12);
61770
+ return firstYear === lastYear ? `${MONTH_LABELS[window2.first % 12]}-${monthLabel(window2.last)}` : `${monthLabel(window2.first)}-${monthLabel(window2.last)}`;
61771
+ }
61700
61772
 
61701
61773
  // ../integration-commoncrawl/src/release-discovery.ts
61702
61774
  var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
@@ -62193,7 +62265,12 @@ function latestSummaryForProject(db, projectId, source, release) {
62193
62265
  eq84(backlinkSummaries.source, source)
62194
62266
  ];
62195
62267
  if (release) conditions.push(eq84(backlinkSummaries.release, release));
62196
- return db.select().from(backlinkSummaries).where(and73(...conditions)).orderBy(desc38(backlinkSummaries.queriedAt)).limit(1).get();
62268
+ const rows = db.select().from(backlinkSummaries).where(and73(...conditions)).orderBy(desc38(backlinkSummaries.queriedAt)).all();
62269
+ return rows.reduce((latest, row) => {
62270
+ if (!latest) return row;
62271
+ const newer = isValidReleaseId(row.release) && isValidReleaseId(latest.release) && compareReleaseIds(row.release, latest.release) > 0;
62272
+ return newer ? row : latest;
62273
+ }, void 0);
62197
62274
  }
62198
62275
  function parseExcludeCrawlers(value) {
62199
62276
  if (!value) return false;
@@ -62235,6 +62312,31 @@ function computeFilteredSummary(db, base) {
62235
62312
  excludedHosts: Math.max(0, unfilteredHosts - totalHosts)
62236
62313
  };
62237
62314
  }
62315
+ function computeFilteredReleaseTotals(db, projectId, source) {
62316
+ const kept = sql35`case when ${backlinkCrawlerExclusionClause()} then 1 else 0 end`;
62317
+ const rows = db.all(sql35`
62318
+ SELECT release,
62319
+ COUNT(*) AS domain_rows,
62320
+ SUM(kept) AS linking_domains,
62321
+ COALESCE(SUM(CASE WHEN kept = 1 THEN num_hosts ELSE 0 END), 0) AS hosts,
62322
+ COALESCE(SUM(CASE WHEN kept = 1 AND kept_rank <= 10 THEN num_hosts ELSE 0 END), 0) AS top10
62323
+ FROM (
62324
+ SELECT ${backlinkDomains.release} AS release, ${backlinkDomains.numHosts} AS num_hosts, ${kept} AS kept,
62325
+ ROW_NUMBER() OVER (PARTITION BY ${backlinkDomains.release}, ${kept} ORDER BY ${backlinkDomains.numHosts} DESC) AS kept_rank
62326
+ FROM ${backlinkDomains}
62327
+ WHERE ${and73(eq84(backlinkDomains.projectId, projectId), eq84(backlinkDomains.source, source))}
62328
+ )
62329
+ GROUP BY release`);
62330
+ return new Map(rows.map((row) => {
62331
+ const hosts = Number(row.hosts);
62332
+ return [row.release, {
62333
+ domainRows: Number(row.domain_rows),
62334
+ linkingDomains: Number(row.linking_domains),
62335
+ hosts,
62336
+ top10HostsShare: Number((hosts > 0 ? Number(row.top10) / hosts : 0).toFixed(6))
62337
+ }];
62338
+ }));
62339
+ }
62238
62340
  function buildSourceAvailability(db, projectId, source, connected2, excludeCrawlers) {
62239
62341
  const summary3 = db.select().from(backlinkSummaries).where(and73(eq84(backlinkSummaries.projectId, projectId), eq84(backlinkSummaries.source, source))).orderBy(desc38(backlinkSummaries.queriedAt)).limit(1).get();
62240
62342
  let totalLinkingDomains = summary3?.totalLinkingDomains ?? 0;
@@ -62485,6 +62587,7 @@ var DEFAULT_VERCEL_SYNC_DEADLINE_MS = 4 * 6e4;
62485
62587
  var CLOUDFLARE_QUEUE_BATCH_SIZE = 100;
62486
62588
  var DEFAULT_CLOUDFLARE_QUEUE_SYNC_DEADLINE_MS = 4 * 6e4;
62487
62589
  var DEFAULT_CLOUDFLARE_QUEUE_MAX_BATCHES = 300;
62590
+ var DEFAULT_CLOUDFLARE_QUEUE_EMPTY_PULL_BACKOFF_MS = [500, 1e3, 2e3, 4e3, 8e3];
62488
62591
  var CLOUDFLARE_QUEUE_RESIDUAL_WARN_COUNT = 1e3;
62489
62592
  var MIN_VERCEL_SYNC_DEADLINE_MS = 3e4;
62490
62593
  var MAX_VERCEL_SYNC_DEADLINE_MS = 15 * 6e4;
@@ -64714,9 +64817,13 @@ async function trafficRoutes(app, opts) {
64714
64817
  const readQueueRetentionSeconds = opts.getCloudflareQueueRetentionSeconds ?? getCloudflareQueueRetentionSeconds;
64715
64818
  const cloudflareQueueMaxBatches = opts.cloudflareQueueMaxBatches ?? DEFAULT_CLOUDFLARE_QUEUE_MAX_BATCHES;
64716
64819
  const cloudflareQueueSyncDeadlineMs = opts.cloudflareQueueSyncDeadlineMs ?? DEFAULT_CLOUDFLARE_QUEUE_SYNC_DEADLINE_MS;
64820
+ const cloudflareQueueEmptyPullBackoffMs = opts.cloudflareQueueEmptyPullBackoffMs ?? DEFAULT_CLOUDFLARE_QUEUE_EMPTY_PULL_BACKOFF_MS;
64717
64821
  if (!Number.isInteger(cloudflareQueueMaxBatches) || cloudflareQueueMaxBatches < 1 || cloudflareQueueMaxBatches > 1e3) {
64718
64822
  throw new RangeError("cloudflareQueueMaxBatches must be an integer from 1 to 1000");
64719
64823
  }
64824
+ if (cloudflareQueueEmptyPullBackoffMs.some((ms) => !Number.isFinite(ms) || ms < 0)) {
64825
+ throw new RangeError("cloudflareQueueEmptyPullBackoffMs must hold non-negative milliseconds");
64826
+ }
64720
64827
  const allowLoopback = opts.allowLoopbackWebhooks === true;
64721
64828
  let rawSampleRetentionTimer;
64722
64829
  app.addHook("onReady", async () => {
@@ -65898,9 +66005,20 @@ async function trafficRoutes(app, opts) {
65898
66005
  break;
65899
66006
  }
65900
66007
  consecutiveEmptyPulls = pulled.skippedUnleasedMessageCount === 0 ? consecutiveEmptyPulls + 1 : 0;
65901
- if (consecutiveEmptyPulls >= 2) {
65902
- stopReason = CloudflareQueueDrainStopReasons["empty-pulls"];
65903
- break;
66008
+ if (consecutiveEmptyPulls > 0) {
66009
+ const waits = remainingBacklogCount >= CLOUDFLARE_QUEUE_BATCH_SIZE ? cloudflareQueueEmptyPullBackoffMs : [0];
66010
+ if (consecutiveEmptyPulls > waits.length) {
66011
+ stopReason = CloudflareQueueDrainStopReasons["empty-pulls"];
66012
+ break;
66013
+ }
66014
+ const waitMs = waits[consecutiveEmptyPulls - 1];
66015
+ if (waitMs > 0) {
66016
+ if (waitMs >= drainDeadlineMs - Date.now()) {
66017
+ stopReason = CloudflareQueueDrainStopReasons.deadline;
66018
+ break;
66019
+ }
66020
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
66021
+ }
65904
66022
  }
65905
66023
  } else {
65906
66024
  consecutiveEmptyPulls = 0;
@@ -73303,6 +73421,15 @@ var FETCHED_SITE_CRAWL_STATES = /* @__PURE__ */ new Set([
73303
73421
  "fetched"
73304
73422
  ]);
73305
73423
  var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
73424
+ function surfaceableSiteAuditFilters(projectId) {
73425
+ return [
73426
+ eq106(siteAuditSnapshots.projectId, projectId),
73427
+ eq106(runs.projectId, projectId),
73428
+ eq106(runs.kind, RunKinds["site-audit"]),
73429
+ inArray47(runs.status, SURFACEABLE_STATUSES),
73430
+ notProbeRun()
73431
+ ];
73432
+ }
73306
73433
  function siteAuditRunPhase(status, attempt, layoutState) {
73307
73434
  switch (status) {
73308
73435
  case RunStatuses.queued:
@@ -73714,11 +73841,7 @@ async function technicalAeoRoutes(app, opts) {
73714
73841
  };
73715
73842
  const resolveAudit = (projectId, runId, recordedBefore) => {
73716
73843
  const filters = [
73717
- eq106(siteAuditSnapshots.projectId, projectId),
73718
- eq106(runs.projectId, projectId),
73719
- eq106(runs.kind, RunKinds["site-audit"]),
73720
- inArray47(runs.status, SURFACEABLE_STATUSES),
73721
- notProbeRun(),
73844
+ ...surfaceableSiteAuditFilters(projectId),
73722
73845
  ...recordedBefore ? [lt16(siteAuditSnapshots.createdAt, recordedBefore)] : []
73723
73846
  ];
73724
73847
  const complete = sql41`case when ${runs.status} = ${RunStatuses.completed} then 1 else 0 end`;
@@ -74006,13 +74129,7 @@ async function technicalAeoRoutes(app, opts) {
74006
74129
  auditedAt: siteAuditSnapshots.auditedAt,
74007
74130
  aggregateScore: siteAuditSnapshots.aggregateScore,
74008
74131
  pagesAudited: siteAuditSnapshots.pagesAudited
74009
- }).from(siteAuditSnapshots).innerJoin(runs, eq106(siteAuditSnapshots.runId, runs.id)).where(and89(
74010
- eq106(siteAuditSnapshots.projectId, project.id),
74011
- eq106(runs.projectId, project.id),
74012
- eq106(runs.kind, RunKinds["site-audit"]),
74013
- inArray47(runs.status, SURFACEABLE_STATUSES),
74014
- notProbeRun()
74015
- )).orderBy(desc48(siteAuditSnapshots.createdAt)).limit(limit).all();
74132
+ }).from(siteAuditSnapshots).innerJoin(runs, eq106(siteAuditSnapshots.runId, runs.id)).where(and89(...surfaceableSiteAuditFilters(project.id))).orderBy(desc48(siteAuditSnapshots.createdAt)).limit(limit).all();
74016
74133
  return { project: project.name, points: rows.reverse() };
74017
74134
  });
74018
74135
  app.get("/projects/:name/technical-aeo/crawl", async (request) => {
@@ -74748,8 +74865,8 @@ async function technicalAeoRoutes(app, opts) {
74748
74865
  const internalPreviousJoin = entity === "link" ? sql41`AND previous.internal = 1` : sql41``;
74749
74866
  const internalPrevious = entity === "link" ? sql41`AND previous.internal = 1` : sql41``;
74750
74867
  const internalCurrentJoin = entity === "link" ? sql41`AND current.internal = 1` : sql41``;
74751
- const difference2 = entity === "page" ? PAGE_CHANGE_PREDICATE : LINK_CHANGE_PREDICATE;
74752
- const afterKind = changeFilter === "added" ? sql41`previous.id IS NULL` : changeFilter === "changed" ? sql41`previous.id IS NOT NULL AND ${difference2}` : sql41`previous.id IS NULL OR (previous.id IS NOT NULL AND ${difference2})`;
74868
+ const difference3 = entity === "page" ? PAGE_CHANGE_PREDICATE : LINK_CHANGE_PREDICATE;
74869
+ const afterKind = changeFilter === "added" ? sql41`previous.id IS NULL` : changeFilter === "changed" ? sql41`previous.id IS NOT NULL AND ${difference3}` : sql41`previous.id IS NULL OR (previous.id IS NOT NULL AND ${difference3})`;
74753
74870
  const afterRows = includeAfter ? app.db.all(sql41`
74754
74871
  SELECT ${entity} AS entity,
74755
74872
  CASE WHEN previous.id IS NULL THEN 'added' ELSE 'changed' END AS change,
@@ -75553,10 +75670,119 @@ async function technicalAeoRoutes(app, opts) {
75553
75670
  });
75554
75671
  }
75555
75672
 
75673
+ // ../api-routes/src/trends.ts
75674
+ import { and as and90, asc as asc25, eq as eq107 } from "drizzle-orm";
75675
+ function parseSurface(value) {
75676
+ const parsed = trendSurfaceSchema.safeParse(value);
75677
+ if (!parsed.success) {
75678
+ throw validationError(`Invalid surface "${value ?? ""}". Must be one of: ${trendSurfaceSchema.options.join(", ")}.`, {
75679
+ field: "surface",
75680
+ value: value ?? null,
75681
+ allowed: [...trendSurfaceSchema.options]
75682
+ });
75683
+ }
75684
+ return parsed.data;
75685
+ }
75686
+ function parseDate(field, value) {
75687
+ if (value === void 0) return void 0;
75688
+ if (!isCalendarDate(value)) throw validationError(`Invalid ${field} "${value}". Use a calendar date, YYYY-MM-DD.`, { field, value });
75689
+ return value;
75690
+ }
75691
+ function parseSelection2(query, surface) {
75692
+ const startDate = parseDate("startDate", query.startDate);
75693
+ const endDate = parseDate("endDate", query.endDate);
75694
+ if (startDate === void 0 && endDate === void 0) {
75695
+ return parseWindowIn(query.window, WINDOW_SETS.metrics, { fallback: TREND_SURFACE_DEFAULT_WINDOW[surface] });
75696
+ }
75697
+ if (query.window !== void 0) {
75698
+ throw validationError("Pass window or a startDate/endDate range, not both.", { field: "window", value: query.window });
75699
+ }
75700
+ if (startDate && endDate && startDate > endDate) {
75701
+ throw validationError(`startDate ${startDate} is after endDate ${endDate}.`, { field: "startDate", value: startDate });
75702
+ }
75703
+ return { ...startDate ? { startDate } : {}, ...endDate ? { endDate } : {} };
75704
+ }
75705
+ async function trendsRoutes(app) {
75706
+ app.get("/projects/:name/trends", async (request) => {
75707
+ const project = resolveProject(app.db, request.params.name);
75708
+ const surface = parseSurface(request.query.surface);
75709
+ const selection = parseSelection2(request.query, surface);
75710
+ const groups = readSurface(app, project.id, surface).filter((group) => group.figures.some((figure) => figure.readings.some((reading) => reading.value !== null)));
75711
+ const span = trendWindowSpan(groups.flatMap((group) => group.figures.flatMap((figure) => figure.readings.filter((reading) => reading.value !== null).map((reading) => reading.date))), selection);
75712
+ const built = groups.map((group) => ({
75713
+ key: group.key,
75714
+ label: group.label,
75715
+ figures: group.figures.map((figure) => buildLevelTrendFigure({ ...figure, span }))
75716
+ }));
75717
+ return {
75718
+ project: project.name,
75719
+ surface,
75720
+ window: typeof selection === "string" ? selection : null,
75721
+ startDate: span?.startDate ?? null,
75722
+ endDate: span?.endDate ?? null,
75723
+ groups: built
75724
+ };
75725
+ });
75726
+ }
75727
+ function readSurface(app, projectId, surface) {
75728
+ switch (surface) {
75729
+ case TrendSurfaces["index-coverage"]: {
75730
+ const google = app.db.select({ date: gscCoverageSnapshots.date, indexed: gscCoverageSnapshots.indexed, notIndexed: gscCoverageSnapshots.notIndexed }).from(gscCoverageSnapshots).where(eq107(gscCoverageSnapshots.projectId, projectId)).orderBy(asc25(gscCoverageSnapshots.date)).all();
75731
+ const bing = app.db.select({ date: bingCoverageSnapshots.date, indexed: bingCoverageSnapshots.indexed, notIndexed: bingCoverageSnapshots.notIndexed }).from(bingCoverageSnapshots).where(eq107(bingCoverageSnapshots.projectId, projectId)).orderBy(asc25(bingCoverageSnapshots.date)).all();
75732
+ const coverage = (key, label, rows) => ({
75733
+ key,
75734
+ label,
75735
+ figures: [
75736
+ { key: "indexed", label: "Indexed", unit: TrendUnits.count, better: TrendBetter.higher, readings: rows.map((row) => ({ date: row.date, value: row.indexed })) },
75737
+ { key: "not-indexed", label: "Not indexed", unit: TrendUnits.count, better: TrendBetter.lower, readings: rows.map((row) => ({ date: row.date, value: row.notIndexed })) }
75738
+ ]
75739
+ });
75740
+ return [coverage("google", "Google", google), coverage("bing", "Bing", bing)];
75741
+ }
75742
+ case TrendSurfaces["site-health"]: {
75743
+ const rows = app.db.select({ auditedAt: siteAuditSnapshots.auditedAt, aggregateScore: siteAuditSnapshots.aggregateScore, pagesAudited: siteAuditSnapshots.pagesAudited }).from(siteAuditSnapshots).innerJoin(runs, eq107(siteAuditSnapshots.runId, runs.id)).where(and90(...surfaceableSiteAuditFilters(projectId))).orderBy(asc25(siteAuditSnapshots.auditedAt)).all();
75744
+ return [{
75745
+ key: "site-health",
75746
+ label: "Site Health",
75747
+ figures: [
75748
+ { key: "score", label: "Site score", unit: TrendUnits.score, better: TrendBetter.higher, readings: rows.map((row) => ({ date: row.auditedAt, observedAt: row.auditedAt, value: row.aggregateScore })) },
75749
+ { key: "pages-checked", label: "Pages checked", unit: TrendUnits.count, better: TrendBetter.neither, readings: rows.map((row) => ({ date: row.auditedAt, observedAt: row.auditedAt, value: row.pagesAudited })) }
75750
+ ]
75751
+ }];
75752
+ }
75753
+ case TrendSurfaces.backlinks: {
75754
+ const totals = computeFilteredReleaseTotals(app.db, projectId, BacklinkSources.commoncrawl);
75755
+ const rows = app.db.select({ release: backlinkSummaries.release, totalLinkingDomains: backlinkSummaries.totalLinkingDomains, queriedAt: backlinkSummaries.queriedAt }).from(backlinkSummaries).where(and90(eq107(backlinkSummaries.projectId, projectId), eq107(backlinkSummaries.source, BacklinkSources.commoncrawl))).all().sort((a, b) => compareReleaseIds(a.release, b.release)).map((row) => {
75756
+ const filtered = totals.get(row.release);
75757
+ const known = (filtered?.domainRows ?? 0) > 0 || row.totalLinkingDomains === 0;
75758
+ const windowEnd = releaseWindowEndMonth(row.release);
75759
+ return {
75760
+ date: windowEnd ?? row.queriedAt,
75761
+ observedAt: windowEnd ? null : row.queriedAt,
75762
+ label: formatReleaseWindow(row.release) ?? row.release,
75763
+ linkingDomains: known ? filtered?.linkingDomains ?? 0 : null,
75764
+ hosts: known ? filtered?.hosts ?? 0 : null,
75765
+ top10HostsShare: known ? filtered?.top10HostsShare ?? 0 : null
75766
+ };
75767
+ });
75768
+ const readings = (value) => rows.map((row) => ({ date: row.date, observedAt: row.observedAt, label: row.label, value: value(row) }));
75769
+ return [{
75770
+ key: "backlinks",
75771
+ label: "Backlinks",
75772
+ figures: [
75773
+ { key: "referring-domains", label: "Referring domains", unit: TrendUnits.count, better: TrendBetter.higher, readings: readings((row) => row.linkingDomains) },
75774
+ { key: "hosts", label: "Total linking hosts", unit: TrendUnits.count, better: TrendBetter.higher, readings: readings((row) => row.hosts) },
75775
+ { key: "top-10-concentration", label: "Top-10 concentration", unit: TrendUnits.fraction, better: TrendBetter.neither, readings: readings((row) => row.top10HostsShare) }
75776
+ ]
75777
+ }];
75778
+ }
75779
+ }
75780
+ }
75781
+
75556
75782
  // ../api-routes/src/research.ts
75557
75783
  import crypto59 from "crypto";
75558
75784
  import { z as z7 } from "zod";
75559
- import { and as and90, count as count9, desc as desc49, eq as eq107, gte as gte31, lt as lt17, or as or22, sql as sql42 } from "drizzle-orm";
75785
+ import { and as and91, count as count9, desc as desc49, eq as eq108, gte as gte31, lt as lt17, or as or22, sql as sql42 } from "drizzle-orm";
75560
75786
  var sameLocation = (a, b) => a.label === b.label && a.city === b.city && a.region === b.region && a.country === b.country && a.timezone === b.timezone;
75561
75787
  var BATCH_RECEIPT_PREFIX = "__canonry_research_batch__:";
75562
75788
  async function researchRoutes(app, opts) {
@@ -75575,7 +75801,7 @@ async function researchRoutes(app, opts) {
75575
75801
  const input = parsed.data;
75576
75802
  if (input.idempotencyKey?.startsWith(BATCH_RECEIPT_PREFIX)) throw validationError("This idempotency key prefix is reserved for internal research batch receipts.");
75577
75803
  const requestHash2 = directResearchRequestHash(input);
75578
- const existingReceipt = input.idempotencyKey ? app.db.select().from(researchRuns).where(and90(eq107(researchRuns.projectId, project.id), eq107(researchRuns.idempotencyKey, input.idempotencyKey))).get() : void 0;
75804
+ const existingReceipt = input.idempotencyKey ? app.db.select().from(researchRuns).where(and91(eq108(researchRuns.projectId, project.id), eq108(researchRuns.idempotencyKey, input.idempotencyKey))).get() : void 0;
75579
75805
  if (existingReceipt) {
75580
75806
  assertSameDirectResearchRequest(existingReceipt, input, requestHash2);
75581
75807
  const result2 = getDetail(app, project.id, existingReceipt.id);
@@ -75610,7 +75836,7 @@ async function researchRoutes(app, opts) {
75610
75836
  const initiatedBy = researchPrincipal(request);
75611
75837
  const decision = app.db.transaction((tx) => {
75612
75838
  if (input.idempotencyKey) {
75613
- const existing = tx.select().from(researchRuns).where(and90(eq107(researchRuns.projectId, project.id), eq107(researchRuns.idempotencyKey, input.idempotencyKey))).get();
75839
+ const existing = tx.select().from(researchRuns).where(and91(eq108(researchRuns.projectId, project.id), eq108(researchRuns.idempotencyKey, input.idempotencyKey))).get();
75614
75840
  if (existing) {
75615
75841
  assertSameDirectResearchRequest(existing, input, requestHash2);
75616
75842
  return { reused: true, id: existing.id, shouldDispatch: existing.status === ResearchRunStatuses.queued };
@@ -75618,8 +75844,8 @@ async function researchRoutes(app, opts) {
75618
75844
  }
75619
75845
  if (initiatedBy?.limited) {
75620
75846
  const { start, end, date } = utcDayBounds(now);
75621
- const used = tx.select({ value: count9() }).from(researchRuns).where(and90(
75622
- eq107(researchRuns.projectId, project.id),
75847
+ const used = tx.select({ value: count9() }).from(researchRuns).where(and91(
75848
+ eq108(researchRuns.projectId, project.id),
75623
75849
  gte31(researchRuns.createdAt, start),
75624
75850
  lt17(researchRuns.createdAt, end),
75625
75851
  sql42`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
@@ -75650,15 +75876,15 @@ async function researchRoutes(app, opts) {
75650
75876
  if (input.idempotencyKey.startsWith(BATCH_RECEIPT_PREFIX)) throw validationError("This idempotency key prefix is reserved for internal research batch receipts.");
75651
75877
  const requestHash2 = researchBatchRequestHash(input);
75652
75878
  const receiptKeys = input.runs.map((_run, index) => batchChildReceiptKey(input.idempotencyKey, index));
75653
- const firstReceipt = app.db.select().from(researchRuns).where(and90(
75654
- eq107(researchRuns.projectId, project.id),
75655
- eq107(researchRuns.idempotencyKey, receiptKeys[0])
75879
+ const firstReceipt = app.db.select().from(researchRuns).where(and91(
75880
+ eq108(researchRuns.projectId, project.id),
75881
+ eq108(researchRuns.idempotencyKey, receiptKeys[0])
75656
75882
  )).get();
75657
75883
  if (firstReceipt) {
75658
75884
  if (firstReceipt.requestHash !== requestHash2) throw alreadyExists("Research batch idempotency key", input.idempotencyKey);
75659
- const saved = receiptKeys.map((key) => app.db.select().from(researchRuns).where(and90(
75660
- eq107(researchRuns.projectId, project.id),
75661
- eq107(researchRuns.idempotencyKey, key)
75885
+ const saved = receiptKeys.map((key) => app.db.select().from(researchRuns).where(and91(
75886
+ eq108(researchRuns.projectId, project.id),
75887
+ eq108(researchRuns.idempotencyKey, key)
75662
75888
  )).get());
75663
75889
  if (saved.some((row) => !row || row.requestHash !== requestHash2)) throw alreadyExists("Research batch idempotency key", input.idempotencyKey);
75664
75890
  const runs3 = saved.map((row) => getDetail(app, project.id, row.id));
@@ -75674,8 +75900,8 @@ async function researchRoutes(app, opts) {
75674
75900
  const decision = app.db.transaction((tx) => {
75675
75901
  if (initiatedBy?.limited) {
75676
75902
  const { start, end, date } = utcDayBounds(now);
75677
- const used = tx.select({ value: count9() }).from(researchRuns).where(and90(
75678
- eq107(researchRuns.projectId, project.id),
75903
+ const used = tx.select({ value: count9() }).from(researchRuns).where(and91(
75904
+ eq108(researchRuns.projectId, project.id),
75679
75905
  gte31(researchRuns.createdAt, start),
75680
75906
  lt17(researchRuns.createdAt, end),
75681
75907
  sql42`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
@@ -75729,9 +75955,9 @@ async function researchRoutes(app, opts) {
75729
75955
  const requested = Number.parseInt(request.query.limit ?? "", 10);
75730
75956
  const limit = Number.isInteger(requested) && requested > 0 ? Math.min(requested, 100) : 20;
75731
75957
  const cursor = request.query.cursor ? parseResearchCursor(request.query.cursor, project.id) : null;
75732
- const rows = app.db.select().from(researchRuns).where(and90(
75733
- eq107(researchRuns.projectId, project.id),
75734
- cursor ? or22(lt17(researchRuns.createdAt, cursor.createdAt), and90(eq107(researchRuns.createdAt, cursor.createdAt), lt17(researchRuns.id, cursor.id))) : void 0
75958
+ const rows = app.db.select().from(researchRuns).where(and91(
75959
+ eq108(researchRuns.projectId, project.id),
75960
+ cursor ? or22(lt17(researchRuns.createdAt, cursor.createdAt), and91(eq108(researchRuns.createdAt, cursor.createdAt), lt17(researchRuns.id, cursor.id))) : void 0
75735
75961
  )).orderBy(desc49(researchRuns.createdAt), desc49(researchRuns.id)).limit(limit + 1).all();
75736
75962
  const runs2 = rows.slice(0, limit).map(serializeRun);
75737
75963
  const last = runs2.at(-1);
@@ -75881,9 +76107,9 @@ function resolveStoredResearchTemplate(stored, selection, idempotencyKey) {
75881
76107
  return stored;
75882
76108
  }
75883
76109
  function resolveResearchTemplate(app, projectId, selection, scope, location) {
75884
- const savedTemplate = app.db.select().from(measurementQueryTemplates).where(and90(
75885
- eq107(measurementQueryTemplates.projectId, projectId),
75886
- eq107(measurementQueryTemplates.id, selection.templateId)
76110
+ const savedTemplate = app.db.select().from(measurementQueryTemplates).where(and91(
76111
+ eq108(measurementQueryTemplates.projectId, projectId),
76112
+ eq108(measurementQueryTemplates.id, selection.templateId)
75887
76113
  )).get();
75888
76114
  if (!savedTemplate) throw validationError("Unknown or stale research template. Reload the selected template and try again.");
75889
76115
  const templateVersion = savedTemplate.updatedAt;
@@ -75892,7 +76118,7 @@ function resolveResearchTemplate(app, projectId, selection, scope, location) {
75892
76118
  }
75893
76119
  const bindingLocation = selection.bindingLocation === void 0 ? location : selection.bindingLocation;
75894
76120
  if (bindingLocation) {
75895
- const project = app.db.select({ locations: projects.locations }).from(projects).where(eq107(projects.id, projectId)).get();
76121
+ const project = app.db.select({ locations: projects.locations }).from(projects).where(eq108(projects.id, projectId)).get();
75896
76122
  if (!project.locations.some((item2) => sameLocation(item2, bindingLocation))) throw validationError("Research template binding location must match a configured project location.");
75897
76123
  }
75898
76124
  const { bindings, output } = expandResearchTemplate(savedTemplate, scope, bindingLocation);
@@ -75905,9 +76131,9 @@ function classifyResearchQueries(project, plan, queries2) {
75905
76131
  return queries2.map((query) => classifier?.classify(query) ?? null);
75906
76132
  }
75907
76133
  function getDetail(app, projectId, id) {
75908
- const row = app.db.select().from(researchRuns).where(and90(eq107(researchRuns.id, id), eq107(researchRuns.projectId, projectId))).get();
76134
+ const row = app.db.select().from(researchRuns).where(and91(eq108(researchRuns.id, id), eq108(researchRuns.projectId, projectId))).get();
75909
76135
  if (!row) throw notFound("Research run", id);
75910
- const queries2 = app.db.select().from(researchRunQueries).where(eq107(researchRunQueries.researchRunId, id)).orderBy(researchRunQueries.position).all().map(serializeQuery);
76136
+ const queries2 = app.db.select().from(researchRunQueries).where(eq108(researchRunQueries.researchRunId, id)).orderBy(researchRunQueries.position).all().map(serializeQuery);
75911
76137
  return { ...serializeRun(row), queries: queries2 };
75912
76138
  }
75913
76139
  function serializeRun(row) {
@@ -75962,7 +76188,7 @@ function utcDayBounds(now) {
75962
76188
 
75963
76189
  // ../api-routes/src/simple-measurement-definitions.ts
75964
76190
  import crypto60 from "crypto";
75965
- import { and as and91, eq as eq108 } from "drizzle-orm";
76191
+ import { and as and92, eq as eq109 } from "drizzle-orm";
75966
76192
  function captureSimpleMeasurementDefinition(db, input) {
75967
76193
  return db.transaction((tx) => {
75968
76194
  const run = tx.select({
@@ -75970,7 +76196,7 @@ function captureSimpleMeasurementDefinition(db, input) {
75970
76196
  trigger: runs.trigger,
75971
76197
  status: runs.status,
75972
76198
  measurementPlanVersionId: runs.measurementPlanVersionId
75973
- }).from(runs).where(and91(eq108(runs.projectId, input.projectId), eq108(runs.id, input.runId))).get();
76199
+ }).from(runs).where(and92(eq109(runs.projectId, input.projectId), eq109(runs.id, input.runId))).get();
75974
76200
  if (!run) throw notFound("Run", input.runId);
75975
76201
  if (run.kind !== RunKinds["answer-visibility"] || run.trigger === RunTriggers.probe || run.measurementPlanVersionId !== null) {
75976
76202
  return null;
@@ -75991,7 +76217,7 @@ function captureSimpleMeasurementDefinition(db, input) {
75991
76217
  throw validationError("Qualified aliases must be a sorted subset of the captured aliases.");
75992
76218
  }
75993
76219
  }
75994
- const existing = tx.select().from(simpleMeasurementDefinitions).where(and91(eq108(simpleMeasurementDefinitions.projectId, input.projectId), eq108(simpleMeasurementDefinitions.runId, input.runId))).get();
76220
+ const existing = tx.select().from(simpleMeasurementDefinitions).where(and92(eq109(simpleMeasurementDefinitions.projectId, input.projectId), eq109(simpleMeasurementDefinitions.runId, input.runId))).get();
75995
76221
  if (existing) {
75996
76222
  const frozen = simpleMeasurementDefinitionSchema.parse(existing.definition);
75997
76223
  const { qualifiedAliases: _liveQualifiedAliases, ...liveIdentity } = definition2.identity;
@@ -76017,15 +76243,15 @@ function captureSimpleMeasurementDefinition(db, input) {
76017
76243
  if (run.status !== RunStatuses.running) {
76018
76244
  throw validationError("Only a running run can capture a new measurement definition.");
76019
76245
  }
76020
- if (tx.select({ id: querySnapshots.id }).from(querySnapshots).where(eq108(querySnapshots.runId, input.runId)).limit(1).get()) {
76246
+ if (tx.select({ id: querySnapshots.id }).from(querySnapshots).where(eq109(querySnapshots.runId, input.runId)).limit(1).get()) {
76021
76247
  throw validationError("This run already has stored answers. Its measurement definition cannot be inferred afterward.");
76022
76248
  }
76023
- const projectQueryIds = new Set(tx.select({ id: queries.id }).from(queries).where(eq108(queries.projectId, input.projectId)).all().map((query) => query.id));
76249
+ const projectQueryIds = new Set(tx.select({ id: queries.id }).from(queries).where(eq109(queries.projectId, input.projectId)).all().map((query) => query.id));
76024
76250
  if (definition2.queries.some((query) => !projectQueryIds.has(query.queryId))) {
76025
76251
  throw validationError("Every captured query must belong to the run project.");
76026
76252
  }
76027
76253
  if (definition2.competitors !== void 0) {
76028
- const liveCompetitorDomains = new Set(tx.select({ domain: competitors.domain }).from(competitors).where(eq108(competitors.projectId, input.projectId)).orderBy(competitors.domain).all().map((competitor) => competitor.domain));
76254
+ const liveCompetitorDomains = new Set(tx.select({ domain: competitors.domain }).from(competitors).where(eq109(competitors.projectId, input.projectId)).orderBy(competitors.domain).all().map((competitor) => competitor.domain));
76029
76255
  const capturedDomains = new Set(definition2.competitors.map((competitor) => competitor.domain));
76030
76256
  if (liveCompetitorDomains.size !== capturedDomains.size || [...liveCompetitorDomains].some((domain) => !capturedDomains.has(domain))) {
76031
76257
  throw validationError("Captured competitors must exactly match the project competitors dispatched for this run.");
@@ -76368,6 +76594,7 @@ async function apiRoutes(app, opts) {
76368
76594
  getCloudflareQueueRetentionSeconds: opts.getCloudflareQueueRetentionSeconds,
76369
76595
  cloudflareQueueMaxBatches: opts.cloudflareQueueMaxBatches,
76370
76596
  cloudflareQueueSyncDeadlineMs: opts.cloudflareQueueSyncDeadlineMs,
76597
+ cloudflareQueueEmptyPullBackoffMs: opts.cloudflareQueueEmptyPullBackoffMs,
76371
76598
  cloudflareTrafficIngestUrl: opts.cloudflareTrafficIngestUrl,
76372
76599
  cloudflareIngestRateLimitMax: opts.cloudflareIngestRateLimitMax,
76373
76600
  cloudflareIngestIpRateLimitMax: opts.cloudflareIngestIpRateLimitMax,
@@ -76407,6 +76634,7 @@ async function apiRoutes(app, opts) {
76407
76634
  await api.register(technicalAeoRoutes, {
76408
76635
  onSiteAuditRequested: opts.onSiteAuditRequested
76409
76636
  });
76637
+ await api.register(trendsRoutes);
76410
76638
  await api.register(doctorRoutes, {
76411
76639
  googleConnectionStore: opts.googleConnectionStore,
76412
76640
  bingConnectionStore: opts.bingConnectionStore,
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  createApiClient
3
- } from "./chunk-VBW33U4E.js";
4
- import "./chunk-U3QNTJBP.js";
3
+ } from "./chunk-TEQF4ZJA.js";
4
+ import "./chunk-2FJB3CSS.js";
5
5
  import "./chunk-AOJU5LKX.js";
6
6
  import "./chunk-HGMYEDAQ.js";
7
- import "./chunk-KUVTXIFX.js";
7
+ import "./chunk-F643WELJ.js";
8
8
  import "./chunk-MLKGABMK.js";
9
9
 
10
10
  // src/commands/citations.ts