@canonry/canonry 7.86.0 → 7.87.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 (142) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +19 -0
  2. package/assets/assets/{AgentSetupPage-0f7Ca4IZ.js → AgentSetupPage-_ZP3F_zA.js} +4 -4
  3. package/assets/assets/{AuditHistoryPanel-BAQuFTNl.js → AuditHistoryPanel-9B0XpYsN.js} +1 -1
  4. package/assets/assets/{BacklinksPage-BK2bn9Xg.js → BacklinksPage-CsvVgXTZ.js} +1 -1
  5. package/assets/assets/{ChartPrimitives-B5L9FUj3.js → ChartPrimitives-4D5wgAn5.js} +1 -1
  6. package/assets/assets/{HistoryPage-ZBfL9nGw.js → HistoryPage-BCxpCul0.js} +1 -1
  7. package/assets/assets/{MeasurementPropertyPage-DDSOq4aU.js → MeasurementPropertyPage-BObvWjVx.js} +4 -4
  8. package/assets/assets/ProjectPage-y378tOaT.js +11 -0
  9. package/assets/assets/{RunsPage-ke0wMYLg.js → RunsPage-CfQ_kqlu.js} +1 -1
  10. package/assets/assets/{SettingsPage-Bbi-sTBk.js → SettingsPage-PIgRvqJc.js} +1 -1
  11. package/assets/assets/SiteHealthSection-DuUE1cB0.js +2 -0
  12. package/assets/assets/ToneBadge-BgTLSTKp.js +60 -0
  13. package/assets/assets/{TrafficPage-nXixENx9.js → TrafficPage-B_YunwjH.js} +1 -1
  14. package/assets/assets/TrafficSourceDetailPage-C51W87fU.js +1 -0
  15. package/assets/assets/{account-context-BmN431xn.js → account-context-CVjb3ZSn.js} +1 -1
  16. package/assets/assets/agent-trust-copy-BlpC8jo5.js +1 -0
  17. package/assets/assets/{card-DUTMWeHB.js → card-BnVez9nG.js} +1 -1
  18. package/assets/assets/{extract-error-message-BFaIHLqb.js → extract-error-message-Dqirbrl0.js} +1 -1
  19. package/assets/assets/{format-helpers-D1w2yznw.js → format-helpers-DVXZD864.js} +1 -1
  20. package/assets/assets/{index-DIHdWY7b.css → index-BOUrrC9J.css} +1 -1
  21. package/assets/assets/index-CsvEqhK4.js +60 -0
  22. package/assets/assets/query-client-CVc7wMKJ.js +1 -0
  23. package/assets/assets/react-query-CSlTSOjz.js +1 -0
  24. package/assets/assets/{react-sigma_core.esm.min-cvMDhRNS.js → react-sigma_core.esm.min-CVfGIEIE.js} +3 -3
  25. package/assets/assets/{service-CvCKbPnf.js → service-BGqeyqhZ.js} +1 -1
  26. package/assets/index.html +9 -9
  27. package/dist/{ads-7IJTHW6T.js → ads-ZUZWSIBV.js} +3 -3
  28. package/dist/{agent-4UC2X7O2.js → agent-YDS3WY7B.js} +7 -7
  29. package/dist/{agent-ask-SVPU2WQU.js → agent-ask-7VUPXTXL.js} +3 -3
  30. package/dist/{agent-conversations-PJQRY5RX.js → agent-conversations-WPEBK3BS.js} +3 -3
  31. package/dist/{agent-journeys-JOD5DQ6G.js → agent-journeys-H5CCYLNB.js} +3 -3
  32. package/dist/{agent-memory-KTMZMUYF.js → agent-memory-CNPKPIWD.js} +3 -3
  33. package/dist/{agent-providers-VEBB3WEW.js → agent-providers-U7FLJO5I.js} +3 -3
  34. package/dist/{agent-transcript-RRZJCMK2.js → agent-transcript-VY3BQNPU.js} +3 -3
  35. package/dist/{analytics-EZV4XJKR.js → analytics-PYUNO7C2.js} +3 -3
  36. package/dist/{apply-NP7ORQS4.js → apply-YNBEYC2G.js} +3 -3
  37. package/dist/{backfill-7T6VFPWT.js → backfill-LIMS34H6.js} +10 -10
  38. package/dist/{backlinks-KTEJM2IP.js → backlinks-LVHZ2MQ7.js} +3 -3
  39. package/dist/{bing-TC54HJUB.js → bing-A2MUG6AV.js} +3 -3
  40. package/dist/{bootstrap-UDSN22NT.js → bootstrap-AZYQXBHN.js} +10 -10
  41. package/dist/{cdp-IZSHF3MS.js → cdp-HCYY4SXR.js} +7 -7
  42. package/dist/{chunk-IILPHGRC.js → chunk-2AXFKYJU.js} +2 -2
  43. package/dist/{chunk-U3QNTJBP.js → chunk-2FJB3CSS.js} +1 -1
  44. package/dist/{chunk-KEIKD6JQ.js → chunk-3KK6FWDF.js} +1 -1
  45. package/dist/{chunk-STHWLBYT.js → chunk-4FWDITQG.js} +1 -1
  46. package/dist/{chunk-3K3QBKNC.js → chunk-4LNZTL7M.js} +3 -3
  47. package/dist/{chunk-Z5LJI5XK.js → chunk-6F5ULQDY.js} +4 -4
  48. package/dist/{chunk-OXI45LZP.js → chunk-BJHHD7VE.js} +2 -2
  49. package/dist/{chunk-EX6GVZAE.js → chunk-CBWGUGME.js} +2 -1
  50. package/dist/{chunk-LJ6VNMCY.js → chunk-D5V6YQXA.js} +7 -7
  51. package/dist/{chunk-CBNFWSWU.js → chunk-DOPUEXMC.js} +2 -2
  52. package/dist/{chunk-UPFWF6TF.js → chunk-EJZHL67Y.js} +4 -4
  53. package/dist/{chunk-DU4QEC65.js → chunk-EQGCGZZY.js} +261 -50
  54. package/dist/{chunk-KUVTXIFX.js → chunk-F643WELJ.js} +1913 -1743
  55. package/dist/{chunk-5WEGX3H7.js → chunk-FAMCDCVK.js} +1 -1
  56. package/dist/{chunk-5ZAQVRFL.js → chunk-G44S34CE.js} +2 -2
  57. package/dist/{chunk-UTG6UUGK.js → chunk-GBWCEOZR.js} +1 -1
  58. package/dist/{chunk-E6IZVSRY.js → chunk-GDU6IMGB.js} +2 -2
  59. package/dist/{chunk-RR76O744.js → chunk-GXTCDNX7.js} +1 -1
  60. package/dist/{chunk-JAK37YKH.js → chunk-IBGZ6D6R.js} +2 -2
  61. package/dist/{chunk-WVMWY4SE.js → chunk-ICS2WJ7H.js} +3 -3
  62. package/dist/{chunk-2J5EUXO2.js → chunk-IK7QEKKD.js} +1 -1
  63. package/dist/{chunk-X3EU4UPS.js → chunk-INH773FB.js} +3 -3
  64. package/dist/{chunk-HHMOUS26.js → chunk-ITOK75B6.js} +5 -5
  65. package/dist/{chunk-BLGYC2K7.js → chunk-JAYUWQAV.js} +1 -1
  66. package/dist/{chunk-GO4RP6XE.js → chunk-JDTKG7KJ.js} +7 -3
  67. package/dist/{chunk-ZFL5DOGJ.js → chunk-JGGDENAA.js} +395 -342
  68. package/dist/{chunk-RI2ZT3OR.js → chunk-KOBOKZXF.js} +6 -6
  69. package/dist/{chunk-4EKPEVJ2.js → chunk-L5NMS7AU.js} +5 -5
  70. package/dist/{chunk-F5SQITTJ.js → chunk-MNLAIB67.js} +1 -1
  71. package/dist/{chunk-SUSQEH2D.js → chunk-O26FQ7RI.js} +1 -1
  72. package/dist/{chunk-TTXBQCE6.js → chunk-OJKWT6U6.js} +21 -21
  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-MJE3SGFW.js → chunk-VDYXB6SV.js} +1 -1
  76. package/dist/{chunk-5KHF5FV5.js → chunk-YKB67YKH.js} +4 -4
  77. package/dist/{citations-TOZK6OFP.js → citations-3EZTHA65.js} +3 -3
  78. package/dist/{cli-catalog-LNWBJNOD.js → cli-catalog-CBTM3JKP.js} +3 -3
  79. package/dist/{cli-commands-3HX4JSDX.js → cli-commands-J6KWSWIS.js} +7 -7
  80. package/dist/cli.js +13 -13
  81. package/dist/{competitor-54HLL354.js → competitor-VH4TXJLT.js} +3 -3
  82. package/dist/{content-ZIGSMH5U.js → content-TSMNHXJZ.js} +3 -3
  83. package/dist/{dashboard-YJH6IQGK.js → dashboard-5QMYF4ZP.js} +6 -6
  84. package/dist/{demo-ZYK5XCP3.js → demo-KGQL5EJM.js} +1 -1
  85. package/dist/{demo-server-LZ6MWJ2S.js → demo-server-ERIR2T2N.js} +13 -12
  86. package/dist/{discover-22QKOFIM.js → discover-XTJXTKCI.js} +3 -3
  87. package/dist/{discover-eval-6IFCCH2S.js → discover-eval-IVQI5YEW.js} +3 -3
  88. package/dist/{doctor-WLLBFOJU.js → doctor-SJ4SHSQO.js} +3 -3
  89. package/dist/{evidence-7VHTWNZW.js → evidence-C6T556YH.js} +3 -3
  90. package/dist/{export-cmd-5RZ3QDFH.js → export-cmd-JQZ5ZRSI.js} +3 -3
  91. package/dist/{ga-LPI3CWY4.js → ga-W2WHVRHN.js} +3 -3
  92. package/dist/{gbp-YBOMWR75.js → gbp-3AZIV5WF.js} +3 -3
  93. package/dist/{google-N42X5FBH.js → google-3CKWPZT7.js} +4 -4
  94. package/dist/{google-marketing-SO4B7Z7L.js → google-marketing-PJBGQYYW.js} +1 -1
  95. package/dist/{health-cmd-W7RQWCEB.js → health-cmd-4JTQMCXM.js} +3 -3
  96. package/dist/{history-SDRTOIDZ.js → history-TXUJFEO2.js} +3 -3
  97. package/dist/index.js +25 -25
  98. package/dist/{init-DPXOGP3P.js → init-HOBW5K2Q.js} +12 -12
  99. package/dist/{insights-636LLPVG.js → insights-XZQO4B45.js} +3 -3
  100. package/dist/intelligence-service-2AH42Y4Q.js +16 -0
  101. package/dist/{journeys-NIXKVYA4.js → journeys-VFIV5JYV.js} +3 -3
  102. package/dist/{keys-U2JR56TX.js → keys-PV23G4LW.js} +3 -3
  103. package/dist/{keyword-GLRK2ZHR.js → keyword-D76KBIV5.js} +3 -3
  104. package/dist/{logs-ENACGWGS.js → logs-DYDZ6IEJ.js} +3 -3
  105. package/dist/{mcp-DST2XTP2.js → mcp-GAR5LDQD.js} +4 -4
  106. package/dist/mcp.js +9 -9
  107. package/dist/{measurement-plan-Y3SA4ZQC.js → measurement-plan-K24EOGMI.js} +5 -5
  108. package/dist/{notify-ZYXJQUHU.js → notify-XQHMPDQQ.js} +3 -3
  109. package/dist/{organic-evidence-NSX7VCNW.js → organic-evidence-UHHE7AO4.js} +3 -3
  110. package/dist/{overview-UO6Z64IT.js → overview-AUZ3XRS5.js} +4 -4
  111. package/dist/{project-BHXUPHQA.js → project-FUCPU2BD.js} +3 -3
  112. package/dist/{query-GDRAKEN5.js → query-HRMSXDKL.js} +3 -3
  113. package/dist/{research-YIOQ7WVP.js → research-R23XLZJ3.js} +3 -3
  114. package/dist/{results-export-KXMCAEHJ.js → results-export-N6IV3KG4.js} +3 -3
  115. package/dist/{run-6PN5GOVE.js → run-CJLG5EXH.js} +4 -4
  116. package/dist/{schedule-5B67GLAM.js → schedule-IPDCQ2I5.js} +3 -3
  117. package/dist/{search-BMVPCN2V.js → search-ZU4AKJ76.js} +3 -3
  118. package/dist/{serve-HB7DI7OC.js → serve-WF5ZXWW6.js} +26 -26
  119. package/dist/{session-3RTDTW6Z.js → session-HEDEP77R.js} +13 -13
  120. package/dist/{settings-FJEYHYRH.js → settings-4FS5NRKP.js} +3 -3
  121. package/dist/{skills-6E7IUQUO.js → skills-GSFHACGN.js} +2 -2
  122. package/dist/{snapshot-BJQ7WPJZ.js → snapshot-4NUVPDK3.js} +3 -3
  123. package/dist/{src-QU75IDHI.js → src-SJYGA7NH.js} +2 -2
  124. package/dist/{status-3GHFPN54.js → status-ASBLMFP2.js} +4 -4
  125. package/dist/{technical-aeo-76JRC66W.js → technical-aeo-QXHRBGYY.js} +3 -3
  126. package/dist/{telemetry-KB73JBDZ.js → telemetry-J2I577I3.js} +4 -4
  127. package/dist/{tools-6VWDHNPZ.js → tools-KYG67FBW.js} +11 -11
  128. package/dist/{traffic-XB7MZJ23.js → traffic-B7Q4WPOV.js} +5 -5
  129. package/dist/trends-YOLCJBNM.js +56 -0
  130. package/dist/{users-HYBG3J72.js → users-LPSCZV3M.js} +3 -3
  131. package/dist/{visibility-stats-ZDB2LMRJ.js → visibility-stats-JJQTZSSK.js} +3 -3
  132. package/dist/{wordpress-SXKR6XNT.js → wordpress-4IKDT5KT.js} +3 -3
  133. package/package.json +1 -1
  134. package/assets/assets/ProjectPage-8cVgOm5o.js +0 -11
  135. package/assets/assets/SiteHealthSection-eKxTDUE2.js +0 -2
  136. package/assets/assets/ToneBadge-WAMyKadz.js +0 -60
  137. package/assets/assets/TrafficSourceDetailPage-ir5zJU_l.js +0 -1
  138. package/assets/assets/agent-trust-copy-zfx8m-cx.js +0 -1
  139. package/assets/assets/index-C5r0VqgO.js +0 -60
  140. package/assets/assets/query-client-DrIawr39.js +0 -1
  141. package/assets/assets/react-query-DQuOVjDi.js +0 -1
  142. package/dist/intelligence-service-JRVESWU2.js +0 -16
@@ -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;
@@ -73303,6 +73405,15 @@ var FETCHED_SITE_CRAWL_STATES = /* @__PURE__ */ new Set([
73303
73405
  "fetched"
73304
73406
  ]);
73305
73407
  var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
73408
+ function surfaceableSiteAuditFilters(projectId) {
73409
+ return [
73410
+ eq106(siteAuditSnapshots.projectId, projectId),
73411
+ eq106(runs.projectId, projectId),
73412
+ eq106(runs.kind, RunKinds["site-audit"]),
73413
+ inArray47(runs.status, SURFACEABLE_STATUSES),
73414
+ notProbeRun()
73415
+ ];
73416
+ }
73306
73417
  function siteAuditRunPhase(status, attempt, layoutState) {
73307
73418
  switch (status) {
73308
73419
  case RunStatuses.queued:
@@ -73714,11 +73825,7 @@ async function technicalAeoRoutes(app, opts) {
73714
73825
  };
73715
73826
  const resolveAudit = (projectId, runId, recordedBefore) => {
73716
73827
  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(),
73828
+ ...surfaceableSiteAuditFilters(projectId),
73722
73829
  ...recordedBefore ? [lt16(siteAuditSnapshots.createdAt, recordedBefore)] : []
73723
73830
  ];
73724
73831
  const complete = sql41`case when ${runs.status} = ${RunStatuses.completed} then 1 else 0 end`;
@@ -74006,13 +74113,7 @@ async function technicalAeoRoutes(app, opts) {
74006
74113
  auditedAt: siteAuditSnapshots.auditedAt,
74007
74114
  aggregateScore: siteAuditSnapshots.aggregateScore,
74008
74115
  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();
74116
+ }).from(siteAuditSnapshots).innerJoin(runs, eq106(siteAuditSnapshots.runId, runs.id)).where(and89(...surfaceableSiteAuditFilters(project.id))).orderBy(desc48(siteAuditSnapshots.createdAt)).limit(limit).all();
74016
74117
  return { project: project.name, points: rows.reverse() };
74017
74118
  });
74018
74119
  app.get("/projects/:name/technical-aeo/crawl", async (request) => {
@@ -74748,8 +74849,8 @@ async function technicalAeoRoutes(app, opts) {
74748
74849
  const internalPreviousJoin = entity === "link" ? sql41`AND previous.internal = 1` : sql41``;
74749
74850
  const internalPrevious = entity === "link" ? sql41`AND previous.internal = 1` : sql41``;
74750
74851
  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})`;
74852
+ const difference3 = entity === "page" ? PAGE_CHANGE_PREDICATE : LINK_CHANGE_PREDICATE;
74853
+ 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
74854
  const afterRows = includeAfter ? app.db.all(sql41`
74754
74855
  SELECT ${entity} AS entity,
74755
74856
  CASE WHEN previous.id IS NULL THEN 'added' ELSE 'changed' END AS change,
@@ -75553,10 +75654,119 @@ async function technicalAeoRoutes(app, opts) {
75553
75654
  });
75554
75655
  }
75555
75656
 
75657
+ // ../api-routes/src/trends.ts
75658
+ import { and as and90, asc as asc25, eq as eq107 } from "drizzle-orm";
75659
+ function parseSurface(value) {
75660
+ const parsed = trendSurfaceSchema.safeParse(value);
75661
+ if (!parsed.success) {
75662
+ throw validationError(`Invalid surface "${value ?? ""}". Must be one of: ${trendSurfaceSchema.options.join(", ")}.`, {
75663
+ field: "surface",
75664
+ value: value ?? null,
75665
+ allowed: [...trendSurfaceSchema.options]
75666
+ });
75667
+ }
75668
+ return parsed.data;
75669
+ }
75670
+ function parseDate(field, value) {
75671
+ if (value === void 0) return void 0;
75672
+ if (!isCalendarDate(value)) throw validationError(`Invalid ${field} "${value}". Use a calendar date, YYYY-MM-DD.`, { field, value });
75673
+ return value;
75674
+ }
75675
+ function parseSelection2(query, surface) {
75676
+ const startDate = parseDate("startDate", query.startDate);
75677
+ const endDate = parseDate("endDate", query.endDate);
75678
+ if (startDate === void 0 && endDate === void 0) {
75679
+ return parseWindowIn(query.window, WINDOW_SETS.metrics, { fallback: TREND_SURFACE_DEFAULT_WINDOW[surface] });
75680
+ }
75681
+ if (query.window !== void 0) {
75682
+ throw validationError("Pass window or a startDate/endDate range, not both.", { field: "window", value: query.window });
75683
+ }
75684
+ if (startDate && endDate && startDate > endDate) {
75685
+ throw validationError(`startDate ${startDate} is after endDate ${endDate}.`, { field: "startDate", value: startDate });
75686
+ }
75687
+ return { ...startDate ? { startDate } : {}, ...endDate ? { endDate } : {} };
75688
+ }
75689
+ async function trendsRoutes(app) {
75690
+ app.get("/projects/:name/trends", async (request) => {
75691
+ const project = resolveProject(app.db, request.params.name);
75692
+ const surface = parseSurface(request.query.surface);
75693
+ const selection = parseSelection2(request.query, surface);
75694
+ const groups = readSurface(app, project.id, surface).filter((group) => group.figures.some((figure) => figure.readings.some((reading) => reading.value !== null)));
75695
+ const span = trendWindowSpan(groups.flatMap((group) => group.figures.flatMap((figure) => figure.readings.filter((reading) => reading.value !== null).map((reading) => reading.date))), selection);
75696
+ const built = groups.map((group) => ({
75697
+ key: group.key,
75698
+ label: group.label,
75699
+ figures: group.figures.map((figure) => buildLevelTrendFigure({ ...figure, span }))
75700
+ }));
75701
+ return {
75702
+ project: project.name,
75703
+ surface,
75704
+ window: typeof selection === "string" ? selection : null,
75705
+ startDate: span?.startDate ?? null,
75706
+ endDate: span?.endDate ?? null,
75707
+ groups: built
75708
+ };
75709
+ });
75710
+ }
75711
+ function readSurface(app, projectId, surface) {
75712
+ switch (surface) {
75713
+ case TrendSurfaces["index-coverage"]: {
75714
+ 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();
75715
+ 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();
75716
+ const coverage = (key, label, rows) => ({
75717
+ key,
75718
+ label,
75719
+ figures: [
75720
+ { key: "indexed", label: "Indexed", unit: TrendUnits.count, better: TrendBetter.higher, readings: rows.map((row) => ({ date: row.date, value: row.indexed })) },
75721
+ { key: "not-indexed", label: "Not indexed", unit: TrendUnits.count, better: TrendBetter.lower, readings: rows.map((row) => ({ date: row.date, value: row.notIndexed })) }
75722
+ ]
75723
+ });
75724
+ return [coverage("google", "Google", google), coverage("bing", "Bing", bing)];
75725
+ }
75726
+ case TrendSurfaces["site-health"]: {
75727
+ 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();
75728
+ return [{
75729
+ key: "site-health",
75730
+ label: "Site Health",
75731
+ figures: [
75732
+ { key: "score", label: "Site score", unit: TrendUnits.score, better: TrendBetter.higher, readings: rows.map((row) => ({ date: row.auditedAt, observedAt: row.auditedAt, value: row.aggregateScore })) },
75733
+ { 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 })) }
75734
+ ]
75735
+ }];
75736
+ }
75737
+ case TrendSurfaces.backlinks: {
75738
+ const totals = computeFilteredReleaseTotals(app.db, projectId, BacklinkSources.commoncrawl);
75739
+ 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) => {
75740
+ const filtered = totals.get(row.release);
75741
+ const known = (filtered?.domainRows ?? 0) > 0 || row.totalLinkingDomains === 0;
75742
+ const windowEnd = releaseWindowEndMonth(row.release);
75743
+ return {
75744
+ date: windowEnd ?? row.queriedAt,
75745
+ observedAt: windowEnd ? null : row.queriedAt,
75746
+ label: formatReleaseWindow(row.release) ?? row.release,
75747
+ linkingDomains: known ? filtered?.linkingDomains ?? 0 : null,
75748
+ hosts: known ? filtered?.hosts ?? 0 : null,
75749
+ top10HostsShare: known ? filtered?.top10HostsShare ?? 0 : null
75750
+ };
75751
+ });
75752
+ const readings = (value) => rows.map((row) => ({ date: row.date, observedAt: row.observedAt, label: row.label, value: value(row) }));
75753
+ return [{
75754
+ key: "backlinks",
75755
+ label: "Backlinks",
75756
+ figures: [
75757
+ { key: "referring-domains", label: "Referring domains", unit: TrendUnits.count, better: TrendBetter.higher, readings: readings((row) => row.linkingDomains) },
75758
+ { key: "hosts", label: "Total linking hosts", unit: TrendUnits.count, better: TrendBetter.higher, readings: readings((row) => row.hosts) },
75759
+ { key: "top-10-concentration", label: "Top-10 concentration", unit: TrendUnits.fraction, better: TrendBetter.neither, readings: readings((row) => row.top10HostsShare) }
75760
+ ]
75761
+ }];
75762
+ }
75763
+ }
75764
+ }
75765
+
75556
75766
  // ../api-routes/src/research.ts
75557
75767
  import crypto59 from "crypto";
75558
75768
  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";
75769
+ 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
75770
  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
75771
  var BATCH_RECEIPT_PREFIX = "__canonry_research_batch__:";
75562
75772
  async function researchRoutes(app, opts) {
@@ -75575,7 +75785,7 @@ async function researchRoutes(app, opts) {
75575
75785
  const input = parsed.data;
75576
75786
  if (input.idempotencyKey?.startsWith(BATCH_RECEIPT_PREFIX)) throw validationError("This idempotency key prefix is reserved for internal research batch receipts.");
75577
75787
  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;
75788
+ 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
75789
  if (existingReceipt) {
75580
75790
  assertSameDirectResearchRequest(existingReceipt, input, requestHash2);
75581
75791
  const result2 = getDetail(app, project.id, existingReceipt.id);
@@ -75610,7 +75820,7 @@ async function researchRoutes(app, opts) {
75610
75820
  const initiatedBy = researchPrincipal(request);
75611
75821
  const decision = app.db.transaction((tx) => {
75612
75822
  if (input.idempotencyKey) {
75613
- const existing = tx.select().from(researchRuns).where(and90(eq107(researchRuns.projectId, project.id), eq107(researchRuns.idempotencyKey, input.idempotencyKey))).get();
75823
+ const existing = tx.select().from(researchRuns).where(and91(eq108(researchRuns.projectId, project.id), eq108(researchRuns.idempotencyKey, input.idempotencyKey))).get();
75614
75824
  if (existing) {
75615
75825
  assertSameDirectResearchRequest(existing, input, requestHash2);
75616
75826
  return { reused: true, id: existing.id, shouldDispatch: existing.status === ResearchRunStatuses.queued };
@@ -75618,8 +75828,8 @@ async function researchRoutes(app, opts) {
75618
75828
  }
75619
75829
  if (initiatedBy?.limited) {
75620
75830
  const { start, end, date } = utcDayBounds(now);
75621
- const used = tx.select({ value: count9() }).from(researchRuns).where(and90(
75622
- eq107(researchRuns.projectId, project.id),
75831
+ const used = tx.select({ value: count9() }).from(researchRuns).where(and91(
75832
+ eq108(researchRuns.projectId, project.id),
75623
75833
  gte31(researchRuns.createdAt, start),
75624
75834
  lt17(researchRuns.createdAt, end),
75625
75835
  sql42`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
@@ -75650,15 +75860,15 @@ async function researchRoutes(app, opts) {
75650
75860
  if (input.idempotencyKey.startsWith(BATCH_RECEIPT_PREFIX)) throw validationError("This idempotency key prefix is reserved for internal research batch receipts.");
75651
75861
  const requestHash2 = researchBatchRequestHash(input);
75652
75862
  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])
75863
+ const firstReceipt = app.db.select().from(researchRuns).where(and91(
75864
+ eq108(researchRuns.projectId, project.id),
75865
+ eq108(researchRuns.idempotencyKey, receiptKeys[0])
75656
75866
  )).get();
75657
75867
  if (firstReceipt) {
75658
75868
  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)
75869
+ const saved = receiptKeys.map((key) => app.db.select().from(researchRuns).where(and91(
75870
+ eq108(researchRuns.projectId, project.id),
75871
+ eq108(researchRuns.idempotencyKey, key)
75662
75872
  )).get());
75663
75873
  if (saved.some((row) => !row || row.requestHash !== requestHash2)) throw alreadyExists("Research batch idempotency key", input.idempotencyKey);
75664
75874
  const runs3 = saved.map((row) => getDetail(app, project.id, row.id));
@@ -75674,8 +75884,8 @@ async function researchRoutes(app, opts) {
75674
75884
  const decision = app.db.transaction((tx) => {
75675
75885
  if (initiatedBy?.limited) {
75676
75886
  const { start, end, date } = utcDayBounds(now);
75677
- const used = tx.select({ value: count9() }).from(researchRuns).where(and90(
75678
- eq107(researchRuns.projectId, project.id),
75887
+ const used = tx.select({ value: count9() }).from(researchRuns).where(and91(
75888
+ eq108(researchRuns.projectId, project.id),
75679
75889
  gte31(researchRuns.createdAt, start),
75680
75890
  lt17(researchRuns.createdAt, end),
75681
75891
  sql42`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
@@ -75729,9 +75939,9 @@ async function researchRoutes(app, opts) {
75729
75939
  const requested = Number.parseInt(request.query.limit ?? "", 10);
75730
75940
  const limit = Number.isInteger(requested) && requested > 0 ? Math.min(requested, 100) : 20;
75731
75941
  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
75942
+ const rows = app.db.select().from(researchRuns).where(and91(
75943
+ eq108(researchRuns.projectId, project.id),
75944
+ cursor ? or22(lt17(researchRuns.createdAt, cursor.createdAt), and91(eq108(researchRuns.createdAt, cursor.createdAt), lt17(researchRuns.id, cursor.id))) : void 0
75735
75945
  )).orderBy(desc49(researchRuns.createdAt), desc49(researchRuns.id)).limit(limit + 1).all();
75736
75946
  const runs2 = rows.slice(0, limit).map(serializeRun);
75737
75947
  const last = runs2.at(-1);
@@ -75881,9 +76091,9 @@ function resolveStoredResearchTemplate(stored, selection, idempotencyKey) {
75881
76091
  return stored;
75882
76092
  }
75883
76093
  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)
76094
+ const savedTemplate = app.db.select().from(measurementQueryTemplates).where(and91(
76095
+ eq108(measurementQueryTemplates.projectId, projectId),
76096
+ eq108(measurementQueryTemplates.id, selection.templateId)
75887
76097
  )).get();
75888
76098
  if (!savedTemplate) throw validationError("Unknown or stale research template. Reload the selected template and try again.");
75889
76099
  const templateVersion = savedTemplate.updatedAt;
@@ -75892,7 +76102,7 @@ function resolveResearchTemplate(app, projectId, selection, scope, location) {
75892
76102
  }
75893
76103
  const bindingLocation = selection.bindingLocation === void 0 ? location : selection.bindingLocation;
75894
76104
  if (bindingLocation) {
75895
- const project = app.db.select({ locations: projects.locations }).from(projects).where(eq107(projects.id, projectId)).get();
76105
+ const project = app.db.select({ locations: projects.locations }).from(projects).where(eq108(projects.id, projectId)).get();
75896
76106
  if (!project.locations.some((item2) => sameLocation(item2, bindingLocation))) throw validationError("Research template binding location must match a configured project location.");
75897
76107
  }
75898
76108
  const { bindings, output } = expandResearchTemplate(savedTemplate, scope, bindingLocation);
@@ -75905,9 +76115,9 @@ function classifyResearchQueries(project, plan, queries2) {
75905
76115
  return queries2.map((query) => classifier?.classify(query) ?? null);
75906
76116
  }
75907
76117
  function getDetail(app, projectId, id) {
75908
- const row = app.db.select().from(researchRuns).where(and90(eq107(researchRuns.id, id), eq107(researchRuns.projectId, projectId))).get();
76118
+ const row = app.db.select().from(researchRuns).where(and91(eq108(researchRuns.id, id), eq108(researchRuns.projectId, projectId))).get();
75909
76119
  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);
76120
+ const queries2 = app.db.select().from(researchRunQueries).where(eq108(researchRunQueries.researchRunId, id)).orderBy(researchRunQueries.position).all().map(serializeQuery);
75911
76121
  return { ...serializeRun(row), queries: queries2 };
75912
76122
  }
75913
76123
  function serializeRun(row) {
@@ -75962,7 +76172,7 @@ function utcDayBounds(now) {
75962
76172
 
75963
76173
  // ../api-routes/src/simple-measurement-definitions.ts
75964
76174
  import crypto60 from "crypto";
75965
- import { and as and91, eq as eq108 } from "drizzle-orm";
76175
+ import { and as and92, eq as eq109 } from "drizzle-orm";
75966
76176
  function captureSimpleMeasurementDefinition(db, input) {
75967
76177
  return db.transaction((tx) => {
75968
76178
  const run = tx.select({
@@ -75970,7 +76180,7 @@ function captureSimpleMeasurementDefinition(db, input) {
75970
76180
  trigger: runs.trigger,
75971
76181
  status: runs.status,
75972
76182
  measurementPlanVersionId: runs.measurementPlanVersionId
75973
- }).from(runs).where(and91(eq108(runs.projectId, input.projectId), eq108(runs.id, input.runId))).get();
76183
+ }).from(runs).where(and92(eq109(runs.projectId, input.projectId), eq109(runs.id, input.runId))).get();
75974
76184
  if (!run) throw notFound("Run", input.runId);
75975
76185
  if (run.kind !== RunKinds["answer-visibility"] || run.trigger === RunTriggers.probe || run.measurementPlanVersionId !== null) {
75976
76186
  return null;
@@ -75991,7 +76201,7 @@ function captureSimpleMeasurementDefinition(db, input) {
75991
76201
  throw validationError("Qualified aliases must be a sorted subset of the captured aliases.");
75992
76202
  }
75993
76203
  }
75994
- const existing = tx.select().from(simpleMeasurementDefinitions).where(and91(eq108(simpleMeasurementDefinitions.projectId, input.projectId), eq108(simpleMeasurementDefinitions.runId, input.runId))).get();
76204
+ const existing = tx.select().from(simpleMeasurementDefinitions).where(and92(eq109(simpleMeasurementDefinitions.projectId, input.projectId), eq109(simpleMeasurementDefinitions.runId, input.runId))).get();
75995
76205
  if (existing) {
75996
76206
  const frozen = simpleMeasurementDefinitionSchema.parse(existing.definition);
75997
76207
  const { qualifiedAliases: _liveQualifiedAliases, ...liveIdentity } = definition2.identity;
@@ -76017,15 +76227,15 @@ function captureSimpleMeasurementDefinition(db, input) {
76017
76227
  if (run.status !== RunStatuses.running) {
76018
76228
  throw validationError("Only a running run can capture a new measurement definition.");
76019
76229
  }
76020
- if (tx.select({ id: querySnapshots.id }).from(querySnapshots).where(eq108(querySnapshots.runId, input.runId)).limit(1).get()) {
76230
+ if (tx.select({ id: querySnapshots.id }).from(querySnapshots).where(eq109(querySnapshots.runId, input.runId)).limit(1).get()) {
76021
76231
  throw validationError("This run already has stored answers. Its measurement definition cannot be inferred afterward.");
76022
76232
  }
76023
- const projectQueryIds = new Set(tx.select({ id: queries.id }).from(queries).where(eq108(queries.projectId, input.projectId)).all().map((query) => query.id));
76233
+ const projectQueryIds = new Set(tx.select({ id: queries.id }).from(queries).where(eq109(queries.projectId, input.projectId)).all().map((query) => query.id));
76024
76234
  if (definition2.queries.some((query) => !projectQueryIds.has(query.queryId))) {
76025
76235
  throw validationError("Every captured query must belong to the run project.");
76026
76236
  }
76027
76237
  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));
76238
+ 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
76239
  const capturedDomains = new Set(definition2.competitors.map((competitor) => competitor.domain));
76030
76240
  if (liveCompetitorDomains.size !== capturedDomains.size || [...liveCompetitorDomains].some((domain) => !capturedDomains.has(domain))) {
76031
76241
  throw validationError("Captured competitors must exactly match the project competitors dispatched for this run.");
@@ -76407,6 +76617,7 @@ async function apiRoutes(app, opts) {
76407
76617
  await api.register(technicalAeoRoutes, {
76408
76618
  onSiteAuditRequested: opts.onSiteAuditRequested
76409
76619
  });
76620
+ await api.register(trendsRoutes);
76410
76621
  await api.register(doctorRoutes, {
76411
76622
  googleConnectionStore: opts.googleConnectionStore,
76412
76623
  bingConnectionStore: opts.bingConnectionStore,