@canonry/canonry 7.65.1 → 7.67.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 (138) hide show
  1. package/assets/agent-workspace/skills/aero/references/seo-analysis.md +15 -1
  2. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +2 -2
  3. package/assets/assets/{AgentSetupPage-BDLMjn1I.js → AgentSetupPage-ORsy3QEa.js} +3 -3
  4. package/assets/assets/{AuditHistoryPanel-qhIB8KRK.js → AuditHistoryPanel-vTHZDbfs.js} +1 -1
  5. package/assets/assets/{BacklinksPage-iWrZ0-Pp.js → BacklinksPage-FqADB5sO.js} +1 -1
  6. package/assets/assets/{HistoryPage-BP1_GGlT.js → HistoryPage-C4l5Toqa.js} +1 -1
  7. package/assets/assets/MeasurementPropertyPage-x8f7xanT.js +4 -0
  8. package/assets/assets/ProjectPage-CG9I2-B4.js +9 -0
  9. package/assets/assets/{RunsPage-Ddd2oE1Y.js → RunsPage-oyhhwAPq.js} +1 -1
  10. package/assets/assets/{SettingsPage-D_QdYWDO.js → SettingsPage-DJc4Di1M.js} +1 -1
  11. package/assets/assets/SiteHealthSection-BvqFg3rG.js +2 -0
  12. package/assets/assets/ToneBadge-CTWpeKu8.js +60 -0
  13. package/assets/assets/{TrafficPage-VnBUE1S6.js → TrafficPage-DAD0FFkL.js} +2 -2
  14. package/assets/assets/TrafficSourceDetailPage-BBEzbawh.js +1 -0
  15. package/assets/assets/{account-context-CWRd3KeS.js → account-context-DMHOefRt.js} +1 -1
  16. package/assets/assets/{card-Cb_fM36G.js → card-Brp6fFNG.js} +1 -1
  17. package/assets/assets/{extract-error-message-DSIAoHTR.js → extract-error-message-5QpQCsGb.js} +1 -1
  18. package/assets/assets/{format-helpers-CxNDa1oa.js → format-helpers-BFM0FRjT.js} +1 -1
  19. package/assets/assets/index-BBNs-RhI.js +60 -0
  20. package/assets/assets/{query-client-DLdAMlsX.js → query-client-BMTW-xss.js} +1 -1
  21. package/assets/assets/react-query-DGmY6hJa.js +1 -0
  22. package/assets/assets/{react-sigma_core.esm.min-DEG8vFYD.js → react-sigma_core.esm.min-C0VjhgDo.js} +1 -1
  23. package/assets/assets/service-DW9u_0aZ.js +1 -0
  24. package/assets/index.html +7 -7
  25. package/dist/{ads-D4WQOT5K.js → ads-BXGEL4WI.js} +3 -3
  26. package/dist/{agent-E3IYC7OJ.js → agent-LFX7GHWD.js} +7 -7
  27. package/dist/{agent-ask-ZSKRPB2Z.js → agent-ask-QJV225AT.js} +3 -3
  28. package/dist/{agent-conversations-YHPYPQE2.js → agent-conversations-2TZDIGA6.js} +3 -3
  29. package/dist/{agent-memory-6OWOOD5O.js → agent-memory-XCY3ZG5C.js} +3 -3
  30. package/dist/{agent-providers-7KZK37UO.js → agent-providers-VS6PH55Y.js} +3 -3
  31. package/dist/{agent-transcript-3O7GPML6.js → agent-transcript-BUUHOTZM.js} +3 -3
  32. package/dist/{analytics-P2LLIUYK.js → analytics-YWJWPUNF.js} +3 -3
  33. package/dist/{apply-VBNNJOCT.js → apply-BMCYFBEJ.js} +3 -3
  34. package/dist/{backfill-F5SSURCH.js → backfill-YBOKT5LT.js} +9 -9
  35. package/dist/{backlinks-4RE3TNNA.js → backlinks-QNNABB4N.js} +3 -3
  36. package/dist/{bing-V6UN7D44.js → bing-KEMXBABP.js} +3 -3
  37. package/dist/{bootstrap-SHTSPAI5.js → bootstrap-X2AADK4M.js} +10 -10
  38. package/dist/{cdp-GDL2VFI3.js → cdp-WVYUEV4U.js} +7 -7
  39. package/dist/{chunk-3QKKHU7D.js → chunk-2X7DFLMD.js} +8 -8
  40. package/dist/{chunk-2AROR73F.js → chunk-32BTTG5N.js} +5 -5
  41. package/dist/{chunk-BV6NXSAH.js → chunk-37R6GTU7.js} +21 -3
  42. package/dist/{chunk-HPUZMBS6.js → chunk-5DMESZDF.js} +2 -2
  43. package/dist/{chunk-XOMUSIM7.js → chunk-6DJN7HCK.js} +83 -11
  44. package/dist/{chunk-SRTX6XYR.js → chunk-AFRCTLUL.js} +370 -337
  45. package/dist/{chunk-Z2EENWM3.js → chunk-AVISYPUU.js} +877 -478
  46. package/dist/{chunk-HI2HCYLB.js → chunk-CBYKQRDH.js} +2 -2
  47. package/dist/{chunk-S4VCXCRQ.js → chunk-EKSXR7RJ.js} +1 -1
  48. package/dist/{chunk-66DPEVLJ.js → chunk-G6PZEDTH.js} +3 -3
  49. package/dist/{chunk-GZPGDAAF.js → chunk-GTPGYFZ4.js} +2 -2
  50. package/dist/{chunk-Q2WZAD5W.js → chunk-HX3WLTZU.js} +1 -1
  51. package/dist/{chunk-JSAYC36T.js → chunk-IEBMODUI.js} +193 -47
  52. package/dist/{chunk-2UFA3DYM.js → chunk-KJLICF53.js} +4 -4
  53. package/dist/{chunk-ZPRJCSSR.js → chunk-LU3M54IY.js} +1 -1
  54. package/dist/{chunk-HO5W6QC7.js → chunk-MD5PHJBX.js} +2 -1
  55. package/dist/{chunk-SL3Y7IOX.js → chunk-MLOGEXIO.js} +1 -1
  56. package/dist/{chunk-LTX3TJ3E.js → chunk-MOJ2XZXK.js} +1 -1
  57. package/dist/{chunk-IYB3EGSD.js → chunk-NHHZQWMI.js} +1 -1
  58. package/dist/{chunk-H6HIJAIU.js → chunk-P74TPIWZ.js} +2 -2
  59. package/dist/{chunk-M3OZL6PQ.js → chunk-RRS7U6HQ.js} +4 -4
  60. package/dist/{chunk-X4AHYJCE.js → chunk-UHLWNU6I.js} +5 -5
  61. package/dist/{chunk-C2PYKUD3.js → chunk-UNCOK6DA.js} +1 -1
  62. package/dist/{chunk-IJKKK7F4.js → chunk-UXTNDZKM.js} +1 -1
  63. package/dist/{chunk-GM5ZOWBK.js → chunk-VR4MYSEH.js} +4 -4
  64. package/dist/{chunk-ZWZOQNLD.js → chunk-WSYLDPYF.js} +1 -1
  65. package/dist/{chunk-AE6HJZBU.js → chunk-WZ4LUM26.js} +3 -3
  66. package/dist/{chunk-DQVJB6CZ.js → chunk-XEGYSZEV.js} +99 -9
  67. package/dist/{chunk-F7NZEUXP.js → chunk-XEPEMON2.js} +14 -2
  68. package/dist/{chunk-CRKOGLKJ.js → chunk-XIA6NWIF.js} +4 -4
  69. package/dist/{chunk-737TOG2F.js → chunk-XMIKSCAO.js} +1 -1
  70. package/dist/{chunk-2Y7RN5HG.js → chunk-Y54PMWBH.js} +3 -3
  71. package/dist/{chunk-QNRMYDK6.js → chunk-YP743QX5.js} +1 -1
  72. package/dist/{chunk-XMKXXWMO.js → chunk-ZGUR4AZM.js} +2 -2
  73. package/dist/{citations-GO4SPZYS.js → citations-ZSI5BOGD.js} +3 -3
  74. package/dist/{cli-catalog-6XYQY3PI.js → cli-catalog-J2HUCGJL.js} +3 -3
  75. package/dist/{cli-commands-2TZKVGK6.js → cli-commands-XFMVFREG.js} +7 -7
  76. package/dist/cli.js +13 -13
  77. package/dist/{competitor-JBTKRJ6Y.js → competitor-WJDN3DLP.js} +3 -3
  78. package/dist/{content-LKXXQ7X4.js → content-K52YV4BK.js} +3 -3
  79. package/dist/{dashboard-LUA7UX6B.js → dashboard-L6RKTP35.js} +6 -6
  80. package/dist/{demo-I7JDZJUW.js → demo-Y7XRD4X4.js} +1 -1
  81. package/dist/{demo-server-GNGGWERV.js → demo-server-QNQX5CNP.js} +73 -11
  82. package/dist/{discover-TODAYYJ2.js → discover-UO5M7HQZ.js} +3 -3
  83. package/dist/{discover-eval-FWQESEYB.js → discover-eval-DN4PS6VN.js} +3 -3
  84. package/dist/{doctor-AXNJT2UC.js → doctor-7UYUXG7J.js} +3 -3
  85. package/dist/{evidence-2XGEY4ZC.js → evidence-DGKN5SGQ.js} +3 -3
  86. package/dist/{export-cmd-7RMYBIBZ.js → export-cmd-QAQWWIRL.js} +3 -3
  87. package/dist/{ga-HZDIZGCO.js → ga-4LGTEA6J.js} +3 -3
  88. package/dist/{gbp-SMRBWJEL.js → gbp-XOLYZ4XC.js} +3 -3
  89. package/dist/{google-2CN345XS.js → google-CAQDCFA6.js} +4 -4
  90. package/dist/{google-marketing-W7T2VTVW.js → google-marketing-HU5I3XPP.js} +1 -1
  91. package/dist/{health-cmd-W7OWZ4ZI.js → health-cmd-3PEB3M45.js} +3 -3
  92. package/dist/{history-UVNCBHTE.js → history-UGUWYPOY.js} +3 -3
  93. package/dist/index.d.ts +342 -161
  94. package/dist/index.js +24 -24
  95. package/dist/{init-2IWS2R7G.js → init-DEW65BLN.js} +11 -11
  96. package/dist/{insights-PQ52G5LU.js → insights-ZC3R4ZYG.js} +3 -3
  97. package/dist/intelligence-service-XRJVXZYI.js +15 -0
  98. package/dist/{journeys-3PP5GYVJ.js → journeys-W6F4BCXF.js} +3 -3
  99. package/dist/{keys-CWZUX7IQ.js → keys-ME7VRF73.js} +3 -3
  100. package/dist/{keyword-5OMMULLY.js → keyword-R3PLGSO5.js} +3 -3
  101. package/dist/{logs-3FTE42E3.js → logs-UM7CWHYB.js} +3 -3
  102. package/dist/{mcp-FK5DTI7I.js → mcp-BUPP5HC6.js} +2 -2
  103. package/dist/mcp.js +9 -9
  104. package/dist/{measurement-plan-ICO2BQNT.js → measurement-plan-R4KZHJII.js} +5 -5
  105. package/dist/{notify-ML4PYD3L.js → notify-4C26D2MY.js} +3 -3
  106. package/dist/{organic-evidence-4U4H3DCR.js → organic-evidence-SLIISYHW.js} +3 -3
  107. package/dist/{overview-NF427VFO.js → overview-JJZZOYQU.js} +4 -4
  108. package/dist/{project-AAO65OF7.js → project-XJH2OECP.js} +3 -3
  109. package/dist/{query-7ZOUQNF5.js → query-ZWAIWIYB.js} +3 -3
  110. package/dist/{research-UQ7AF5N3.js → research-GTVWVZBI.js} +3 -3
  111. package/dist/{results-export-CZCLC4ZH.js → results-export-CPAWO5AV.js} +3 -3
  112. package/dist/{run-4XJZW52R.js → run-RI2BMD7U.js} +4 -4
  113. package/dist/{schedule-D3E5FDBE.js → schedule-HDDWULBX.js} +3 -3
  114. package/dist/{search-DYET2IL4.js → search-TT3Q2BBP.js} +3 -3
  115. package/dist/{serve-QWMTR3SN.js → serve-6JYPUU24.js} +25 -25
  116. package/dist/{session-7CW2V5EH.js → session-DDESFV3N.js} +12 -12
  117. package/dist/{settings-ZISHACFX.js → settings-4AYXYJVA.js} +3 -3
  118. package/dist/{skills-WU6IN3NL.js → skills-UFMLMMY5.js} +2 -2
  119. package/dist/{snapshot-KCKQDJ2W.js → snapshot-KLWQGZB6.js} +3 -3
  120. package/dist/{src-WIOU5G4V.js → src-6IHGEMT2.js} +6 -2
  121. package/dist/{status-CYZFVMYA.js → status-HRIUPGUM.js} +4 -4
  122. package/dist/{technical-aeo-P5CS22AR.js → technical-aeo-HKJQHMMS.js} +3 -3
  123. package/dist/{telemetry-LPK5F2WU.js → telemetry-IS6KZK32.js} +4 -4
  124. package/dist/{tools-E3B2GZ6Q.js → tools-ZNVQFEAX.js} +11 -11
  125. package/dist/{traffic-5KZYXXIQ.js → traffic-PFHFGN44.js} +5 -5
  126. package/dist/{users-2F346HIW.js → users-BSLUSU6W.js} +3 -3
  127. package/dist/{visibility-stats-ITA6DNF3.js → visibility-stats-MPRNHPZW.js} +3 -3
  128. package/dist/{wordpress-ZHCFT6V3.js → wordpress-U5XKHURL.js} +3 -3
  129. package/package.json +1 -1
  130. package/assets/assets/MeasurementPropertyPage-dxGJ1GF9.js +0 -4
  131. package/assets/assets/ProjectPage-W6oYA85b.js +0 -9
  132. package/assets/assets/SiteHealthSection-DT20rNw-.js +0 -2
  133. package/assets/assets/ToneBadge-CJ6ObMRe.js +0 -60
  134. package/assets/assets/TrafficSourceDetailPage-CdoL5Dt9.js +0 -1
  135. package/assets/assets/index-D6WlecSB.js +0 -60
  136. package/assets/assets/react-query-BktZZtSC.js +0 -1
  137. package/assets/assets/service-DKP-BIBy.js +0 -1
  138. package/dist/intelligence-service-IAUGKMDO.js +0 -15
@@ -9,14 +9,14 @@ import {
9
9
  normalizeCloudflareWorkerEvent,
10
10
  sessionizeJourneyRequests,
11
11
  verifyRequestSignature
12
- } from "./chunk-Q2WZAD5W.js";
12
+ } from "./chunk-HX3WLTZU.js";
13
13
  import {
14
14
  currentOutcomeAttribution,
15
15
  getRequestContext,
16
16
  registerRequestContext,
17
17
  requestAuditIdentity,
18
18
  sanitizeRequestContext
19
- } from "./chunk-IJKKK7F4.js";
19
+ } from "./chunk-UXTNDZKM.js";
20
20
  import {
21
21
  SentimentIdempotencyConflict,
22
22
  SentimentRepository,
@@ -49,6 +49,8 @@ import {
49
49
  conversionTrackingContracts,
50
50
  crawlerEventsHourly,
51
51
  crawlerVerificationManifestsHourly,
52
+ currentSeoTaskCondition,
53
+ currentSeoTasks,
52
54
  dashboardSessions,
53
55
  discoveryProbes,
54
56
  discoverySessions,
@@ -154,7 +156,7 @@ import {
154
156
  userSessions,
155
157
  users,
156
158
  writeNegativeReviewMaxStars
157
- } from "./chunk-XOMUSIM7.js";
159
+ } from "./chunk-6DJN7HCK.js";
158
160
  import {
159
161
  ADS_ACTIVATE_SCOPE,
160
162
  ADS_ACTIVATION_ABSOLUTE_MAX_ENTITIES,
@@ -338,6 +340,8 @@ import {
338
340
  SENTIMENT_ATTEMPT_PAGE_DEFAULT,
339
341
  SENTIMENT_INTERVAL_LIMITATION,
340
342
  SENTIMENT_QUERY_PAGE_DEFAULT,
343
+ SEO_MARKET_RANKING_MAX_SEARCHES,
344
+ SEO_PROJECT_MARKET_KEY,
341
345
  SEO_RUN_SCOPE,
342
346
  SEO_WRITE_SCOPE,
343
347
  SITE_AUDIT_ISSUES_DEFAULT_LIMIT,
@@ -364,6 +368,7 @@ import {
364
368
  SeoAdapterOutcomes,
365
369
  SeoAvailability,
366
370
  SeoCapabilities,
371
+ SeoMarketRankingSearchStatuses,
367
372
  SeoRunStatuses,
368
373
  SeoScopeKinds,
369
374
  SeoTargetKinds,
@@ -603,7 +608,6 @@ import {
603
608
  effectiveDomains,
604
609
  emptyCitationVisibility,
605
610
  emptySentimentCounts,
606
- emptySeoResults,
607
611
  escapeLikePattern,
608
612
  escapeRegExp,
609
613
  evaluateModelPointerExposure,
@@ -949,6 +953,7 @@ import {
949
953
  providersFailing,
950
954
  qualifiedNameIncludesName,
951
955
  queryClassFilterSchema,
956
+ queryClassSchema,
952
957
  queryDtoSchema,
953
958
  queryGenerateRequestSchema,
954
959
  queryGenerateResponseSchema,
@@ -1017,6 +1022,7 @@ import {
1017
1022
  runtimeStateMissing,
1018
1023
  schedulableRunKindSchema,
1019
1024
  scheduleDtoSchema,
1025
+ scheduleEnabledAt,
1020
1026
  scheduleExpectedUpdatedAtSchema,
1021
1027
  scheduleUpsertRequestSchema,
1022
1028
  scheduleVersionConflict,
@@ -1042,6 +1048,8 @@ import {
1042
1048
  seoDomainIntelligenceListSchema,
1043
1049
  seoEvidenceQuerySchema,
1044
1050
  seoKeywordMetricsListSchema,
1051
+ seoMarketRankingListSchema,
1052
+ seoMarketRankingQuerySchema,
1045
1053
  seoOperatorTaskSchema,
1046
1054
  seoOverviewSchema,
1047
1055
  seoProfileInputSchema,
@@ -1055,6 +1063,7 @@ import {
1055
1063
  seoRunDetailSchema,
1056
1064
  seoRunListSchema,
1057
1065
  seoRunSchema,
1066
+ seoSearchTextForQuery,
1058
1067
  seoStatusSchema,
1059
1068
  seoSyncInputSchema,
1060
1069
  serializeRunError,
@@ -1167,7 +1176,7 @@ import {
1167
1176
  wordpressSchemaDeployResultDtoSchema,
1168
1177
  wordpressSchemaStatusResultDtoSchema,
1169
1178
  wordpressStatusDtoSchema
1170
- } from "./chunk-DQVJB6CZ.js";
1179
+ } from "./chunk-XEGYSZEV.js";
1171
1180
 
1172
1181
  // ../api-routes/src/user-session.ts
1173
1182
  import crypto2 from "crypto";
@@ -7256,9 +7265,9 @@ var SentimentService = class {
7256
7265
  const estimatedInputTokens = this.options.estimate ? inputs.reduce((sum, input) => sum + this.options.estimate(input), 0) : 0;
7257
7266
  const expiresAt = new Date(Date.parse(this.now()) + 15 * 6e4).toISOString();
7258
7267
  const previewToken = definitionId && settings.enabled && settings.ready && source.assessments.length ? this.sign({ projectId, selection: frozenSelection, definitionId, fingerprint: sentimentHash(source.assessments), expiresAt }) : null;
7259
- const skipped5 = source.runIds.flatMap((runId) => Object.entries(source.skippedByRun?.[runId] ?? {}).map(([reason, count10]) => ({ runId, reason, count: count10 })));
7268
+ const skipped6 = source.runIds.flatMap((runId) => Object.entries(source.skippedByRun?.[runId] ?? {}).map(([reason, count10]) => ({ runId, reason, count: count10 })));
7260
7269
  const estimateMethod = this.options.estimate ? `Adapter estimate of the full request including all questions and evidence candidates; final billed usage is recorded per attempt.${definitionId ? "" : " Sentiment is not configured for this project, so the estimate uses the current evaluator definition."}` : "Token and cost estimates unavailable on this host.";
7261
- return { previewToken, expiresAt: previewToken ? expiresAt : null, selection: frozenSelection, evaluationDefinitionId: definitionId, eligibleAssessments: source.assessments.length, alreadyClassified, skipped: skipped5, estimatedInputTokens, estimatedCostUsd: this.options.estimate ? estimatedInputTokens * 0.042 / 1e6 : null, estimateMethod };
7270
+ return { previewToken, expiresAt: previewToken ? expiresAt : null, selection: frozenSelection, evaluationDefinitionId: definitionId, eligibleAssessments: source.assessments.length, alreadyClassified, skipped: skipped6, estimatedInputTokens, estimatedCostUsd: this.options.estimate ? estimatedInputTokens * 0.042 / 1e6 : null, estimateMethod };
7262
7271
  }
7263
7272
  /** The receipt carries the first page of attempts; a replay of a long-running job never returns all of them. */
7264
7273
  submit(projectId, previewToken, idempotencyKey, actor2) {
@@ -8413,6 +8422,7 @@ var HEALTH_CHECK_LABELS = {
8413
8422
  "ga.auth.connection": "GA4 sign-in",
8414
8423
  "ga.data.recent-data": "GA4 data stopped",
8415
8424
  "gsc.data.recent-data": "Search Console data stopped",
8425
+ "seo.data.recent-refresh": "SEO refresh stalled",
8416
8426
  "report.sweeps": "Monthly report sweeps",
8417
8427
  "report.models": "Monthly report models",
8418
8428
  "report.daily-data": "Monthly report data gaps",
@@ -8465,7 +8475,11 @@ var HEALTH_FIXED_LINES = {
8465
8475
  "traffic.source.all-errored-recently": "Traffic syncs are working again.",
8466
8476
  "traffic.queue-backlog.remaining": "The traffic backlog is cleared.",
8467
8477
  "traffic.worker-version.stale": "The Cloudflare worker is up to date.",
8468
- "gbp.auth.no-connection": "Business Profile is connected."
8478
+ "gbp.auth.no-connection": "Business Profile is connected.",
8479
+ "seo.data.refresh-failed": "SEO refreshes complete again.",
8480
+ "seo.data.refresh-partial": "SEO refreshes complete again.",
8481
+ "seo.data.overdue": "SEO data is current again.",
8482
+ "seo.data.schedule-invalid": "The SEO refresh schedule can run again."
8469
8483
  };
8470
8484
  function firstSentence(text, max2 = 180) {
8471
8485
  if (!text) return null;
@@ -15214,7 +15228,40 @@ function runDto(job, counts2) {
15214
15228
  return { id: job.id, projectId: job.projectKey, profileId: job.profileId, profileRevision: job.profileRevision, locationCode: job.profile.locationCode, languageCode: job.profile.languageCode, cancelRequested: job.cancelRequested, status: job.status, capabilities: job.profile.capabilities, scope: job.profile.scope, taskCount: counts2.reduce((sum, group) => sum + group.count, 0), completedTasks: counts2.filter((group) => group.status === SeoRunStatuses.completed || group.status === SeoRunStatuses.partial).reduce((sum, group) => sum + group.count, 0), failedTasks: counts2.filter((group) => group.status === SeoRunStatuses.failed).reduce((sum, group) => sum + group.count, 0), createdAt: job.createdAt, startedAt: job.startedAt, finishedAt: job.finishedAt, error: job.error };
15215
15229
  }
15216
15230
  function resultRows(tasks) {
15217
- return tasks.filter((task) => task.reconciliation?.outcome !== SeoAdapterOutcomes.failed).reduce((results, task) => ({ keywordMetrics: results.keywordMetrics.concat(task.results.keywordMetrics), domainIntelligence: results.domainIntelligence.concat(task.results.domainIntelligence), rankedKeywords: results.rankedKeywords.concat(task.results.rankedKeywords) }), emptySeoResults());
15231
+ const kept = tasks.filter((task) => task.reconciliation?.outcome !== SeoAdapterOutcomes.failed);
15232
+ return {
15233
+ keywordMetrics: kept.flatMap((task) => task.results.keywordMetrics),
15234
+ domainIntelligence: kept.flatMap((task) => task.results.domainIntelligence),
15235
+ rankedKeywords: kept.flatMap((task) => task.results.rankedKeywords),
15236
+ marketRankings: tasks.flatMap(marketRankingRows)
15237
+ };
15238
+ }
15239
+ function marketRankingRows(task) {
15240
+ const stored = task.reconciliation?.outcome === SeoAdapterOutcomes.failed ? [] : task.results.marketRankings ?? [];
15241
+ if (stored.length > 0) return stored.map((row) => ({ ...row, searchStatus: row.searchStatus ?? SeoMarketRankingSearchStatuses.completed }));
15242
+ const { search, target } = task.request;
15243
+ if (task.request.capability !== SeoCapabilities["market-rankings"] || !search || !target) return [];
15244
+ const searchStatus = task.status === SeoRunStatuses.cancelled ? SeoMarketRankingSearchStatuses.cancelled : task.status === SeoRunStatuses.failed || task.status === SeoRunStatuses["reconciliation-required"] ? SeoMarketRankingSearchStatuses.failed : null;
15245
+ if (!searchStatus) return [];
15246
+ return search.markets.map((market) => ({
15247
+ ...market.context,
15248
+ searchText: task.request.keywords[0] ?? "",
15249
+ locationLabel: search.locationLabel,
15250
+ target,
15251
+ searchStatus,
15252
+ rankOrganic: null,
15253
+ rankAbsolute: null,
15254
+ url: null,
15255
+ depth: 0,
15256
+ competitors: [],
15257
+ provenance: null
15258
+ }));
15259
+ }
15260
+ function taskSearch(task) {
15261
+ return task.request.capability === SeoCapabilities["market-rankings"] ? { searchText: task.request.keywords[0] ?? null, locationLabel: task.request.search?.locationLabel ?? null } : { searchText: null, locationLabel: null };
15262
+ }
15263
+ function compareMarketRankings(a, b) {
15264
+ return compareText(a.regionLabel ?? "", b.regionLabel ?? "") || compareText(a.marketLabel, b.marketLabel) || compareText(a.searchText, b.searchText) || compareText(a.queryText, b.queryText);
15218
15265
  }
15219
15266
  function scopedPlanTargets(plan, scope) {
15220
15267
  const market = scope.kind === SeoScopeKinds.market ? plan.reportingScopes?.find((market2) => market2.stableKey === scope.key) : void 0;
@@ -15222,9 +15269,14 @@ function scopedPlanTargets(plan, scope) {
15222
15269
  return { targets: plan.targets.filter((target) => targetKeys.includes(target.stableKey)), market, targetKeys };
15223
15270
  }
15224
15271
  function scopeCapabilities(kind, targets) {
15225
- if (kind === SeoScopeKinds.project) return [SeoCapabilities["keyword-metrics"], SeoCapabilities["domain-intelligence"], SeoCapabilities["ranked-keywords"]];
15272
+ if (kind === SeoScopeKinds.project) return [SeoCapabilities["keyword-metrics"], SeoCapabilities["domain-intelligence"], SeoCapabilities["ranked-keywords"], SeoCapabilities["market-rankings"]];
15226
15273
  const exactHosts = targets.length > 0 && targets.every((target) => target.urlMatchers.length > 0 && target.urlMatchers.every((matcher) => matcher.kind === "host"));
15227
- return exactHosts ? [SeoCapabilities["keyword-metrics"], SeoCapabilities["ranked-keywords"]] : [SeoCapabilities["keyword-metrics"]];
15274
+ const markets = kind === SeoScopeKinds.group || kind === SeoScopeKinds.market ? [SeoCapabilities["market-rankings"]] : [];
15275
+ return exactHosts ? [SeoCapabilities["keyword-metrics"], SeoCapabilities["ranked-keywords"], ...markets] : [SeoCapabilities["keyword-metrics"], ...markets];
15276
+ }
15277
+ function scopeReason(capabilities) {
15278
+ const markets = capabilities.includes(SeoCapabilities["market-rankings"]);
15279
+ return capabilities.includes(SeoCapabilities["ranked-keywords"]) ? `Supports explicit keyword metrics${markets ? ", market rankings" : ""} and exact-host ranked keywords. Whole-domain intelligence requires project scope.` : `Supports explicit keyword metrics${markets ? " and market rankings" : " only"}. Path and entity Properties cannot be replaced by whole-domain data.`;
15228
15280
  }
15229
15281
  function cancelSeoRefresh(db, projectId, id) {
15230
15282
  db.transaction((tx) => {
@@ -15252,7 +15304,7 @@ function resolveScope2(db, projectId, input) {
15252
15304
  if (supported.includes(SeoCapabilities["ranked-keywords"])) throw validationError("Domain intelligence requires project scope with a whole-domain Target. This Advanced scope supports exact-host ranked keywords and explicit keyword metrics.");
15253
15305
  throw validationError("This Advanced scope contains path or entity Properties and supports explicit keyword metrics only. Select a host-only scope for ranked keywords, or project scope for whole-domain intelligence.");
15254
15306
  }
15255
- if (input.scope.kind !== SeoScopeKinds.project && input.capabilities.some((capability) => capability !== SeoCapabilities["keyword-metrics"])) {
15307
+ if (input.scope.kind !== SeoScopeKinds.project && input.capabilities.includes(SeoCapabilities["ranked-keywords"])) {
15256
15308
  const matchers = targets.flatMap((target) => target.urlMatchers).filter((matcher) => matcher.kind === "host");
15257
15309
  if (input.targets.some((target) => target.kind !== SeoTargetKinds.host || target.includeSubdomains || !matchers.some((matcher) => matchesMeasurementTargetUrl(`https://${target.value}/`, matcher)))) throw validationError("Ranked keywords in this Advanced scope require an exact host Target belonging to a selected Property. Other hosts and broader domains are not supported.");
15258
15310
  }
@@ -15271,12 +15323,128 @@ function normalizeProfile(input) {
15271
15323
  const keywords = [...new Set(input.keywords)];
15272
15324
  if (input.capabilities.includes(SeoCapabilities["keyword-metrics"]) && !keywords.length) throw validationError("Keyword metrics requires at least one explicit keyword.");
15273
15325
  if (input.capabilities.some((capability) => capability !== SeoCapabilities["keyword-metrics"]) && !targets.length) throw validationError("Domain capabilities require at least one explicit Target.");
15326
+ if (input.capabilities.includes(SeoCapabilities["market-rankings"])) {
15327
+ if (input.capabilities.length !== 1) throw validationError("Keep market rankings in their own profile: they run one search for each tracked query and market.");
15328
+ if (!input.rankings) throw validationError("Market rankings need search settings: market locations, result depth, device and search text.");
15329
+ if (targets.length !== 1) throw validationError("Market rankings need exactly one Target: the site whose position they report.");
15330
+ if (keywords.length) throw validationError("Market rankings search the project's tracked queries. Leave keywords empty.");
15331
+ const keys = input.rankings.locations.map((location) => location.key);
15332
+ if (new Set(keys).size !== keys.length) throw validationError("Set each market location once.");
15333
+ } else if (input.rankings) {
15334
+ throw validationError("Search settings apply only to market rankings.");
15335
+ }
15274
15336
  return { ...input, targets, keywords };
15275
15337
  }
15276
- function taskRequests(profile) {
15277
- return profile.capabilities.flatMap((capability) => (capability === SeoCapabilities["keyword-metrics"] ? [null] : profile.targets).map((target) => ({ capability, target, keywords: capability === SeoCapabilities["keyword-metrics"] ? profile.keywords : [], locationCode: profile.locationCode, languageCode: profile.languageCode, limit: profile.limit, tag: "" })));
15338
+ var NO_MARKET_RANKINGS = { searches: [] };
15339
+ function advancedMarketQueries(plan, scope) {
15340
+ const groups = new Map(plan.groups.map((group) => [group.stableKey, group]));
15341
+ const lineage = (groupKey) => {
15342
+ const keys = [];
15343
+ for (let key = groupKey; key && !keys.includes(key); key = groups.get(key)?.parentGroupKey) keys.push(key);
15344
+ return keys;
15345
+ };
15346
+ const nonBrandEdge = new Set(plan.assignments.filter((assignment) => assignment.queryClass === queryClassSchema.enum["non-brand"]).map((assignment) => `${assignment.targetKey}\0${assignment.queryId}`));
15347
+ const queryText = new Map(plan.querySnapshots.map((snapshot) => [snapshot.queryId, snapshot.queryText]));
15348
+ const queriesFor = (edges) => {
15349
+ const nodes = /* @__PURE__ */ new Map();
15350
+ for (const edge of edges) nodes.set(edge.queryId, (nodes.get(edge.queryId) ?? /* @__PURE__ */ new Set()).add(edge.executionNodeKey));
15351
+ return [...nodes].flatMap(([id, keys]) => {
15352
+ const text = queryText.get(id);
15353
+ return text ? [{ id, text, executionNodeKeys: [...keys] }] : [];
15354
+ });
15355
+ };
15356
+ const markets = plan.reportingScopes ?? [];
15357
+ if (!markets.length) {
15358
+ const { targetKeys } = scopedPlanTargets(plan, scope);
15359
+ return [{ context: { marketKey: SEO_PROJECT_MARKET_KEY, marketLabel: "Project", regionKey: null, regionLabel: null }, locationKeys: [SEO_PROJECT_MARKET_KEY], queries: queriesFor(plan.assignments.filter((assignment) => targetKeys.includes(assignment.targetKey) && nonBrandEdge.has(`${assignment.targetKey}\0${assignment.queryId}`))) }];
15360
+ }
15361
+ return markets.flatMap((market) => {
15362
+ const chain = lineage(market.groupKey);
15363
+ const inScope = scope.kind === SeoScopeKinds.project || (scope.kind === SeoScopeKinds.market ? market.stableKey === scope.key : scope.kind === SeoScopeKinds.group && chain.includes(scope.key));
15364
+ if (!inScope) return [];
15365
+ const region = chain.at(-1);
15366
+ return [{
15367
+ context: { marketKey: market.stableKey, marketLabel: market.label, regionKey: region ?? null, regionLabel: region ? groups.get(region)?.label ?? region : null },
15368
+ locationKeys: [market.stableKey, ...chain],
15369
+ queries: queriesFor(market.usageEdges.filter((edge) => nonBrandEdge.has(`${edge.targetKey}\0${edge.queryId}`)))
15370
+ }];
15371
+ });
15372
+ }
15373
+ function simpleMarketQueries(db, project) {
15374
+ const rows = db.select({ id: queries.id, query: queries.query }).from(queries).where(eq32(queries.projectId, project.id)).all();
15375
+ const classes = readQueryClasses(db, project, rows);
15376
+ if (rows.length > 0 && rows.every((row) => classes.get(row.id) == null)) {
15377
+ throw validationError("Canonry cannot tell this project's branded queries apart without a brand name. Add one before setting up market rankings.");
15378
+ }
15379
+ const nonBrand = rows.filter((row) => classes.get(row.id) === queryClassSchema.enum["non-brand"]).map((row) => ({ id: row.id, text: row.query, executionNodeKeys: [] }));
15380
+ const locations = project.locations.length > 0 ? project.locations.map((location) => ({ key: location.label, label: location.label })) : [{ key: SEO_PROJECT_MARKET_KEY, label: "Project" }];
15381
+ return locations.map((location) => ({ context: { marketKey: location.key, marketLabel: location.label, regionKey: null, regionLabel: null }, locationKeys: [location.key], queries: nonBrand }));
15382
+ }
15383
+ function marketRankingPlan(db, projectId, input) {
15384
+ if (!input.capabilities.includes(SeoCapabilities["market-rankings"]) || !input.rankings) return NO_MARKET_RANKINGS;
15385
+ const settings = input.rankings;
15386
+ const project = db.select().from(projects).where(eq32(projects.id, projectId)).get();
15387
+ if (!project) throw notFound("Project", projectId);
15388
+ const own = new Set(effectiveDomains({ canonicalDomain: project.canonicalDomain, ownedDomains: project.ownedDomains }).map((domain) => registrableDomain(domain)).filter(Boolean));
15389
+ const target = input.targets[0];
15390
+ if (!target || !own.has(registrableDomain(target.value))) throw validationError("The market-rankings Target must be one of this project's own domains.");
15391
+ const active = activeMeasurementPlan(db, projectId);
15392
+ const advanced = active?.plan.schemaVersion === 2 ? { versionId: active.version.id, plan: active.plan } : null;
15393
+ const markets = advanced ? advancedMarketQueries(advanced.plan, input.scope) : simpleMarketQueries(db, project);
15394
+ const rival = (domain) => {
15395
+ const registrable = registrableDomain(normalizeHostname(domain) ?? domain);
15396
+ return registrable && !own.has(registrable) ? [registrable] : [];
15397
+ };
15398
+ const projectCompetitors = db.select({ domain: competitors.domain }).from(competitors).where(eq32(competitors.projectId, projectId)).all().flatMap((row) => rival(row.domain));
15399
+ const pinnedFor = createPlanPinLookup(db);
15400
+ const competitorsFor = (query) => [.../* @__PURE__ */ new Set([...projectCompetitors, ...query.executionNodeKeys.flatMap((key) => pinnedFor(advanced?.versionId, key)).flatMap(rival)])].sort(compareText);
15401
+ const locations = new Map(settings.locations.map((location) => [location.key, location]));
15402
+ const searches = /* @__PURE__ */ new Map();
15403
+ const unmapped = [];
15404
+ for (const market of markets) {
15405
+ const mapped = market.locationKeys.map((key) => locations.get(key)).find((location2) => location2 !== void 0);
15406
+ const location = mapped ?? (market.context.marketKey === SEO_PROJECT_MARKET_KEY ? { locationCode: input.locationCode, label: `Location ${input.locationCode}` } : void 0);
15407
+ if (!location) {
15408
+ unmapped.push(market.context.marketLabel);
15409
+ continue;
15410
+ }
15411
+ for (const query of market.queries) {
15412
+ const searchText = seoSearchTextForQuery(query.text, market.context.marketKey === SEO_PROJECT_MARKET_KEY ? null : market.context.marketLabel, settings.searchText);
15413
+ if (!searchText) continue;
15414
+ const answers = {
15415
+ context: { ...market.context, queryId: query.id, queryText: query.text, queryClass: queryClassSchema.enum["non-brand"] },
15416
+ competitors: competitorsFor(query)
15417
+ };
15418
+ const key = `${location.locationCode}\0${searchText.toLowerCase()}`;
15419
+ const existing = searches.get(key);
15420
+ if (existing) existing.markets.push(answers);
15421
+ else searches.set(key, { searchText, locationCode: location.locationCode, locationLabel: location.label, markets: [answers] });
15422
+ }
15423
+ }
15424
+ if (unmapped.length) throw validationError(`Set a search location for ${unmapped.length === 1 ? "1 market" : `${unmapped.length} markets`} or their groups: ${unmapped.slice(0, 5).join("; ")}${unmapped.length > 5 ? "; \u2026" : ""}.`);
15425
+ if (!searches.size) throw validationError("There are no non-brand tracked queries to search in this scope.");
15426
+ if (searches.size > SEO_MARKET_RANKING_MAX_SEARCHES) throw validationError(`One market-rankings refresh runs at most ${SEO_MARKET_RANKING_MAX_SEARCHES} searches; this scope needs ${searches.size}. Select a narrower scope.`);
15427
+ return { searches: [...searches.values()].sort((a, b) => compareText(a.locationLabel, b.locationLabel) || compareText(a.searchText, b.searchText)) };
15428
+ }
15429
+ function taskRequests(profile, rankings) {
15430
+ return profile.capabilities.flatMap((capability) => {
15431
+ if (capability === SeoCapabilities["market-rankings"]) {
15432
+ const settings = profile.rankings;
15433
+ return rankings.searches.map((search) => ({
15434
+ capability,
15435
+ target: profile.targets[0],
15436
+ keywords: [search.searchText],
15437
+ locationCode: search.locationCode,
15438
+ languageCode: profile.languageCode,
15439
+ limit: profile.limit,
15440
+ tag: "",
15441
+ search: { device: settings.device, depth: settings.depth, locationLabel: search.locationLabel, markets: search.markets }
15442
+ }));
15443
+ }
15444
+ return (capability === SeoCapabilities["keyword-metrics"] ? [null] : profile.targets).map((target) => ({ capability, target, keywords: capability === SeoCapabilities["keyword-metrics"] ? profile.keywords : [], locationCode: profile.locationCode, languageCode: profile.languageCode, limit: profile.limit, tag: "" }));
15445
+ });
15278
15446
  }
15279
- function planSeoTasks(opts, input) {
15447
+ function planSeoTasks(opts, input, rankings = NO_MARKET_RANKINGS) {
15280
15448
  const runtime = opts.getSeoRuntime?.() ?? null;
15281
15449
  const connection = runtime?.connections.find((connection2) => connection2.id === input.connectionId);
15282
15450
  if (!runtime || !opts.onSeoRunRequested) throw missingDependency("SEO execution is not configured on this host.");
@@ -15284,7 +15452,7 @@ function planSeoTasks(opts, input) {
15284
15452
  if (input.capabilities.some((capability) => !connection.adapter.capabilities.includes(capability))) throw validationError("The selected SEO connection does not support every requested capability. Select capabilities listed for this connection in SEO context.");
15285
15453
  let planned;
15286
15454
  try {
15287
- planned = taskRequests(input).map((request) => {
15455
+ planned = taskRequests(input, rankings).map((request) => {
15288
15456
  const estimate = connection.adapter.estimate(request);
15289
15457
  return { request, reservedMicros: estimate.maxCostMicros, catalogRevision: estimate.catalogRevision };
15290
15458
  });
@@ -15304,14 +15472,20 @@ function budgetBuckets(account, projectId, policy, now) {
15304
15472
  { scope: `project-month:${projectId}`, period: now.slice(0, 7), limit: policy.projectMonthlyLimitMicros }
15305
15473
  ];
15306
15474
  }
15475
+ function skippedRefreshReason(profileId, reason) {
15476
+ return `Scheduled SEO refresh for ${profileId ? `profile "${profileId}"` : "this project"} did not start: ${reason}`;
15477
+ }
15478
+ var SKIPPED_REFRESH_PROFILE = /^Scheduled SEO refresh for profile "([^"]+)" did not start: /;
15479
+ function skippedRefreshProfile(error) {
15480
+ return error ? SKIPPED_REFRESH_PROFILE.exec(error)?.[1] ?? null : null;
15481
+ }
15307
15482
  function startScheduledSeoRefreshes(db, opts, input) {
15308
15483
  const repository = new SeoRepository(db);
15309
15484
  const principalId = `schedule:${input.scheduleId}`;
15310
15485
  const recordSkipped = (profileId, reason, detail) => {
15311
15486
  const runId = crypto17.randomUUID();
15312
15487
  const now = (/* @__PURE__ */ new Date()).toISOString();
15313
- const subject = profileId ? `profile "${profileId}"` : "this project";
15314
- db.insert(runs).values({ id: runId, projectId: input.projectId, kind: RunKinds["seo-sync"], status: RunStatuses.failed, trigger: RunTriggers.scheduled, createdAt: now, finishedAt: now, error: `Scheduled SEO refresh for ${subject} did not start: ${reason}` }).run();
15488
+ db.insert(runs).values({ id: runId, projectId: input.projectId, kind: RunKinds["seo-sync"], status: RunStatuses.failed, trigger: RunTriggers.scheduled, createdAt: now, finishedAt: now, error: skippedRefreshReason(profileId, reason) }).run();
15315
15489
  return { profileId, status: "skipped", runId, reason, ...detail ? { detail } : {} };
15316
15490
  };
15317
15491
  const saved = db.select().from(seoProfiles).where(eq32(seoProfiles.projectId, input.projectId)).orderBy(asc6(seoProfiles.id)).all().map(profileDto);
@@ -15322,7 +15496,7 @@ function startScheduledSeoRefreshes(db, opts, input) {
15322
15496
  if (prior?.runId) return { profileId: profile.id, status: "replayed", runId: prior.runId };
15323
15497
  try {
15324
15498
  const scopeSnapshot = resolveScope2(db, input.projectId, profile);
15325
- const runtime = planSeoTasks(opts, profile);
15499
+ const runtime = planSeoTasks(opts, profile, marketRankingPlan(db, input.projectId, profile));
15326
15500
  const now = (/* @__PURE__ */ new Date()).toISOString();
15327
15501
  const account = runtime.connection.adapter.accountKey;
15328
15502
  const requestedId = crypto17.randomUUID();
@@ -15363,8 +15537,8 @@ async function seoRoutes(app, opts) {
15363
15537
  if (!row) throw notFound("SEO profile", id);
15364
15538
  return profileDto(row);
15365
15539
  };
15366
- const tasks = (id) => app.db.select().from(seoTasks).where(eq32(seoTasks.jobId, id)).orderBy(asc6(seoTasks.id)).all();
15367
- const taskCounts = (ids) => ids.length ? app.db.select({ jobId: seoTasks.jobId, status: seoTasks.status, count: sql8`count(*)` }).from(seoTasks).where(inArray16(seoTasks.jobId, ids)).groupBy(seoTasks.jobId, seoTasks.status).all() : [];
15540
+ const tasks = (id) => currentSeoTasks(app.db.select().from(seoTasks).where(eq32(seoTasks.jobId, id)).orderBy(asc6(seoTasks.id)).all());
15541
+ const taskCounts = (ids) => ids.length ? app.db.select({ jobId: seoTasks.jobId, status: seoTasks.status, count: sql8`count(*)` }).from(seoTasks).where(and24(inArray16(seoTasks.jobId, ids), currentSeoTaskCondition())).groupBy(seoTasks.jobId, seoTasks.status).all() : [];
15368
15542
  const runSummary = (row) => runDto(row, taskCounts([row.id]));
15369
15543
  const job = (projectId, id) => {
15370
15544
  const row = app.db.select().from(seoJobs).where(and24(eq32(seoJobs.projectId, projectId), isNotNull3(seoJobs.runId), eq32(seoJobs.id, id))).get();
@@ -15394,7 +15568,7 @@ async function seoRoutes(app, opts) {
15394
15568
  const available = !!runtime?.connections.length && !!opts.onSeoRunRequested;
15395
15569
  return { available, reason: available ? null : "SEO execution is not configured on this host.", capabilities: [...new Set(runtime?.connections.flatMap((connection) => [...connection.adapter.capabilities]) ?? [])], profileCount: profiles(projectId).length, latestRunId: latest?.id ?? null, latestRunStatus: latest?.status ?? null, lastCompletedAt: last?.finishedAt ?? null };
15396
15570
  };
15397
- const planTasks = (input) => planSeoTasks(opts, input);
15571
+ const planTasks = (projectId, input) => planSeoTasks(opts, input, marketRankingPlan(app.db, projectId, input));
15398
15572
  app.get("/seo/operator/tasks/:id", async (request) => {
15399
15573
  requireOperator(request);
15400
15574
  const task = app.db.select().from(seoTasks).where(eq32(seoTasks.id, request.params.id)).get();
@@ -15422,7 +15596,7 @@ async function seoRoutes(app, opts) {
15422
15596
  const plan = active?.plan.schemaVersion === 2 ? active.plan : null;
15423
15597
  const scopes = plan ? planScopeOptions(plan, { marketLinks: true }).map((option) => {
15424
15598
  const capabilities = scopeCapabilities(option.kind, scopedPlanTargets(plan, { kind: option.kind, ...option.kind === SeoScopeKinds.project ? {} : { key: option.id } }).targets).filter((capability) => current.capabilities.includes(capability));
15425
- return { kind: option.kind, key: option.kind === SeoScopeKinds.project ? null : option.id, label: option.label, planRevision: active.version.revision, capabilities, supported: capabilities.length > 0, reason: !capabilities.length ? current.reason ?? "No configured connection supports this scope." : option.kind === SeoScopeKinds.project ? null : capabilities.includes(SeoCapabilities["ranked-keywords"]) ? "Supports explicit keyword metrics and exact-host ranked keywords. Whole-domain intelligence requires project scope." : "Supports explicit keyword metrics only. Path and entity Properties cannot be replaced by whole-domain data." };
15599
+ return { kind: option.kind, key: option.kind === SeoScopeKinds.project ? null : option.id, label: option.label, planRevision: active.version.revision, capabilities, supported: capabilities.length > 0, reason: !capabilities.length ? current.reason ?? "No configured connection supports this scope." : option.kind === SeoScopeKinds.project ? null : scopeReason(capabilities) };
15426
15600
  }) : [{ kind: SeoScopeKinds.project, key: null, label: "Project", capabilities: active ? [] : current.capabilities, supported: !active && current.capabilities.length > 0, reason: active ? "Publish a current Advanced Measurement plan first." : current.reason }];
15427
15601
  return { available: current.available, reason: current.reason, connections: getRuntime()?.connections.map((connection) => ({ id: connection.id, label: connection.label, provider: connection.adapter.provider, capabilities: [...connection.adapter.capabilities] })) ?? [], scopes, access: { canRun: can(request, SEO_RUN_SCOPE), canWrite: can(request, SEO_WRITE_SCOPE) } };
15428
15602
  });
@@ -15438,7 +15612,7 @@ async function seoRoutes(app, opts) {
15438
15612
  if (!parsed.success) throw validationError("Invalid SEO profile.", { issues: parsed.error.issues });
15439
15613
  const input = normalizeProfile(parsed.data);
15440
15614
  resolveScope2(app.db, project.id, input);
15441
- planTasks(input);
15615
+ planTasks(project.id, input);
15442
15616
  const now = (/* @__PURE__ */ new Date()).toISOString();
15443
15617
  app.db.transaction((tx) => {
15444
15618
  const current = tx.select().from(seoProfiles).where(and24(eq32(seoProfiles.projectId, project.id), eq32(seoProfiles.id, request.params.id))).get();
@@ -15454,7 +15628,7 @@ async function seoRoutes(app, opts) {
15454
15628
  if (!parsed.success) throw validationError("Select an SEO profile.");
15455
15629
  const saved = profile(project.id, parsed.data.profileId);
15456
15630
  const scope = resolveScope2(app.db, project.id, saved);
15457
- const planned = planTasks(saved);
15631
+ const planned = planTasks(project.id, saved);
15458
15632
  const expiresAt = new Date(Date.now() + 5 * 6e4).toISOString();
15459
15633
  const payload = Buffer.from(JSON.stringify({ projectId: project.id, actor: actor(request), profileId: saved.id, revision: saved.revision, scopeHash: hash2(JSON.stringify(scope)), intentHash: planned.intentHash, nonce: crypto17.randomUUID(), expiresAt })).toString("base64url");
15460
15634
  const signature = crypto17.createHmac("sha256", intentSecret).update(payload).digest("base64url");
@@ -15491,7 +15665,7 @@ async function seoRoutes(app, opts) {
15491
15665
  const saved = profile(project.id, claims.profileId);
15492
15666
  const scopeSnapshot = resolveScope2(app.db, project.id, saved);
15493
15667
  if (saved.revision !== claims.revision || hash2(JSON.stringify(scopeSnapshot)) !== claims.scopeHash) throw validationError("This SEO profile or measurement scope changed. Create a new refresh intent.");
15494
- const runtime = planTasks(saved);
15668
+ const runtime = planTasks(project.id, saved);
15495
15669
  if (runtime.intentHash !== claims.intentHash) throw validationError("The SEO connection or host policy changed. Create a new refresh intent.");
15496
15670
  const now = (/* @__PURE__ */ new Date()).toISOString();
15497
15671
  const account = runtime.connection.adapter.accountKey;
@@ -15515,7 +15689,7 @@ async function seoRoutes(app, opts) {
15515
15689
  const row = job(resolveProject(app.db, request.params.name).id, request.params.id);
15516
15690
  const rows = tasks(row.id);
15517
15691
  const results = resultRows(rows);
15518
- return { ...runDto(row, rows.map((task) => ({ status: task.status, count: 1 }))), tasks: rows.map((task) => ({ id: task.id, capability: task.request.capability, target: task.request.target, status: task.status, error: task.error, remoteTaskId: task.receipts[0]?.remoteTaskId ?? null })), results: { keywordMetrics: results.keywordMetrics.slice(0, 20), domainIntelligence: results.domainIntelligence.slice(0, 20), rankedKeywords: results.rankedKeywords.slice(0, 20) }, resultCounts: { keywordMetrics: results.keywordMetrics.length, domainIntelligence: results.domainIntelligence.length, rankedKeywords: results.rankedKeywords.length }, resultsTruncated: Object.values(results).some((items) => items.length > 20) };
15692
+ return { ...runDto(row, rows.map((task) => ({ status: task.status, count: 1 }))), tasks: rows.map((task) => ({ id: task.id, capability: task.request.capability, target: task.request.target, status: task.status, error: task.error, remoteTaskId: task.receipts[0]?.remoteTaskId ?? null, ...taskSearch(task), retried: task.retryOf !== null })), results: { keywordMetrics: results.keywordMetrics.slice(0, 20), domainIntelligence: results.domainIntelligence.slice(0, 20), rankedKeywords: results.rankedKeywords.slice(0, 20), marketRankings: [...results.marketRankings].sort(compareMarketRankings).slice(0, 20) }, resultCounts: { keywordMetrics: results.keywordMetrics.length, domainIntelligence: results.domainIntelligence.length, rankedKeywords: results.rankedKeywords.length, marketRankings: results.marketRankings.length }, resultsTruncated: Object.values(results).some((items) => items.length > 20) };
15519
15693
  });
15520
15694
  app.post("/projects/:name/seo/runs/:id/cancel", { config: { paidRead: true, writeScope: SEO_RUN_SCOPE } }, async (request) => {
15521
15695
  requireScope(request, SEO_RUN_SCOPE);
@@ -15534,7 +15708,7 @@ async function seoRoutes(app, opts) {
15534
15708
  const results = resultRows(rows);
15535
15709
  const primary = row?.profile.targets[0];
15536
15710
  const domain = primary ? results.domainIntelligence.find((result) => result.target.kind === primary.kind && result.target.value === primary.value && result.target.includeSubdomains === primary.includeSubdomains) : null;
15537
- return { status: status(project.id), profiles: profiles(project.id), latestRun: latest ? runSummary(latest) : null, evidenceRunId: row?.id ?? null, evidenceRun: row ? runDto(row, rows.map((task) => ({ status: task.status, count: 1 }))) : null, keywordMetricsCount: results.keywordMetrics.length, domainIntelligenceCount: results.domainIntelligence.length, rankedKeywordCount: results.rankedKeywords.length, summary: domain ? { target: domain.target, organicKeywords: domain.organicKeywords, inTop10: domain.top1 !== null && domain.top2To3 !== null && domain.top4To10 !== null ? domain.top1 + domain.top2To3 + domain.top4To10 : null, estimatedOrganicTraffic: domain.estimatedOrganicTraffic, provenance: domain.provenance } : null };
15711
+ return { status: status(project.id), profiles: profiles(project.id), latestRun: latest ? runSummary(latest) : null, evidenceRunId: row?.id ?? null, evidenceRun: row ? runDto(row, rows.map((task) => ({ status: task.status, count: 1 }))) : null, keywordMetricsCount: results.keywordMetrics.length, domainIntelligenceCount: results.domainIntelligence.length, rankedKeywordCount: results.rankedKeywords.length, marketRankingCount: results.marketRankings.length, summary: domain ? { target: domain.target, organicKeywords: domain.organicKeywords, inTop10: domain.top1 !== null && domain.top2To3 !== null && domain.top4To10 !== null ? domain.top1 + domain.top2To3 + domain.top4To10 : null, estimatedOrganicTraffic: domain.estimatedOrganicTraffic, provenance: domain.provenance } : null };
15538
15712
  });
15539
15713
  for (const [path8, key] of [["keyword-metrics", "keywordMetrics"], ["domain-intelligence", "domainIntelligence"], ["ranked-keywords", "rankedKeywords"]]) app.get(`/projects/:name/seo/${path8}`, async (request) => {
15540
15714
  const query = queryInput(request.query);
@@ -15544,6 +15718,17 @@ async function seoRoutes(app, opts) {
15544
15718
  const offset = query.offset ?? 0;
15545
15719
  return { runId: row?.id ?? null, items: items.slice(offset, offset + limit), total: items.length, limit, offset };
15546
15720
  });
15721
+ app.get("/projects/:name/seo/market-rankings", async (request) => {
15722
+ const raw = request.query ?? {};
15723
+ const parsed = seoMarketRankingQuerySchema.safeParse({ ...raw, ...raw.limit === void 0 ? {} : { limit: Number(raw.limit) }, ...raw.offset === void 0 ? {} : { offset: Number(raw.offset) } });
15724
+ if (!parsed.success) throw validationError("Invalid SEO evidence selection.");
15725
+ const query = parsed.data;
15726
+ const row = selected(resolveProject(app.db, request.params.name).id, query, true);
15727
+ const items = (row ? resultRows(tasks(row.id)).marketRankings : []).filter((item2) => (query.marketKey === void 0 || item2.marketKey === query.marketKey) && (query.regionKey === void 0 || item2.regionKey === query.regionKey)).sort(compareMarketRankings);
15728
+ const limit = query.limit ?? 50;
15729
+ const offset = query.offset ?? 0;
15730
+ return { runId: row?.id ?? null, items: items.slice(offset, offset + limit), total: items.length, limit, offset };
15731
+ });
15547
15732
  }
15548
15733
 
15549
15734
  // ../api-routes/src/runs.ts
@@ -26989,7 +27174,7 @@ async function applyRoutes(app, opts) {
26989
27174
  timezone: resolvedSchedule.timezone,
26990
27175
  nextRunAt,
26991
27176
  providers: config.spec.schedule?.providers ?? [],
26992
- ...specEnabled === void 0 ? {} : { enabled: specEnabled },
27177
+ ...specEnabled === void 0 ? {} : { enabled: specEnabled, enabledAt: scheduleEnabledAt(existingSched, specEnabled, now) },
26993
27178
  updatedAt: nextScheduleUpdatedAt(existingSched.updatedAt, Date.parse(now))
26994
27179
  }).where(eq44(schedules.id, existingSched.id)).run();
26995
27180
  } else {
@@ -27003,6 +27188,7 @@ async function applyRoutes(app, opts) {
27003
27188
  timezone: resolvedSchedule.timezone,
27004
27189
  nextRunAt,
27005
27190
  enabled: specEnabled ?? true,
27191
+ enabledAt: scheduleEnabledAt(void 0, specEnabled ?? true, now),
27006
27192
  providers: config.spec.schedule?.providers ?? [],
27007
27193
  createdAt: now,
27008
27194
  updatedAt: now
@@ -33750,6 +33936,7 @@ var SCHEMA_TABLE = {
33750
33936
  SeoKeywordMetricsList: seoKeywordMetricsListSchema,
33751
33937
  SeoDomainIntelligenceList: seoDomainIntelligenceListSchema,
33752
33938
  SeoRankedKeywordList: seoRankedKeywordListSchema,
33939
+ SeoMarketRankingList: seoMarketRankingListSchema,
33753
33940
  SentimentSummary: sentimentSummarySchema,
33754
33941
  SentimentSettings: sentimentSettingsSchema,
33755
33942
  SentimentSettingsUpdate: sentimentSettingsUpdateSchema,
@@ -34977,6 +35164,26 @@ var routeCatalog = [
34977
35164
  409: errorResponse("Selection, intent or execution identity changed.")
34978
35165
  }
34979
35166
  },
35167
+ {
35168
+ method: "get",
35169
+ path: "/api/v1/projects/{name}/seo/market-rankings",
35170
+ summary: "Read stored market rankings",
35171
+ tags: ["seo"],
35172
+ description: "Stored evidence only; never calls a provider. The project's position for each tracked non-brand query, searched on Google from its market's location, ordered by region, market and search text. searchStatus says how each search ended: in a completed or partial row a null rank means the site was not in the checked results (depth, which is lower when only part of the results came back); a failed or cancelled search keeps its rows with no rank.",
35173
+ parameters: [
35174
+ nameParameter,
35175
+ ...seoEvidenceParameters,
35176
+ { name: "marketKey", in: "query", description: "Only rows for this market (an Advanced market key or a Simple location label).", schema: stringSchema },
35177
+ { name: "regionKey", in: "query", description: "Only rows whose market sits under this top-level group.", schema: stringSchema }
35178
+ ],
35179
+ responses: {
35180
+ 200: jsonResponse("SEO response.", "SeoMarketRankingList"),
35181
+ 400: errorResponse("Invalid scope or input."),
35182
+ 403: errorResponse("Insufficient authority."),
35183
+ 404: errorResponse("Project or selected evidence not found."),
35184
+ 409: errorResponse("Selection, intent or execution identity changed.")
35185
+ }
35186
+ },
34980
35187
  {
34981
35188
  method: "put",
34982
35189
  path: "/api/v1/projects/{name}/seo/profiles/{id}",
@@ -42929,6 +43136,7 @@ async function scheduleRoutes(app, opts) {
42929
43136
  providers: providers ?? [],
42930
43137
  sourceId: sourceId ?? null,
42931
43138
  enabled: enabledBool,
43139
+ enabledAt: scheduleEnabledAt(existing, enabledBool, now),
42932
43140
  updatedAt: now
42933
43141
  };
42934
43142
  if (existing) {
@@ -59673,11 +59881,11 @@ async function wordpressRoutes(app, opts) {
59673
59881
  reportMetaStep(metaWriteOutcome(result.results));
59674
59882
  const applied = result.results.filter((r) => r.status === "applied").length;
59675
59883
  const manual = result.results.filter((r) => r.status === "manual").length;
59676
- const skipped5 = result.results.filter((r) => r.status === "skipped").length;
59884
+ const skipped6 = result.results.filter((r) => r.status === "skipped").length;
59677
59885
  steps.push({
59678
59886
  name: "set-meta",
59679
59887
  status: "completed",
59680
- summary: `${applied} applied, ${manual} manual-assist, ${skipped5} skipped`
59888
+ summary: `${applied} applied, ${manual} manual-assist, ${skipped6} skipped`
59681
59889
  });
59682
59890
  }
59683
59891
  } catch (err) {
@@ -59707,11 +59915,11 @@ async function wordpressRoutes(app, opts) {
59707
59915
  reportSchemaStep(schemaDeployOutcome(result.results));
59708
59916
  const deployed = result.results.filter((r) => r.status === "deployed").length;
59709
59917
  const stripped = result.results.filter((r) => r.status === "stripped").length;
59710
- const skipped5 = result.results.filter((r) => r.status === "skipped").length;
59918
+ const skipped6 = result.results.filter((r) => r.status === "skipped").length;
59711
59919
  steps.push({
59712
59920
  name: "schema-deploy",
59713
59921
  status: "completed",
59714
- summary: `${deployed} deployed, ${stripped} stripped (manual-assist), ${skipped5} skipped`
59922
+ summary: `${deployed} deployed, ${stripped} stripped (manual-assist), ${skipped6} skipped`
59715
59923
  });
59716
59924
  }
59717
59925
  } catch (err) {
@@ -60099,7 +60307,7 @@ async function queryBacklinks(opts) {
60099
60307
  const reversed = opts.targets.map(reverseDomain);
60100
60308
  const targetList = reversed.map(quote).join(", ");
60101
60309
  const limitClause = opts.limitPerTarget ? `QUALIFY row_number() OVER (PARTITION BY t.target_rev_domain ORDER BY v.num_hosts DESC) <= ${Math.floor(opts.limitPerTarget)}` : "";
60102
- const sql37 = `
60310
+ const sql38 = `
60103
60311
  WITH vertices AS (
60104
60312
  SELECT * FROM read_csv(
60105
60313
  ${quote(opts.vertexPath)},
@@ -60135,7 +60343,7 @@ async function queryBacklinks(opts) {
60135
60343
  const conn = await instance.connect();
60136
60344
  let rows;
60137
60345
  try {
60138
- const reader = await conn.runAndReadAll(sql37);
60346
+ const reader = await conn.runAndReadAll(sql38);
60139
60347
  rows = reader.getRowObjects();
60140
60348
  } finally {
60141
60349
  conn.disconnectSync?.();
@@ -62742,6 +62950,7 @@ function bindTrafficSyncSchedule(tx, projectId, sourceId, now, createIfMissing =
62742
62950
  preset: null,
62743
62951
  timezone: "UTC",
62744
62952
  enabled: true,
62953
+ enabledAt: now,
62745
62954
  providers: [],
62746
62955
  sourceId,
62747
62956
  createdAt: now,
@@ -67251,6 +67460,195 @@ var gscRecentDataCheck = {
67251
67460
  };
67252
67461
  var DATA_FRESHNESS_CHECKS = [gaRecentDataCheck, gscRecentDataCheck];
67253
67462
 
67463
+ // ../api-routes/src/doctor/checks/seo-refresh.ts
67464
+ import { and as and75, asc as asc20, desc as desc42, eq as eq89, gt as gt8, inArray as inArray39, sql as sql32 } from "drizzle-orm";
67465
+ var GRACE_DAYS = 2;
67466
+ var DAY_MS5 = 24 * 60 * 60 * 1e3;
67467
+ var FIRING_SPAN_MS = 6e4;
67468
+ var REASON_LIMIT = 300;
67469
+ function skipped2(code, summary3) {
67470
+ return { status: CheckStatuses.skipped, code, summary: summary3, remediation: null };
67471
+ }
67472
+ function seoSyncRuns(projectId) {
67473
+ return and75(eq89(runs.projectId, projectId), eq89(runs.kind, RunKinds["seo-sync"]));
67474
+ }
67475
+ function scheduleTiming(schedule) {
67476
+ if (!schedule.recurrence) return { cronExpr: schedule.cronExpr, timezone: schedule.timezone, recurrence: null };
67477
+ const recurrence = calendarRecurrenceSchema.safeParse(schedule.recurrence).data;
67478
+ return recurrence ? { cronExpr: schedule.cronExpr, timezone: schedule.timezone, recurrence } : null;
67479
+ }
67480
+ function latestFiring(ctx, projectId, enabledSince) {
67481
+ const scheduled = and75(seoSyncRuns(projectId), eq89(runs.trigger, RunTriggers.scheduled), gt8(runs.createdAt, enabledSince));
67482
+ const newest = ctx.db.select({ createdAt: runs.createdAt }).from(runs).where(scheduled).orderBy(desc42(runs.createdAt)).limit(1).get();
67483
+ if (!newest) return [];
67484
+ const newestMs = Date.parse(newest.createdAt);
67485
+ const after = Number.isFinite(newestMs) ? new Date(newestMs - FIRING_SPAN_MS).toISOString() : newest.createdAt;
67486
+ return ctx.db.select({ id: runs.id, status: runs.status, error: runs.error, createdAt: runs.createdAt }).from(runs).where(and75(scheduled, gt8(runs.createdAt, after))).orderBy(asc20(runs.createdAt), asc20(runs.id)).all();
67487
+ }
67488
+ function lastCompleted(ctx, projectId) {
67489
+ const at = sql32`coalesce(${runs.finishedAt}, ${runs.createdAt})`;
67490
+ return ctx.db.select({ id: runs.id, at }).from(runs).where(and75(seoSyncRuns(projectId), eq89(runs.status, RunStatuses.completed))).orderBy(desc42(at)).limit(1).get() ?? null;
67491
+ }
67492
+ function isIncomplete(run) {
67493
+ return run.status === RunStatuses.failed || run.status === RunStatuses.partial;
67494
+ }
67495
+ function runProfiles(ctx, firing) {
67496
+ const jobs = firing.length === 0 ? [] : ctx.db.select({ runId: seoJobs.runId, profileId: seoJobs.profileId }).from(seoJobs).where(inArray39(seoJobs.runId, firing.map((run) => run.id))).all();
67497
+ const byRun = new Map(jobs.flatMap((job) => job.runId ? [[job.runId, job.profileId]] : []));
67498
+ return new Map(firing.map((run) => [run.id, byRun.get(run.id) ?? skippedRefreshProfile(run.error)]));
67499
+ }
67500
+ function unsettledRuns(ctx, projectId, firing, profiles) {
67501
+ const incomplete = firing.filter(isIncomplete);
67502
+ const firedAt = firing[firing.length - 1]?.createdAt;
67503
+ if (incomplete.length === 0 || !firedAt) return incomplete;
67504
+ const refreshed = new Set(ctx.db.select({ profileId: seoJobs.profileId }).from(runs).innerJoin(seoJobs, eq89(seoJobs.runId, runs.id)).where(and75(seoSyncRuns(projectId), eq89(runs.status, RunStatuses.completed), gt8(runs.createdAt, firedAt))).all().map((row) => row.profileId));
67505
+ return incomplete.filter((run) => {
67506
+ const profile = profiles.get(run.id) ?? null;
67507
+ return profile === null ? refreshed.size === 0 : !refreshed.has(profile);
67508
+ });
67509
+ }
67510
+ var COUNT_LIST = new Intl.ListFormat("en", { style: "long", type: "conjunction" });
67511
+ function refreshReason(ctx, run) {
67512
+ const stored = parseRunError(run.error);
67513
+ if (stored) return formatRunErrorOneLine(stored);
67514
+ const tasks = currentSeoTasks(ctx.db.select({ id: seoTasks.id, retryOf: seoTasks.retryOf, status: seoTasks.status, error: seoTasks.error, request: seoTasks.request }).from(seoTasks).innerJoin(seoJobs, eq89(seoTasks.jobId, seoJobs.id)).where(eq89(seoJobs.runId, run.id)).all());
67515
+ const failed = tasks.filter((task) => task.status === SeoRunStatuses.failed || task.status === SeoRunStatuses["reconciliation-required"]);
67516
+ const partial = tasks.filter((task) => task.status === SeoRunStatuses.partial).length;
67517
+ const cancelled = tasks.filter((task) => task.status === SeoRunStatuses.cancelled).length;
67518
+ const errors = [...new Set(failed.flatMap((task) => task.error ? [task.error] : []))].join(" ");
67519
+ if (tasks.length <= 1) {
67520
+ if (errors) return errors;
67521
+ if (partial > 0) return "Some SEO results could not be collected.";
67522
+ return cancelled > 0 ? "The refresh was cancelled." : "No reason was recorded.";
67523
+ }
67524
+ const unit = tasks.every((task) => task.request.capability === SeoCapabilities["market-rankings"]) ? "searches" : "requests";
67525
+ const counts2 = [
67526
+ ...failed.length > 0 ? [`${failed.length} failed`] : [],
67527
+ ...partial > 0 ? [`${partial} returned only part of their results`] : [],
67528
+ ...cancelled > 0 ? [`${cancelled} did not run`] : []
67529
+ ];
67530
+ if (counts2.length === 0) return "No reason was recorded.";
67531
+ return `Of ${tasks.length} ${unit}, ${COUNT_LIST.format(counts2)}.${errors ? ` ${errors}` : ""}`;
67532
+ }
67533
+ function gradeSeoRefresh(ctx, project) {
67534
+ const schedule = ctx.db.select().from(schedules).where(and75(eq89(schedules.projectId, project.id), eq89(schedules.kind, SchedulableRunKinds["seo-sync"]))).get();
67535
+ if (!schedule) return skipped2("seo.data.not-scheduled", "No SEO refresh schedule is set for this project.");
67536
+ if (!schedule.enabled) return skipped2("seo.data.not-scheduled", "The SEO refresh schedule is disabled.");
67537
+ const name = project.name;
67538
+ const day = (iso) => formatIsoDateInTimeZone(iso, schedule.timezone);
67539
+ const now = Date.now();
67540
+ const timing = scheduleTiming(schedule);
67541
+ const last = lastCompleted(ctx, project.id);
67542
+ const enabledSince = schedule.enabledAt ?? schedule.createdAt;
67543
+ const reference = last && Date.parse(last.at) > Date.parse(enabledSince) ? last.at : enabledSince;
67544
+ const dueAt = timing ? nextRunFromSchedule(timing, new Date(reference)) : null;
67545
+ const deadlineAt = dueAt ? new Date(Date.parse(dueAt) + GRACE_DAYS * DAY_MS5).toISOString() : null;
67546
+ const firing = latestFiring(ctx, project.id, enabledSince);
67547
+ const profiles = runProfiles(ctx, firing);
67548
+ const details = {
67549
+ scheduleId: schedule.id,
67550
+ cronExpr: schedule.cronExpr || null,
67551
+ recurrence: schedule.recurrence ?? null,
67552
+ timezone: schedule.timezone,
67553
+ enabledSince,
67554
+ graceDays: GRACE_DAYS,
67555
+ lastCompletedAt: last?.at ?? null,
67556
+ lastCompletedRunId: last?.id ?? null,
67557
+ dueAt,
67558
+ deadlineAt,
67559
+ latestScheduledRuns: firing.map((run) => ({
67560
+ id: run.id,
67561
+ profileId: profiles.get(run.id) ?? null,
67562
+ status: run.status,
67563
+ createdAt: run.createdAt,
67564
+ reason: isIncomplete(run) ? refreshReason(ctx, run) : null
67565
+ }))
67566
+ };
67567
+ if (!timing || !dueAt || !deadlineAt) {
67568
+ return {
67569
+ status: CheckStatuses.warn,
67570
+ code: "seo.data.schedule-invalid",
67571
+ summary: "The SEO refresh schedule has no time it can run, so no scheduled SEO refresh will start.",
67572
+ remediation: `Set a schedule that can run, for example \`canonry schedule set ${name} --kind seo-sync --cron '0 7 2 * *' --timezone UTC\`.`,
67573
+ details,
67574
+ alert: {
67575
+ label: "SEO schedule invalid",
67576
+ line: "The SEO refresh schedule has no time it can run, so SEO data never refreshes.",
67577
+ next: `Set it again with canonry schedule set ${name} --kind seo-sync.`
67578
+ }
67579
+ };
67580
+ }
67581
+ const nextAt = nextRunFromSchedule(timing, new Date(now));
67582
+ const unsettled = unsettledRuns(ctx, project.id, firing, profiles);
67583
+ const newestFired = firing[firing.length - 1];
67584
+ if (newestFired && unsettled.length > 0) {
67585
+ const failed = unsettled.some((run) => run.status === RunStatuses.failed);
67586
+ const firedOn = day(newestFired.createdAt);
67587
+ const reasons = [...new Set(unsettled.map((run) => refreshReason(ctx, run)))].join(" ");
67588
+ const reason = reasons.length > REASON_LIMIT ? `${truncateUtf16(reasons, REASON_LIMIT)}...` : reasons;
67589
+ const outcome2 = firing.length === 1 ? `The scheduled SEO refresh on ${firedOn} ${failed ? "failed" : "only partly completed"}` : `${unsettled.length} of ${firing.length} profile refreshes in the scheduled SEO refresh on ${firedOn} did not fully complete`;
67590
+ const line = firing.length > 1 ? `${outcome2}.` : unsettled[0].error ? reason : `${outcome2}: ${reason}`;
67591
+ const next = nextAt ? ` on ${day(nextAt)}` : "";
67592
+ return {
67593
+ status: CheckStatuses.warn,
67594
+ code: failed ? "seo.data.refresh-failed" : "seo.data.refresh-partial",
67595
+ summary: `${outcome2}. Reason: ${reason} ${last ? `The last completed SEO refresh was on ${day(last.at)}.` : "No SEO refresh has completed yet."}`,
67596
+ remediation: `Fix what the reason names, then refresh now with \`canonry seo refresh-intent ${name} <profile-id>\` and \`canonry seo sync\`, or wait for the next scheduled refresh${next}. A completed refresh of the same profile clears this warning.`,
67597
+ details,
67598
+ alert: {
67599
+ label: failed ? "SEO refresh failed" : "SEO refresh incomplete",
67600
+ line: firstSentence(line) ?? line,
67601
+ next: `Fix the cause, then refresh by hand or wait for the next scheduled refresh${next}.`
67602
+ }
67603
+ };
67604
+ }
67605
+ if (now >= Date.parse(deadlineAt)) {
67606
+ const active = ctx.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and75(seoSyncRuns(project.id), inArray39(runs.status, [RunStatuses.queued, RunStatuses.running]))).orderBy(asc20(runs.createdAt)).limit(1).get();
67607
+ const since = last ? `No SEO refresh has completed since ${day(last.at)}.` : "No SEO refresh has completed since the schedule was enabled.";
67608
+ const missed = last ? `The scheduled refresh due ${day(dueAt)} did not complete within ${GRACE_DAYS} days.` : `Its first refresh, due ${day(dueAt)}, did not complete within ${GRACE_DAYS} days.`;
67609
+ return {
67610
+ status: CheckStatuses.warn,
67611
+ code: "seo.data.overdue",
67612
+ summary: `${since} ${missed}${active ? ` A refresh started ${day(active.createdAt)} has not finished, and scheduled refreshes wait for it.` : ""}`,
67613
+ remediation: active ? `Check that refresh with \`canonry seo run show ${name} ${active.id}\`, and cancel it with \`canonry seo run cancel ${name} ${active.id}\` if it is stuck.` : `Check that the server was running on ${day(dueAt)} and what \`canonry runs ${name} --kind seo-sync\` shows for that day. To refresh now, run \`canonry seo refresh-intent ${name} <profile-id>\`, then \`canonry seo sync\`.`,
67614
+ details: { ...details, activeRunId: active?.id ?? null },
67615
+ alert: {
67616
+ label: "SEO refresh overdue",
67617
+ line: since,
67618
+ next: active ? "Check the refresh that has not finished, and cancel it if it is stuck." : `Check that the server was up on ${day(dueAt)}, then refresh by hand.`
67619
+ }
67620
+ };
67621
+ }
67622
+ const pending = Date.parse(dueAt) <= now;
67623
+ if (!last) {
67624
+ return {
67625
+ status: CheckStatuses.ok,
67626
+ code: "seo.data.awaiting-first-refresh",
67627
+ summary: pending ? `No SEO refresh has completed yet. The first scheduled refresh, due ${day(dueAt)}, has until ${day(deadlineAt)} to complete.` : `No SEO refresh has completed yet. The first scheduled refresh is due ${day(dueAt)}.`,
67628
+ remediation: null,
67629
+ details
67630
+ };
67631
+ }
67632
+ return {
67633
+ status: CheckStatuses.ok,
67634
+ code: "seo.data.fresh",
67635
+ summary: `The last SEO refresh completed on ${day(last.at)}. ${pending ? `The scheduled refresh due ${day(dueAt)} has until ${day(deadlineAt)} to complete.` : `The next scheduled refresh is due ${day(dueAt)}.`}`,
67636
+ remediation: null,
67637
+ details
67638
+ };
67639
+ }
67640
+ var seoRecentRefreshCheck = {
67641
+ id: "seo.data.recent-refresh",
67642
+ category: CheckCategories.integrations,
67643
+ scope: CheckScopes.project,
67644
+ title: "SEO data still refreshing",
67645
+ run: (ctx) => {
67646
+ if (!ctx.project) return skipped2("seo.data.no-project", "Project context required.");
67647
+ return gradeSeoRefresh(ctx, ctx.project);
67648
+ }
67649
+ };
67650
+ var SEO_REFRESH_CHECKS = [seoRecentRefreshCheck];
67651
+
67254
67652
  // ../api-routes/src/site-reachability.ts
67255
67653
  import dns3 from "dns/promises";
67256
67654
  import http3 from "http";
@@ -67454,7 +67852,7 @@ function probeHostFromDomain(value) {
67454
67852
  if (!hostname || !hostname.includes(".") || hostname.endsWith(".localhost")) return null;
67455
67853
  return hostname;
67456
67854
  }
67457
- var skipped2 = (code, summary3) => ({ status: CheckStatuses.skipped, code, summary: summary3, remediation: null });
67855
+ var skipped3 = (code, summary3) => ({ status: CheckStatuses.skipped, code, summary: summary3, remediation: null });
67458
67856
  var siteReachabilityCheck = {
67459
67857
  id: SITE_REACHABILITY_CHECK_ID,
67460
67858
  category: CheckCategories.integrations,
@@ -67465,16 +67863,16 @@ var siteReachabilityCheck = {
67465
67863
  // `canonry doctor --project` must not exit 1 on a single dropped packet.
67466
67864
  optIn: true,
67467
67865
  run: async (ctx) => {
67468
- if (!ctx.project) return skipped2("site.reachability.no-project", "Project context required.");
67866
+ if (!ctx.project) return skipped3("site.reachability.no-project", "Project context required.");
67469
67867
  const host = probeHostFromDomain(ctx.project.canonicalDomain);
67470
67868
  if (!host) {
67471
- return skipped2("site.reachability.no-domain", `Project domain "${(ctx.project.canonicalDomain ?? "").trim()}" is not a probeable hostname.`);
67869
+ return skipped3("site.reachability.no-domain", `Project domain "${(ctx.project.canonicalDomain ?? "").trim()}" is not a probeable hostname.`);
67472
67870
  }
67473
67871
  const url = `https://${host}/`;
67474
67872
  const probe = ctx.probeSiteReachability ?? probeSiteReachability;
67475
67873
  const result = await probe(url);
67476
67874
  if (result.state === "unavailable") {
67477
- return skipped2("site.reachability.probe-unavailable", `Could not probe ${url} from this host: ${result.reason}.`);
67875
+ return skipped3("site.reachability.probe-unavailable", `Could not probe ${url} from this host: ${result.reason}.`);
67478
67876
  }
67479
67877
  if (result.state === "up") {
67480
67878
  return {
@@ -67515,7 +67913,7 @@ var siteReachabilityCheck = {
67515
67913
  var SITE_REACHABILITY_CHECKS = [siteReachabilityCheck];
67516
67914
 
67517
67915
  // ../api-routes/src/doctor/checks/api-hosts.ts
67518
- import { eq as eq90 } from "drizzle-orm";
67916
+ import { eq as eq91 } from "drizzle-orm";
67519
67917
 
67520
67918
  // ../api-routes/src/api-host-probe.ts
67521
67919
  import dns4 from "dns/promises";
@@ -67621,7 +68019,7 @@ async function probeApiHost(host, options = {}) {
67621
68019
  }
67622
68020
 
67623
68021
  // ../api-routes/src/doctor/checks/integration-sync.ts
67624
- import { and as and75, count as count7, desc as desc42, eq as eq89, gte as gte23, gt as gt8, inArray as inArray39, isNull as isNull14, notInArray as notInArray3, or as or19 } from "drizzle-orm";
68022
+ import { and as and76, count as count7, desc as desc43, eq as eq90, gte as gte23, gt as gt9, inArray as inArray40, isNull as isNull14, notInArray as notInArray3, or as or19 } from "drizzle-orm";
67625
68023
  var SYNC_FAILURE_WINDOW = 3;
67626
68024
  var SUMMARY_ERROR_LIMIT = 300;
67627
68025
  var FINISHED_STATUSES = [RunStatuses.completed, RunStatuses.partial, RunStatuses.failed];
@@ -67642,7 +68040,7 @@ function gscConnection(ctx, project) {
67642
68040
  function gbpConnection(ctx, project) {
67643
68041
  if (!ctx.googleConnectionStore) return { state: "store-unavailable" };
67644
68042
  if (!ctx.googleConnectionStore.getConnection(project.canonicalDomain, "gbp")) return { state: "not-connected" };
67645
- const selected = ctx.db.select({ locationName: gbpLocations.locationName }).from(gbpLocations).where(and75(eq89(gbpLocations.projectId, project.id), eq89(gbpLocations.selected, true))).limit(1).get();
68043
+ const selected = ctx.db.select({ locationName: gbpLocations.locationName }).from(gbpLocations).where(and76(eq90(gbpLocations.projectId, project.id), eq90(gbpLocations.selected, true))).limit(1).get();
67646
68044
  return selected ? CONNECTED : { state: "no-selected-locations" };
67647
68045
  }
67648
68046
  function gaConnection(ctx, project) {
@@ -67699,7 +68097,7 @@ var SPECS = [
67699
68097
  causeCheckIds: ["google.auth.connection", "google.auth.property-access", "google.auth.scopes", "gsc.api.reachable"]
67700
68098
  }
67701
68099
  ];
67702
- function skipped3(code, summary3) {
68100
+ function skipped4(code, summary3) {
67703
68101
  return { status: CheckStatuses.skipped, code, summary: summary3, remediation: null };
67704
68102
  }
67705
68103
  function latestErrorText(raw) {
@@ -67707,16 +68105,16 @@ function latestErrorText(raw) {
67707
68105
  return parsed ? formatRunErrorOneLine(parsed) : "no error was recorded";
67708
68106
  }
67709
68107
  function readSyncRunState(db, projectId, kind, options = {}) {
67710
- const scope = and75(
67711
- eq89(runs.projectId, projectId),
67712
- eq89(runs.kind, kind),
67713
- ...options.sourceId ? [eq89(runs.sourceId, options.sourceId)] : [],
68108
+ const scope = and76(
68109
+ eq90(runs.projectId, projectId),
68110
+ eq90(runs.kind, kind),
68111
+ ...options.sourceId ? [eq90(runs.sourceId, options.sourceId)] : [],
67714
68112
  ...options.nothingToSyncErrors?.length ? [or19(isNull14(runs.error), notInArray3(runs.error, [...options.nothingToSyncErrors]))] : []
67715
68113
  );
67716
- const graded = options.since ? and75(scope, gte23(runs.createdAt, options.since)) : scope;
67717
- const recent = db.select({ id: runs.id, status: runs.status, trigger: runs.trigger, error: runs.error, createdAt: runs.createdAt }).from(runs).where(and75(graded, inArray39(runs.status, FINISHED_STATUSES))).orderBy(desc42(runs.createdAt)).limit(SYNC_FAILURE_WINDOW).all();
67718
- const lastSuccess = db.select({ createdAt: runs.createdAt, finishedAt: runs.finishedAt }).from(runs).where(and75(graded, inArray39(runs.status, SUCCESS_STATUSES))).orderBy(desc42(runs.createdAt)).limit(1).get();
67719
- const failureCount = db.select({ count: count7() }).from(runs).where(and75(lastSuccess ? and75(graded, gt8(runs.createdAt, lastSuccess.createdAt)) : graded, eq89(runs.status, RunStatuses.failed))).get();
68114
+ const graded = options.since ? and76(scope, gte23(runs.createdAt, options.since)) : scope;
68115
+ const recent = db.select({ id: runs.id, status: runs.status, trigger: runs.trigger, error: runs.error, createdAt: runs.createdAt }).from(runs).where(and76(graded, inArray40(runs.status, FINISHED_STATUSES))).orderBy(desc43(runs.createdAt)).limit(SYNC_FAILURE_WINDOW).all();
68116
+ const lastSuccess = db.select({ createdAt: runs.createdAt, finishedAt: runs.finishedAt }).from(runs).where(and76(graded, inArray40(runs.status, SUCCESS_STATUSES))).orderBy(desc43(runs.createdAt)).limit(1).get();
68117
+ const failureCount = db.select({ count: count7() }).from(runs).where(and76(lastSuccess ? and76(graded, gt9(runs.createdAt, lastSuccess.createdAt)) : graded, eq90(runs.status, RunStatuses.failed))).get();
67720
68118
  const failuresSinceSuccess = Number(failureCount?.count ?? 0);
67721
68119
  const latest = recent[0];
67722
68120
  return {
@@ -67733,21 +68131,21 @@ function readSyncRunState(db, projectId, kind, options = {}) {
67733
68131
  function gradeSyncFailures(ctx, spec) {
67734
68132
  const code = (suffix) => `${spec.prefix}.sync.${suffix}`;
67735
68133
  const project = ctx.project;
67736
- if (!project) return skipped3(code("no-project"), "Project context required.");
68134
+ if (!project) return skipped4(code("no-project"), "Project context required.");
67737
68135
  const connection = spec.connection(ctx, project);
67738
68136
  if (connection.state === "store-unavailable") {
67739
- return skipped3(code("store-unavailable"), `No ${spec.label} connection store configured for this deployment.`);
68137
+ return skipped4(code("store-unavailable"), `No ${spec.label} connection store configured for this deployment.`);
67740
68138
  }
67741
68139
  if (connection.state === "not-connected") {
67742
- return skipped3(code("not-connected"), `${spec.label} is not connected for this project.`);
68140
+ return skipped4(code("not-connected"), `${spec.label} is not connected for this project.`);
67743
68141
  }
67744
68142
  if (connection.state === "no-selected-locations") {
67745
- return skipped3(code("no-selected-locations"), `No ${spec.label} locations are selected for this project.`);
68143
+ return skipped4(code("no-selected-locations"), `No ${spec.label} locations are selected for this project.`);
67746
68144
  }
67747
68145
  const nothingToSync = spec.nothingToSyncErrors ?? [];
67748
68146
  const state = readSyncRunState(ctx.db, project.id, spec.kind, { since: connection.since, nothingToSyncErrors: nothingToSync });
67749
68147
  if (state.latestStatus === null) {
67750
- return skipped3(code("no-runs"), `${spec.label} is connected but no sync has finished yet.`);
68148
+ return skipped4(code("no-runs"), `${spec.label} is connected but no sync has finished yet.`);
67751
68149
  }
67752
68150
  const latestFailed = state.latestStatus === RunStatuses.failed;
67753
68151
  const details = {
@@ -67803,7 +68201,7 @@ var API_HOST_TABLE = {
67803
68201
  ads: ["api.ads.openai.com"]
67804
68202
  };
67805
68203
  var API_HOST_EXCLUDED = { gsc: [new URL(INDEXING_API_BASE).hostname] };
67806
- function skipped4(code, summary3) {
68204
+ function skipped5(code, summary3) {
67807
68205
  return { status: CheckStatuses.skipped, code, summary: summary3, remediation: null };
67808
68206
  }
67809
68207
  function connectionFor(prefix, ctx, project) {
@@ -67813,7 +68211,7 @@ function connectionFor(prefix, ctx, project) {
67813
68211
  if (prefix === "google-ads") return googleMarketingConnection("googleAds")(ctx);
67814
68212
  if (prefix === "gtm") return googleMarketingConnection("gtm")(ctx);
67815
68213
  if (prefix === "bing") return ctx.bingConnectionStore?.getConnection(project.canonicalDomain) ? { state: "connected", since: null } : { state: "not-connected" };
67816
- return ctx.db.select({ id: adsConnections.id }).from(adsConnections).where(eq90(adsConnections.projectId, project.id)).limit(1).get() ? { state: "connected", since: null } : { state: "not-connected" };
68214
+ return ctx.db.select({ id: adsConnections.id }).from(adsConnections).where(eq91(adsConnections.projectId, project.id)).limit(1).get() ? { state: "connected", since: null } : { state: "not-connected" };
67817
68215
  }
67818
68216
  function syncCommand(prefix) {
67819
68217
  if (prefix === "gsc") return "google sync";
@@ -67827,9 +68225,9 @@ function syncCommand(prefix) {
67827
68225
  async function grade2(prefix, ctx) {
67828
68226
  const id = `${prefix}.api`;
67829
68227
  const checkId = `${id}.reachable`;
67830
- if (!ctx.project) return skipped4(`${id}.no-project`, "Project context required.");
68228
+ if (!ctx.project) return skipped5(`${id}.no-project`, "Project context required.");
67831
68229
  const connection = connectionFor(prefix, ctx, ctx.project);
67832
- if (connection.state !== "connected") return skipped4(`${id}.not-connected`, `${prefix} is not connected for this project.`);
68230
+ if (connection.state !== "connected") return skipped5(`${id}.not-connected`, `${prefix} is not connected for this project.`);
67833
68231
  const probe = ctx.probeApiHost ?? probeApiHost;
67834
68232
  const results = await Promise.all(API_HOST_TABLE[prefix].map((host) => probe(host)));
67835
68233
  const details = { hosts: results };
@@ -67858,7 +68256,7 @@ function definition(prefix) {
67858
68256
  var API_HOST_CHECKS = Object.keys(API_HOST_TABLE).map(definition);
67859
68257
 
67860
68258
  // ../api-routes/src/doctor/checks/gbp-auth.ts
67861
- import { and as and76, eq as eq91 } from "drizzle-orm";
68259
+ import { and as and77, eq as eq92 } from "drizzle-orm";
67862
68260
  var RECENT_SYNC_WARN_DAYS = 4;
67863
68261
  var RECENT_SYNC_FAIL_DAYS = 30;
67864
68262
  function skippedNoProject3() {
@@ -67878,7 +68276,7 @@ function storeUnavailable() {
67878
68276
  };
67879
68277
  }
67880
68278
  function hasSelectedLocation(ctx, projectId) {
67881
- return !!ctx.db.select({ id: gbpLocations.id }).from(gbpLocations).where(and76(eq91(gbpLocations.projectId, projectId), eq91(gbpLocations.selected, true))).limit(1).get();
68279
+ return !!ctx.db.select({ id: gbpLocations.id }).from(gbpLocations).where(and77(eq92(gbpLocations.projectId, projectId), eq92(gbpLocations.selected, true))).limit(1).get();
67882
68280
  }
67883
68281
  async function resolveGbpToken(ctx) {
67884
68282
  if (!ctx.project) return { ok: false, output: skippedNoProject3() };
@@ -68138,7 +68536,7 @@ var recentSyncCheck = {
68138
68536
  title: "GBP recent sync",
68139
68537
  run: (ctx) => {
68140
68538
  if (!ctx.project) return skippedNoProject3();
68141
- const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and76(eq91(gbpLocations.projectId, ctx.project.id), eq91(gbpLocations.selected, true))).all();
68539
+ const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and77(eq92(gbpLocations.projectId, ctx.project.id), eq92(gbpLocations.selected, true))).all();
68142
68540
  if (selected.length === 0) {
68143
68541
  return {
68144
68542
  status: CheckStatuses.skipped,
@@ -68198,7 +68596,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
68198
68596
  );
68199
68597
 
68200
68598
  // ../api-routes/src/doctor/checks/places.ts
68201
- import { eq as eq92 } from "drizzle-orm";
68599
+ import { eq as eq93 } from "drizzle-orm";
68202
68600
  var apiKeyCheck = {
68203
68601
  id: "gbp.places.api-key",
68204
68602
  category: CheckCategories.auth,
@@ -68243,7 +68641,7 @@ var apiKeyCheck = {
68243
68641
  details: { tier: cfg.tier }
68244
68642
  };
68245
68643
  }
68246
- const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq92(gbpLocations.projectId, ctx.project.id)).all();
68644
+ const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq93(gbpLocations.projectId, ctx.project.id)).all();
68247
68645
  const selected = rows.filter((r) => r.selected);
68248
68646
  const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
68249
68647
  const details = {
@@ -68276,7 +68674,7 @@ var PLACES_CHECK_BY_ID = Object.fromEntries(
68276
68674
  );
68277
68675
 
68278
68676
  // ../api-routes/src/doctor/checks/gbp-reviews.ts
68279
- import { and as and77, desc as desc43, eq as eq93, gte as gte24, inArray as inArray40, sql as sql32 } from "drizzle-orm";
68677
+ import { and as and78, desc as desc44, eq as eq94, gte as gte24, inArray as inArray41, sql as sql33 } from "drizzle-orm";
68280
68678
  var REVIEW_EVENTS = /* @__PURE__ */ new Set(["review.negative", "review.rating-dropped"]);
68281
68679
  var V4_ACCESS_REMEDIATION = "Business Profile reviews use the legacy My Business API (mybusiness.googleapis.com), which Google enables per Cloud project only on request; the API Library and `gcloud services enable` cannot turn it on. Open the enable link in the API access approval email as the approved account, or reply to the access-request thread asking Google to enable mybusiness.googleapis.com for your project number, then run `canonry gbp sync`.";
68282
68680
  var reviewsAccessCheck = {
@@ -68304,7 +68702,7 @@ var reviewsAccessCheck = {
68304
68702
  reviewsAccess: gbpLocations.reviewsAccess,
68305
68703
  reviewsAccessReason: gbpLocations.reviewsAccessReason,
68306
68704
  reviewsCheckedAt: gbpLocations.reviewsCheckedAt
68307
- }).from(gbpLocations).where(and77(eq93(gbpLocations.projectId, project.id), eq93(gbpLocations.selected, true))).all();
68705
+ }).from(gbpLocations).where(and78(eq94(gbpLocations.projectId, project.id), eq94(gbpLocations.selected, true))).all();
68308
68706
  if (locations.length === 0) {
68309
68707
  return {
68310
68708
  status: CheckStatuses.skipped,
@@ -68324,7 +68722,7 @@ var reviewsAccessCheck = {
68324
68722
  const places = ctx.getPlacesConfig?.();
68325
68723
  const placesFallback = places !== void 0 && places.tier === "atmosphere" && Boolean(places.apiKey);
68326
68724
  const v4Worked = new Set(
68327
- ctx.db.select({ locationName: gbpReviewRatings.locationName }).from(gbpReviewRatings).where(and77(eq93(gbpReviewRatings.projectId, project.id), eq93(gbpReviewRatings.origin, "gbp"))).all().map((row) => row.locationName)
68725
+ ctx.db.select({ locationName: gbpReviewRatings.locationName }).from(gbpReviewRatings).where(and78(eq94(gbpReviewRatings.projectId, project.id), eq94(gbpReviewRatings.origin, "gbp"))).all().map((row) => row.locationName)
68328
68726
  );
68329
68727
  const coverageOf = (loc) => {
68330
68728
  if (businessProfileReviewsCover(loc.reviewsAccess ?? null, v4Worked.has(loc.locationName))) return "full";
@@ -68358,7 +68756,7 @@ var reviewsAccessCheck = {
68358
68756
  none > 0 ? `${none} have no review source` : null
68359
68757
  ].filter((part) => part !== null);
68360
68758
  const summary3 = `Business Profile reviews are not available${why}. ${parts.join("; ")}.`;
68361
- const subscribed = ctx.db.select({ config: notifications.config, enabled: notifications.enabled }).from(notifications).where(eq93(notifications.projectId, project.id)).all().some((n) => n.enabled && (n.config.events ?? []).some((event) => REVIEW_EVENTS.has(event)));
68759
+ const subscribed = ctx.db.select({ config: notifications.config, enabled: notifications.enabled }).from(notifications).where(eq94(notifications.projectId, project.id)).all().some((n) => n.enabled && (n.config.events ?? []).some((event) => REVIEW_EVENTS.has(event)));
68362
68760
  const placesHint = placesFallback ? "" : " For partial coverage meanwhile, set a Places API key (`GOOGLE_PLACES_API_KEY` or `places.apiKey`) with `places.tier: atmosphere`.";
68363
68761
  if (none > 0 && subscribed) {
68364
68762
  return {
@@ -68389,7 +68787,7 @@ var reviewDeliveryCheck = {
68389
68787
  return { status: CheckStatuses.skipped, code: "gbp.reviews.delivery.no-project", summary: "Project context required.", remediation: null };
68390
68788
  }
68391
68789
  const since = new Date(Date.now() - UNDELIVERED_ATTENTION_DAYS * 864e5).toISOString();
68392
- const lastTouched = sql32`coalesce(${reviewWebhookDeliveries.attemptedAt}, ${reviewWebhookDeliveries.createdAt})`;
68790
+ const lastTouched = sql33`coalesce(${reviewWebhookDeliveries.attemptedAt}, ${reviewWebhookDeliveries.createdAt})`;
68393
68791
  const rows = ctx.db.select({
68394
68792
  id: reviewWebhookDeliveries.id,
68395
68793
  notificationId: reviewWebhookDeliveries.notificationId,
@@ -68397,11 +68795,11 @@ var reviewDeliveryCheck = {
68397
68795
  attempts: reviewWebhookDeliveries.attempts,
68398
68796
  lastTouched,
68399
68797
  payload: reviewWebhookDeliveries.payload
68400
- }).from(reviewWebhookDeliveries).where(and77(
68401
- eq93(reviewWebhookDeliveries.projectId, ctx.project.id),
68402
- inArray40(reviewWebhookDeliveries.state, [ReviewDeliveryStates.Uncertain, ReviewDeliveryStates.Failed]),
68798
+ }).from(reviewWebhookDeliveries).where(and78(
68799
+ eq94(reviewWebhookDeliveries.projectId, ctx.project.id),
68800
+ inArray41(reviewWebhookDeliveries.state, [ReviewDeliveryStates.Uncertain, ReviewDeliveryStates.Failed]),
68403
68801
  gte24(lastTouched, since)
68404
- )).orderBy(desc43(lastTouched)).all();
68802
+ )).orderBy(desc44(lastTouched)).all();
68405
68803
  if (rows.length === 0) {
68406
68804
  return {
68407
68805
  status: CheckStatuses.ok,
@@ -68410,7 +68808,7 @@ var reviewDeliveryCheck = {
68410
68808
  remediation: null
68411
68809
  };
68412
68810
  }
68413
- const destinations = new Map(ctx.db.select({ id: notifications.id, config: notifications.config }).from(notifications).where(eq93(notifications.projectId, ctx.project.id)).all().map((row) => [row.id, redactNotificationUrl(row.config.url).urlDisplay]));
68811
+ const destinations = new Map(ctx.db.select({ id: notifications.id, config: notifications.config }).from(notifications).where(eq94(notifications.projectId, ctx.project.id)).all().map((row) => [row.id, redactNotificationUrl(row.config.url).urlDisplay]));
68414
68812
  const deliveries = rows.map((row) => {
68415
68813
  const review = row.payload.reviews[0];
68416
68814
  return {
@@ -68984,7 +69382,7 @@ var RUNTIME_STATE_CHECKS = [
68984
69382
  ];
68985
69383
 
68986
69384
  // ../api-routes/src/doctor/checks/traffic-source.ts
68987
- import { and as and78, desc as desc44, eq as eq94, gte as gte25, inArray as inArray41, isNotNull as isNotNull9, ne as ne16, sql as sql33 } from "drizzle-orm";
69385
+ import { and as and79, desc as desc45, eq as eq95, gte as gte25, inArray as inArray42, isNotNull as isNotNull9, ne as ne16, sql as sql34 } from "drizzle-orm";
68988
69386
  var RECENT_DATA_WARN_DAYS = 7;
68989
69387
  var RECENT_DATA_FAIL_DAYS = 30;
68990
69388
  function isCloudflareDirectPush(source) {
@@ -69023,8 +69421,8 @@ function skippedNoProject5() {
69023
69421
  function loadProbes(ctx) {
69024
69422
  if (!ctx.project) return [];
69025
69423
  const rows = ctx.db.select().from(trafficSources).where(
69026
- and78(
69027
- eq94(trafficSources.projectId, ctx.project.id),
69424
+ and79(
69425
+ eq95(trafficSources.projectId, ctx.project.id),
69028
69426
  ne16(trafficSources.status, TrafficSourceStatuses.archived)
69029
69427
  )
69030
69428
  ).all();
@@ -69161,28 +69559,28 @@ var recentDataCheck = {
69161
69559
  const warnCutoff = new Date(now.getTime() - RECENT_DATA_WARN_DAYS * 24 * 60 * 6e4).toISOString();
69162
69560
  const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
69163
69561
  const recentCrawlers = Number(
69164
- ctx.db.select({ total: sql33`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
69165
- and78(
69166
- eq94(crawlerEventsHourly.projectId, ctx.project.id),
69167
- inArray41(crawlerEventsHourly.sourceId, activeSourceIds),
69562
+ ctx.db.select({ total: sql34`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
69563
+ and79(
69564
+ eq95(crawlerEventsHourly.projectId, ctx.project.id),
69565
+ inArray42(crawlerEventsHourly.sourceId, activeSourceIds),
69168
69566
  gte25(crawlerEventsHourly.tsHour, warnCutoff)
69169
69567
  )
69170
69568
  ).get()?.total ?? 0
69171
69569
  );
69172
69570
  const recentReferrals = Number(
69173
- ctx.db.select({ total: sql33`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
69174
- and78(
69175
- eq94(aiReferralEventsHourly.projectId, ctx.project.id),
69176
- inArray41(aiReferralEventsHourly.sourceId, activeSourceIds),
69571
+ ctx.db.select({ total: sql34`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
69572
+ and79(
69573
+ eq95(aiReferralEventsHourly.projectId, ctx.project.id),
69574
+ inArray42(aiReferralEventsHourly.sourceId, activeSourceIds),
69177
69575
  gte25(aiReferralEventsHourly.tsHour, warnCutoff)
69178
69576
  )
69179
69577
  ).get()?.total ?? 0
69180
69578
  );
69181
69579
  const recentUserFetches = Number(
69182
- ctx.db.select({ total: sql33`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
69183
- and78(
69184
- eq94(aiUserFetchEventsHourly.projectId, ctx.project.id),
69185
- inArray41(aiUserFetchEventsHourly.sourceId, activeSourceIds),
69580
+ ctx.db.select({ total: sql34`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
69581
+ and79(
69582
+ eq95(aiUserFetchEventsHourly.projectId, ctx.project.id),
69583
+ inArray42(aiUserFetchEventsHourly.sourceId, activeSourceIds),
69186
69584
  gte25(aiUserFetchEventsHourly.tsHour, warnCutoff)
69187
69585
  )
69188
69586
  ).get()?.total ?? 0
@@ -69205,28 +69603,28 @@ var recentDataCheck = {
69205
69603
  };
69206
69604
  }
69207
69605
  const olderCrawlers = Number(
69208
- ctx.db.select({ total: sql33`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
69209
- and78(
69210
- eq94(crawlerEventsHourly.projectId, ctx.project.id),
69211
- inArray41(crawlerEventsHourly.sourceId, activeSourceIds),
69606
+ ctx.db.select({ total: sql34`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
69607
+ and79(
69608
+ eq95(crawlerEventsHourly.projectId, ctx.project.id),
69609
+ inArray42(crawlerEventsHourly.sourceId, activeSourceIds),
69212
69610
  gte25(crawlerEventsHourly.tsHour, failCutoff)
69213
69611
  )
69214
69612
  ).get()?.total ?? 0
69215
69613
  );
69216
69614
  const olderReferrals = Number(
69217
- ctx.db.select({ total: sql33`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
69218
- and78(
69219
- eq94(aiReferralEventsHourly.projectId, ctx.project.id),
69220
- inArray41(aiReferralEventsHourly.sourceId, activeSourceIds),
69615
+ ctx.db.select({ total: sql34`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
69616
+ and79(
69617
+ eq95(aiReferralEventsHourly.projectId, ctx.project.id),
69618
+ inArray42(aiReferralEventsHourly.sourceId, activeSourceIds),
69221
69619
  gte25(aiReferralEventsHourly.tsHour, failCutoff)
69222
69620
  )
69223
69621
  ).get()?.total ?? 0
69224
69622
  );
69225
69623
  const olderUserFetches = Number(
69226
- ctx.db.select({ total: sql33`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
69227
- and78(
69228
- eq94(aiUserFetchEventsHourly.projectId, ctx.project.id),
69229
- inArray41(aiUserFetchEventsHourly.sourceId, activeSourceIds),
69624
+ ctx.db.select({ total: sql34`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
69625
+ and79(
69626
+ eq95(aiUserFetchEventsHourly.projectId, ctx.project.id),
69627
+ inArray42(aiUserFetchEventsHourly.sourceId, activeSourceIds),
69230
69628
  gte25(aiUserFetchEventsHourly.tsHour, failCutoff)
69231
69629
  )
69232
69630
  ).get()?.total ?? 0
@@ -69291,7 +69689,7 @@ async function runValidator(source, validator, fallbackId, fallbackLabel) {
69291
69689
  function summarizePerSourceResults(fallbackId, fallbackLabel, results) {
69292
69690
  const failed = results.filter((r) => r.output.status === CheckStatuses.fail);
69293
69691
  const warned = results.filter((r) => r.output.status === CheckStatuses.warn);
69294
- const skipped5 = results.filter((r) => r.output.status === CheckStatuses.skipped);
69692
+ const skipped6 = results.filter((r) => r.output.status === CheckStatuses.skipped);
69295
69693
  const ok = results.filter((r) => r.output.status === CheckStatuses.ok);
69296
69694
  const detail = {
69297
69695
  sources: results.map((r) => ({
@@ -69325,7 +69723,7 @@ function summarizePerSourceResults(fallbackId, fallbackLabel, results) {
69325
69723
  return {
69326
69724
  status: CheckStatuses.ok,
69327
69725
  code: `traffic.${fallbackId}.ok`,
69328
- summary: `${ok.length} source(s) passed ${fallbackLabel} validation${skipped5.length > 0 ? ` (${skipped5.length} skipped)` : ""}.`,
69726
+ summary: `${ok.length} source(s) passed ${fallbackLabel} validation${skipped6.length > 0 ? ` (${skipped6.length} skipped)` : ""}.`,
69329
69727
  details: detail
69330
69728
  };
69331
69729
  }
@@ -69333,7 +69731,7 @@ function summarizePerSourceResults(fallbackId, fallbackLabel, results) {
69333
69731
  status: CheckStatuses.skipped,
69334
69732
  code: `traffic.${fallbackId}.all-skipped`,
69335
69733
  summary: `No source-type validator was available for any of the ${results.length} connected source(s).`,
69336
- remediation: skipped5.find((result) => result.output.remediation)?.output.remediation,
69734
+ remediation: skipped6.find((result) => result.output.remediation)?.output.remediation,
69337
69735
  details: detail
69338
69736
  };
69339
69737
  }
@@ -69552,12 +69950,12 @@ var syncLagCheck = {
69552
69950
  }
69553
69951
  };
69554
69952
  function lastQueueStopReason(ctx, sourceId) {
69555
- const row = ctx.db.select({ syncSummary: runs.syncSummary }).from(runs).where(and78(
69556
- eq94(runs.projectId, ctx.project.id),
69557
- eq94(runs.kind, RunKinds["traffic-sync"]),
69558
- eq94(runs.sourceId, sourceId),
69953
+ const row = ctx.db.select({ syncSummary: runs.syncSummary }).from(runs).where(and79(
69954
+ eq95(runs.projectId, ctx.project.id),
69955
+ eq95(runs.kind, RunKinds["traffic-sync"]),
69956
+ eq95(runs.sourceId, sourceId),
69559
69957
  isNotNull9(runs.syncSummary)
69560
- )).orderBy(desc44(runs.createdAt)).limit(1).get();
69958
+ )).orderBy(desc45(runs.createdAt)).limit(1).get();
69561
69959
  return row?.syncSummary?.type === "cloudflare-queue" ? row.syncSummary.stopReason : null;
69562
69960
  }
69563
69961
  var QUEUE_LIMIT_STOPS = [
@@ -69847,7 +70245,7 @@ var GTM_REQUIRED_OAUTH_SCOPES = [
69847
70245
  ];
69848
70246
  var FRESH_SNAPSHOT_WARN_DAYS = 7;
69849
70247
  var FRESH_SNAPSHOT_FAIL_DAYS = 30;
69850
- var DAY_MS5 = 24 * 60 * 60 * 1e3;
70248
+ var DAY_MS6 = 24 * 60 * 60 * 1e3;
69851
70249
  function unavailableStatus() {
69852
70250
  return {
69853
70251
  status: CheckStatuses.skipped,
@@ -69934,7 +70332,7 @@ function snapshotOutput(provider, latestSnapshotAt, now) {
69934
70332
  details: { latestSnapshotAt }
69935
70333
  };
69936
70334
  }
69937
- const ageDays2 = (now.getTime() - snapshotMs) / DAY_MS5;
70335
+ const ageDays2 = (now.getTime() - snapshotMs) / DAY_MS6;
69938
70336
  const details = { latestSnapshotAt, ageDays: Math.max(0, Math.round(ageDays2)) };
69939
70337
  if (ageDays2 < 0) {
69940
70338
  return {
@@ -70157,6 +70555,7 @@ var ALL_CHECKS = [
70157
70555
  ...WORDPRESS_PUBLISH_CHECKS,
70158
70556
  ...GA_AUTH_CHECKS,
70159
70557
  ...DATA_FRESHNESS_CHECKS,
70558
+ ...SEO_REFRESH_CHECKS,
70160
70559
  reportSweepsCheck,
70161
70560
  reportModelsCheck,
70162
70561
  reportDailyDataCheck,
@@ -70354,7 +70753,7 @@ async function doctorRoutes(app, opts) {
70354
70753
  }
70355
70754
 
70356
70755
  // ../api-routes/src/status.ts
70357
- import { and as and79, desc as desc45, eq as eq95, isNull as isNull15 } from "drizzle-orm";
70756
+ import { and as and80, desc as desc46, eq as eq96, isNull as isNull15 } from "drizzle-orm";
70358
70757
  function ageDays(iso) {
70359
70758
  if (!iso) return null;
70360
70759
  const ms = Date.now() - Date.parse(iso);
@@ -70404,7 +70803,7 @@ function makeRow(integration, label, runKind, state, staleAfterDays, dataThrough
70404
70803
  };
70405
70804
  }
70406
70805
  function latestDate2(db, table, projectId) {
70407
- const row = db.select({ date: table.date }).from(table).where(eq95(table.projectId, projectId)).orderBy(desc45(table.date)).limit(1).get();
70806
+ const row = db.select({ date: table.date }).from(table).where(eq96(table.projectId, projectId)).orderBy(desc46(table.date)).limit(1).get();
70408
70807
  return row?.date ?? null;
70409
70808
  }
70410
70809
  function projectRows(opts, project) {
@@ -70419,7 +70818,7 @@ function projectRows(opts, project) {
70419
70818
  const rows = [];
70420
70819
  const gsc = gscConnection(ctx, info);
70421
70820
  if (connected(gsc)) {
70422
- const watermarkRow = opts.db.select({ dataThroughDate: gscDataWatermarks.dataThroughDate }).from(gscDataWatermarks).where(eq95(gscDataWatermarks.projectId, project.id)).limit(1).get();
70821
+ const watermarkRow = opts.db.select({ dataThroughDate: gscDataWatermarks.dataThroughDate }).from(gscDataWatermarks).where(eq96(gscDataWatermarks.projectId, project.id)).limit(1).get();
70423
70822
  const state = readSyncRunState(opts.db, project.id, RunKinds["gsc-sync"], { since: gsc.since });
70424
70823
  const dataThrough = watermarkRow?.dataThroughDate ?? latestDate2(opts.db, gscDailyTotals, project.id);
70425
70824
  rows.push(makeRow("gsc", "Search Console", RunKinds["gsc-sync"], state, SYNC_IDLE_DAYS, dataThrough, null, ["gsc.sync.recent-failures"], GSC_DATA_AGING_DAYS));
@@ -70432,12 +70831,12 @@ function projectRows(opts, project) {
70432
70831
  const gbp = gbpConnection(ctx, info);
70433
70832
  if (connected(gbp)) rows.push(makeRow("gbp", "Google Business Profile", RunKinds["gbp-sync"], readSyncRunState(opts.db, project.id, RunKinds["gbp-sync"], { nothingToSyncErrors: GBP_NOTHING_TO_SYNC_ERRORS }), RECENT_SYNC_WARN_DAYS, null, null, ["gbp.sync.recent-failures"]));
70434
70833
  if (opts.bingConnectionStore?.getConnection(project.canonicalDomain)) rows.push(makeRow("bing", "Bing Webmaster Tools", RunKinds["bing-inspect-sitemap"], readSyncRunState(opts.db, project.id, RunKinds["bing-inspect-sitemap"]), null));
70435
- const ads = opts.db.select({ id: adsConnections.id, lastSyncedAt: adsConnections.lastSyncedAt }).from(adsConnections).where(eq95(adsConnections.projectId, project.id)).limit(1).get();
70834
+ const ads = opts.db.select({ id: adsConnections.id, lastSyncedAt: adsConnections.lastSyncedAt }).from(adsConnections).where(eq96(adsConnections.projectId, project.id)).limit(1).get();
70436
70835
  if (ads) rows.push(makeRow("ads", "OpenAI Ads", RunKinds["ads-sync"], readSyncRunState(opts.db, project.id, RunKinds["ads-sync"]), ADS_RECENT_SYNC_WARN_DAYS, null, null, ["ads.data.recent-sync"], ADS_RECENT_SYNC_WARN_DAYS, ads.lastSyncedAt));
70437
70836
  const marketing = opts.getGoogleMarketingDoctorInput?.(ctx);
70438
70837
  if (marketing?.googleAds) rows.push(makeRow("google-ads", "Google Ads", RunKinds["google-ads-sync"], readSyncRunState(opts.db, project.id, RunKinds["google-ads-sync"]), FRESH_SNAPSHOT_WARN_DAYS, null, null, ["google-ads.sync.recent-failures"], FRESH_SNAPSHOT_WARN_DAYS, marketing.googleAds.latestSnapshotAt));
70439
70838
  if (marketing?.gtm) rows.push(makeRow("gtm", "Google Tag Manager", RunKinds["gtm-sync"], readSyncRunState(opts.db, project.id, RunKinds["gtm-sync"]), FRESH_SNAPSHOT_WARN_DAYS, null, null, ["gtm.sync.recent-failures"], FRESH_SNAPSHOT_WARN_DAYS, marketing.gtm.latestSnapshotAt));
70440
- const sources = opts.db.select().from(trafficSources).where(and79(eq95(trafficSources.projectId, project.id), isNull15(trafficSources.archivedAt))).all();
70839
+ const sources = opts.db.select().from(trafficSources).where(and80(eq96(trafficSources.projectId, project.id), isNull15(trafficSources.archivedAt))).all();
70441
70840
  for (const source of sources) {
70442
70841
  const row = makeRow("traffic", source.displayName, RunKinds["traffic-sync"], readSyncRunState(opts.db, project.id, RunKinds["traffic-sync"], { sourceId: source.id }), null, null, source.id, ["traffic.source.sync-lag", "traffic.source.recent-data"]);
70443
70842
  const lag = source.lastSyncedAt ? Date.now() - Date.parse(source.lastSyncedAt) : Number.POSITIVE_INFINITY;
@@ -70449,7 +70848,7 @@ function projectRows(opts, project) {
70449
70848
  }
70450
70849
  function projectStatus(opts, project) {
70451
70850
  const integrations = projectRows(opts, project);
70452
- const healthRow = opts.db.select().from(doctorHealthState).where(eq95(doctorHealthState.projectId, project.id)).limit(1).get();
70851
+ const healthRow = opts.db.select().from(doctorHealthState).where(eq96(doctorHealthState.projectId, project.id)).limit(1).get();
70453
70852
  const health = healthRow ? { status: healthRow.status, code: healthRow.code, summary: healthRow.summary, checkedAt: healthRow.checkedAt, notifiedAt: healthRow.notifiedAt } : null;
70454
70853
  const rank = { ok: 0, "never-synced": 1, warning: 2, stale: 3, failing: 4 };
70455
70854
  const healthState = health?.status === "fail" ? "failing" : health?.status === "warn" ? "warning" : "ok";
@@ -70469,7 +70868,7 @@ async function statusRoutes(app, opts) {
70469
70868
  if (request.query.problems === "true") result.projects = result.projects.map((item2) => ({ ...item2, integrations: item2.integrations.filter((row) => row.state !== "ok") }));
70470
70869
  return instanceStatusDtoSchema.parse(result);
70471
70870
  }
70472
- const projectsRows = (scopedId ? opts.db.select().from(projects).where(eq95(projects.id, scopedId)) : opts.db.select().from(projects)).all();
70871
+ const projectsRows = (scopedId ? opts.db.select().from(projects).where(eq96(projects.id, scopedId)) : opts.db.select().from(projects)).all();
70473
70872
  const output = { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), ...opts.getMcpHealth ? { mcp: opts.getMcpHealth() } : {}, summary: { projects: projectsRows.length, ok: 0, failing: 0, warning: 0, stale: 0, neverSynced: 0 }, projects: projectsRows.map((project) => projectStatus(opts, project)) };
70474
70873
  for (const project of output.projects) {
70475
70874
  const key = project.state === "never-synced" ? "neverSynced" : project.state;
@@ -70482,7 +70881,7 @@ async function statusRoutes(app, opts) {
70482
70881
 
70483
70882
  // ../api-routes/src/discovery/routes.ts
70484
70883
  import crypto54 from "crypto";
70485
- import { and as and80, desc as desc46, eq as eq96, gte as gte26, inArray as inArray42, isNull as isNull16, or as or20 } from "drizzle-orm";
70884
+ import { and as and81, desc as desc47, eq as eq97, gte as gte26, inArray as inArray43, isNull as isNull16, or as or20 } from "drizzle-orm";
70486
70885
  var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
70487
70886
  async function discoveryRoutes(app, opts) {
70488
70887
  app.post("/projects/:name/discover/run", async (request, reply) => {
@@ -70516,13 +70915,13 @@ async function discoveryRoutes(app, opts) {
70516
70915
  const now = (/* @__PURE__ */ new Date()).toISOString();
70517
70916
  const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
70518
70917
  const decision = app.db.transaction((tx) => {
70519
- const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and80(
70520
- eq96(discoverySessions.projectId, project.id),
70521
- eq96(discoverySessions.icpDescription, icpDescription),
70918
+ const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and81(
70919
+ eq97(discoverySessions.projectId, project.id),
70920
+ eq97(discoverySessions.icpDescription, icpDescription),
70522
70921
  // Buyer is part of session identity: it changes the seed prompt's
70523
70922
  // semantics, so a request with a different (or no) buyer must start
70524
70923
  // its own session, never adopt another buyer's probes.
70525
- parsed.data.buyerDescription == null ? isNull16(discoverySessions.buyerDescription) : eq96(discoverySessions.buyerDescription, parsed.data.buyerDescription),
70924
+ parsed.data.buyerDescription == null ? isNull16(discoverySessions.buyerDescription) : eq97(discoverySessions.buyerDescription, parsed.data.buyerDescription),
70526
70925
  // Locations are identity too: a different service-area subset seeds
70527
70926
  // and probes a different geo, so it must never reuse another geo's
70528
70927
  // session. resolveLocations is deterministic (project-config order),
@@ -70532,18 +70931,18 @@ async function discoveryRoutes(app, opts) {
70532
70931
  // locations a NULL row's subset is unknowable, so it conservatively
70533
70932
  // never reuses — a one-time, bounded (2h window) non-reuse after
70534
70933
  // upgrade, never a wrong reuse.
70535
- locations.length === 0 ? or20(isNull16(discoverySessions.locations), eq96(discoverySessions.locations, locations)) : eq96(discoverySessions.locations, locations),
70934
+ locations.length === 0 ? or20(isNull16(discoverySessions.locations), eq97(discoverySessions.locations, locations)) : eq97(discoverySessions.locations, locations),
70536
70935
  // Seed provider set is identity: a different phrasing distribution
70537
70936
  // must never reuse another set's session. Null (= Gemini-only
70538
70937
  // default, including explicit ['gemini']) matches legacy rows.
70539
- seedProviders == null ? isNull16(discoverySessions.seedProviders) : eq96(discoverySessions.seedProviders, seedProviders),
70540
- inArray42(discoverySessions.status, [
70938
+ seedProviders == null ? isNull16(discoverySessions.seedProviders) : eq97(discoverySessions.seedProviders, seedProviders),
70939
+ inArray43(discoverySessions.status, [
70541
70940
  DiscoverySessionStatuses.queued,
70542
70941
  DiscoverySessionStatuses.seeding,
70543
70942
  DiscoverySessionStatuses.probing
70544
70943
  ]),
70545
70944
  gte26(discoverySessions.createdAt, ageFloorIso)
70546
- )).orderBy(desc46(discoverySessions.createdAt)).get();
70945
+ )).orderBy(desc47(discoverySessions.createdAt)).get();
70547
70946
  if (existing && existing.runId) {
70548
70947
  return { reused: true, sessionId: existing.id, runId: existing.runId };
70549
70948
  }
@@ -70612,7 +71011,7 @@ async function discoveryRoutes(app, opts) {
70612
71011
  const project = resolveProject(app.db, request.params.name);
70613
71012
  const parsedLimit = parseInt(request.query.limit ?? "", 10);
70614
71013
  const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
70615
- const rows = app.db.select().from(discoverySessions).where(eq96(discoverySessions.projectId, project.id)).orderBy(desc46(discoverySessions.createdAt)).limit(limit).all();
71014
+ const rows = app.db.select().from(discoverySessions).where(eq97(discoverySessions.projectId, project.id)).orderBy(desc47(discoverySessions.createdAt)).limit(limit).all();
70616
71015
  return reply.send(rows.map(serializeSession));
70617
71016
  }
70618
71017
  );
@@ -70620,11 +71019,11 @@ async function discoveryRoutes(app, opts) {
70620
71019
  "/projects/:name/discover/sessions/:id",
70621
71020
  async (request, reply) => {
70622
71021
  const project = resolveProject(app.db, request.params.name);
70623
- const session = app.db.select().from(discoverySessions).where(eq96(discoverySessions.id, request.params.id)).get();
71022
+ const session = app.db.select().from(discoverySessions).where(eq97(discoverySessions.id, request.params.id)).get();
70624
71023
  if (!session || session.projectId !== project.id) {
70625
71024
  throw notFound("Discovery session", request.params.id);
70626
71025
  }
70627
- const probeRows = app.db.select().from(discoveryProbes).where(eq96(discoveryProbes.sessionId, session.id)).all();
71026
+ const probeRows = app.db.select().from(discoveryProbes).where(eq97(discoveryProbes.sessionId, session.id)).all();
70628
71027
  const detail = {
70629
71028
  ...serializeSession(session),
70630
71029
  probes: probeRows.map(serializeProbe)
@@ -70636,7 +71035,7 @@ async function discoveryRoutes(app, opts) {
70636
71035
  "/projects/:name/discover/sessions/:id/harvest",
70637
71036
  async (request, reply) => {
70638
71037
  const project = resolveProject(app.db, request.params.name);
70639
- const session = app.db.select().from(discoverySessions).where(eq96(discoverySessions.id, request.params.id)).get();
71038
+ const session = app.db.select().from(discoverySessions).where(eq97(discoverySessions.id, request.params.id)).get();
70640
71039
  if (!session || session.projectId !== project.id) {
70641
71040
  throw notFound("Discovery session", request.params.id);
70642
71041
  }
@@ -70644,7 +71043,7 @@ async function discoveryRoutes(app, opts) {
70644
71043
  const minProbeHits = Number.isNaN(parsedFloor) || parsedFloor < 1 ? 1 : parsedFloor;
70645
71044
  const applyAnchor = request.query.anchor !== "false";
70646
71045
  const provider = session.seedProvider ?? "gemini";
70647
- const probeRows = app.db.select().from(discoveryProbes).where(eq96(discoveryProbes.sessionId, session.id)).all();
71046
+ const probeRows = app.db.select().from(discoveryProbes).where(eq97(discoveryProbes.sessionId, session.id)).all();
70648
71047
  const extract = opts.harvestSearchQueries;
70649
71048
  const probesWithQueries = probeRows.map((row) => {
70650
71049
  if (!extract || !row.rawResponse) return { searchQueries: [] };
@@ -70655,7 +71054,7 @@ async function discoveryRoutes(app, opts) {
70655
71054
  return { searchQueries: [] };
70656
71055
  }
70657
71056
  });
70658
- const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(eq96(queries.projectId, project.id)).all().map((r) => r.query);
71057
+ const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(eq97(queries.projectId, project.id)).all().map((r) => r.query);
70659
71058
  const anchorTerms = buildHarvestAnchorTerms(
70660
71059
  [session.icpDescription ?? "", ...trackedQueries],
70661
71060
  effectiveDomains(project)
@@ -70702,11 +71101,11 @@ async function discoveryRoutes(app, opts) {
70702
71101
  "/projects/:name/discover/sessions/:id/promote",
70703
71102
  async (request, reply) => {
70704
71103
  const project = resolveProject(app.db, request.params.name);
70705
- const session = app.db.select().from(discoverySessions).where(eq96(discoverySessions.id, request.params.id)).get();
71104
+ const session = app.db.select().from(discoverySessions).where(eq97(discoverySessions.id, request.params.id)).get();
70706
71105
  if (!session || session.projectId !== project.id) {
70707
71106
  throw notFound("Discovery session", request.params.id);
70708
71107
  }
70709
- const probeRows = app.db.select().from(discoveryProbes).where(eq96(discoveryProbes.sessionId, session.id)).all();
71108
+ const probeRows = app.db.select().from(discoveryProbes).where(eq97(discoveryProbes.sessionId, session.id)).all();
70710
71109
  const cited = /* @__PURE__ */ new Set();
70711
71110
  const aspirational = /* @__PURE__ */ new Set();
70712
71111
  const wasted = /* @__PURE__ */ new Set();
@@ -70741,7 +71140,7 @@ async function discoveryRoutes(app, opts) {
70741
71140
  );
70742
71141
  app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
70743
71142
  const project = resolveProject(app.db, request.params.name);
70744
- const session = app.db.select().from(discoverySessions).where(eq96(discoverySessions.id, request.params.id)).get();
71143
+ const session = app.db.select().from(discoverySessions).where(eq97(discoverySessions.id, request.params.id)).get();
70745
71144
  if (!session || session.projectId !== project.id) {
70746
71145
  throw notFound("Discovery session", request.params.id);
70747
71146
  }
@@ -70764,7 +71163,7 @@ async function discoveryRoutes(app, opts) {
70764
71163
  const bucketSet = new Set(buckets);
70765
71164
  const includeCompetitors = parsed.data.includeCompetitors ?? true;
70766
71165
  const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
70767
- const probeRows = app.db.select().from(discoveryProbes).where(eq96(discoveryProbes.sessionId, session.id)).all();
71166
+ const probeRows = app.db.select().from(discoveryProbes).where(eq97(discoveryProbes.sessionId, session.id)).all();
70768
71167
  const candidateQueries = /* @__PURE__ */ new Set();
70769
71168
  for (const probe of probeRows) {
70770
71169
  if (!probe.bucket) continue;
@@ -70772,7 +71171,7 @@ async function discoveryRoutes(app, opts) {
70772
71171
  if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
70773
71172
  }
70774
71173
  const existingQueries = new Set(
70775
- app.db.select({ query: queries.query }).from(queries).where(eq96(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
71174
+ app.db.select({ query: queries.query }).from(queries).where(eq97(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
70776
71175
  );
70777
71176
  const promotedQueries = [];
70778
71177
  const skippedQueries = [];
@@ -70944,21 +71343,21 @@ function planCompetitorPromotion(input) {
70944
71343
  const preferred = new Set(input.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES);
70945
71344
  const eligible = groupCompetitorMap(input.competitorMap, input.probes, preferred).filter((group) => group.hits >= DISCOVERY_PROMOTE_COMPETITOR_MIN_HITS).filter((group) => !typeFilter || group.sources.some((source) => typeFilter.has(source.competitorType))).sort((a, b) => b.hits - a.hits || a.domain.localeCompare(b.domain));
70946
71345
  const tracked = new Set(input.tracked.map((row) => normalizeCompetitorDomain(row.domain)));
70947
- const skipped5 = [];
71346
+ const skipped6 = [];
70948
71347
  const candidates = [];
70949
71348
  for (const group of eligible) {
70950
71349
  const projectDomain = competitorDomainProjectClaim(group.domain, input.project.domains);
70951
71350
  const subdomains = [...new Set(group.sources.map((source) => hostOf(source.domain) ?? source.domain))].filter((host) => host !== group.domain);
70952
71351
  if (projectDomain) {
70953
- skipped5.push({
71352
+ skipped6.push({
70954
71353
  ...group,
70955
71354
  reason: DiscoveryPromoteCompetitorSkipReasons["project-domain"],
70956
71355
  message: `${group.domain} overlaps the project domain ${projectDomain}, so every citation of the project would count for it`
70957
71356
  });
70958
71357
  } else if (tracked.has(group.domain)) {
70959
- skipped5.push({ ...group, reason: DiscoveryPromoteCompetitorSkipReasons["already-tracked"], message: `${group.domain} is already tracked` });
71358
+ skipped6.push({ ...group, reason: DiscoveryPromoteCompetitorSkipReasons["already-tracked"], message: `${group.domain} is already tracked` });
70960
71359
  } else if (subdomains.length > 1) {
70961
- skipped5.push({
71360
+ skipped6.push({
70962
71361
  ...group,
70963
71362
  reason: DiscoveryPromoteCompetitorSkipReasons["shared-host"],
70964
71363
  message: `${group.domain} was cited as ${subdomains.length} different subdomains (${subdomains.join(", ")}), which may be different sites on one shared host, so it is not merged into one competitor; if they are one competitor, add it with canonry competitor add <project> ${group.domain}`
@@ -70972,7 +71371,7 @@ function planCompetitorPromotion(input) {
70972
71371
  for (const group of candidates) {
70973
71372
  const rejections = claimed.get(group.domain);
70974
71373
  if (rejections) {
70975
- skipped5.push({
71374
+ skipped6.push({
70976
71375
  ...group,
70977
71376
  reason: DiscoveryPromoteCompetitorSkipReasons["claimed-by-alias"],
70978
71377
  message: rejections.map((rejection) => `${group.domain} ${describeCompetitorAliasRejection(rejection)}`).join("; ")
@@ -70981,12 +71380,12 @@ function planCompetitorPromotion(input) {
70981
71380
  promote.push(group);
70982
71381
  }
70983
71382
  }
70984
- return { promote, skipped: skipped5 };
71383
+ return { promote, skipped: skipped6 };
70985
71384
  }
70986
71385
 
70987
71386
  // ../api-routes/src/discovery/orchestrate.ts
70988
71387
  import crypto55 from "crypto";
70989
- import { eq as eq97 } from "drizzle-orm";
71388
+ import { eq as eq98 } from "drizzle-orm";
70990
71389
  var DEFAULT_MAX_PROBES = 100;
70991
71390
  var ABSOLUTE_MAX_PROBES = 500;
70992
71391
  function classifyProbeBucket(input) {
@@ -71072,7 +71471,7 @@ async function executeDiscovery(opts) {
71072
71471
  status: DiscoverySessionStatuses.seeding,
71073
71472
  dedupThreshold,
71074
71473
  startedAt
71075
- }).where(eq97(discoverySessions.id, opts.sessionId)).run();
71474
+ }).where(eq98(discoverySessions.id, opts.sessionId)).run();
71076
71475
  const seedResult = await opts.deps.seed({
71077
71476
  project: opts.project,
71078
71477
  icpDescription: opts.icpDescription,
@@ -71133,7 +71532,7 @@ async function executeDiscovery(opts) {
71133
71532
  dedupBandPairFraction: dedupStats.bandPairFraction,
71134
71533
  dedupPairsTotal: dedupStats.pairsTotal,
71135
71534
  warning
71136
- }).where(eq97(discoverySessions.id, opts.sessionId)).run();
71535
+ }).where(eq98(discoverySessions.id, opts.sessionId)).run();
71137
71536
  const probeLocation = opts.locations?.[0];
71138
71537
  const probeResults = await mapWithConcurrency(
71139
71538
  probedCanonicals,
@@ -71187,7 +71586,7 @@ async function executeDiscovery(opts) {
71187
71586
  wastedCount: buckets["wasted-surface"],
71188
71587
  competitorMap,
71189
71588
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
71190
- }).where(eq97(discoverySessions.id, opts.sessionId)).run();
71589
+ }).where(eq98(discoverySessions.id, opts.sessionId)).run();
71191
71590
  upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
71192
71591
  return {
71193
71592
  buckets,
@@ -71227,7 +71626,7 @@ function markSessionFailed(db, sessionId, error) {
71227
71626
  status: DiscoverySessionStatuses.failed,
71228
71627
  error,
71229
71628
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
71230
- }).where(eq97(discoverySessions.id, sessionId)).run();
71629
+ }).where(eq98(discoverySessions.id, sessionId)).run();
71231
71630
  }
71232
71631
  function dedupeStrings(input) {
71233
71632
  const seen2 = /* @__PURE__ */ new Set();
@@ -71245,12 +71644,12 @@ function dedupeStrings(input) {
71245
71644
 
71246
71645
  // ../api-routes/src/technical-aeo.ts
71247
71646
  import crypto56 from "crypto";
71248
- import { and as and82, asc as asc21, count as count8, desc as desc47, eq as eq99, gte as gte27, inArray as inArray43, isNotNull as isNotNull10, isNull as isNull17, lt as lt14, or as or21, sql as sql35 } from "drizzle-orm";
71647
+ import { and as and83, asc as asc22, count as count8, desc as desc48, eq as eq100, gte as gte27, inArray as inArray44, isNotNull as isNotNull10, isNull as isNull17, lt as lt14, or as or21, sql as sql36 } from "drizzle-orm";
71249
71648
  import { alias } from "drizzle-orm/sqlite-core";
71250
71649
 
71251
71650
  // ../api-routes/src/technical-aeo-issues.ts
71252
71651
  import { createHash as createHash10 } from "crypto";
71253
- import { and as and81, asc as asc20, eq as eq98, gt as gt9, sql as sql34 } from "drizzle-orm";
71652
+ import { and as and82, asc as asc21, eq as eq99, gt as gt10, sql as sql35 } from "drizzle-orm";
71254
71653
  var ADVISORY_CODES = /* @__PURE__ */ new Set([
71255
71654
  "entity-consistency.title.short",
71256
71655
  "site.duplicate-title",
@@ -71395,12 +71794,12 @@ function readSiteAuditIssues(input) {
71395
71794
  }
71396
71795
  }
71397
71796
  if (snapshot.detailsAvailable && snapshot.attemptId) {
71398
- const scope = and81(
71399
- eq98(siteCrawlPages.projectId, project.id),
71400
- eq98(siteCrawlPages.runId, snapshot.runId),
71401
- eq98(siteCrawlPages.attemptId, snapshot.attemptId)
71797
+ const scope = and82(
71798
+ eq99(siteCrawlPages.projectId, project.id),
71799
+ eq99(siteCrawlPages.runId, snapshot.runId),
71800
+ eq99(siteCrawlPages.attemptId, snapshot.attemptId)
71402
71801
  );
71403
- const size = db.select({ bytes: sql34`coalesce(sum(length(cast(${siteCrawlPages.auditFields} as blob)) + length(cast(${siteCrawlPages.url} as blob))), 0)` }).from(siteCrawlPages).where(scope).get()?.bytes ?? 0;
71802
+ const size = db.select({ bytes: sql35`coalesce(sum(length(cast(${siteCrawlPages.auditFields} as blob)) + length(cast(${siteCrawlPages.url} as blob))), 0)` }).from(siteCrawlPages).where(scope).get()?.bytes ?? 0;
71404
71803
  if (size + crossBytes > MAX_EVIDENCE_BYTES) {
71405
71804
  throw validationError("Stored audit exceeds the 128 MiB issue-evidence read budget");
71406
71805
  }
@@ -71411,7 +71810,7 @@ function readSiteAuditIssues(input) {
71411
71810
  url: siteCrawlPages.url,
71412
71811
  auditScore: siteCrawlPages.auditScore,
71413
71812
  auditFields: siteCrawlPages.auditFields
71414
- }).from(siteCrawlPages).where(and81(scope, after === void 0 ? void 0 : gt9(siteCrawlPages.nodeKey, after))).orderBy(asc20(siteCrawlPages.nodeKey)).limit(200).all();
71813
+ }).from(siteCrawlPages).where(and82(scope, after === void 0 ? void 0 : gt10(siteCrawlPages.nodeKey, after))).orderBy(asc21(siteCrawlPages.nodeKey)).limit(200).all();
71415
71814
  for (const page3 of pages) {
71416
71815
  evidenceHash.update(JSON.stringify(page3));
71417
71816
  const fields = record(page3.auditFields);
@@ -71773,13 +72172,13 @@ function parseLinkKind(value) {
71773
72172
  return allowed.data;
71774
72173
  }
71775
72174
  function linkKindFilter(linkKind) {
71776
- if (linkKind === SiteHealthLinkKinds.content) return eq99(siteCrawlEdges.isTemplate, false);
71777
- if (linkKind === SiteHealthLinkKinds.template) return eq99(siteCrawlEdges.isTemplate, true);
72175
+ if (linkKind === SiteHealthLinkKinds.content) return eq100(siteCrawlEdges.isTemplate, false);
72176
+ if (linkKind === SiteHealthLinkKinds.template) return eq100(siteCrawlEdges.isTemplate, true);
71778
72177
  return void 0;
71779
72178
  }
71780
72179
  var graphSourceNode = alias(siteCrawlGraphNodes, "site_crawl_graph_source_node");
71781
72180
  var graphTargetNode = alias(siteCrawlGraphNodes, "site_crawl_graph_target_node");
71782
- var PAGE_CHANGE_PREDICATE = sql35`(
72181
+ var PAGE_CHANGE_PREDICATE = sql36`(
71783
72182
  current.url IS NOT previous.url
71784
72183
  OR current.final_url IS NOT previous.final_url
71785
72184
  OR current.path IS NOT previous.path
@@ -71801,7 +72200,7 @@ var PAGE_CHANGE_PREDICATE = sql35`(
71801
72200
  OR current.link_score_raw IS NOT previous.link_score_raw
71802
72201
  OR current.link_score_normalized IS NOT previous.link_score_normalized
71803
72202
  )`;
71804
- var LINK_CHANGE_PREDICATE = sql35`(
72203
+ var LINK_CHANGE_PREDICATE = sql36`(
71805
72204
  current.source_node_key IS NOT previous.source_node_key
71806
72205
  OR current.source_url IS NOT previous.source_url
71807
72206
  OR current.target_node_key IS NOT previous.target_node_key
@@ -71878,20 +72277,20 @@ function changedFields(before, after, fields) {
71878
72277
  }
71879
72278
  async function technicalAeoRoutes(app, opts) {
71880
72279
  const visibleCrawlFilters = (projectId) => [
71881
- eq99(siteCrawlSnapshots.projectId, projectId),
71882
- eq99(runs.projectId, projectId),
71883
- eq99(runs.kind, RunKinds["site-audit"]),
71884
- inArray43(runs.status, SURFACEABLE_STATUSES),
72280
+ eq100(siteCrawlSnapshots.projectId, projectId),
72281
+ eq100(runs.projectId, projectId),
72282
+ eq100(runs.kind, RunKinds["site-audit"]),
72283
+ inArray44(runs.status, SURFACEABLE_STATUSES),
71885
72284
  notProbeRun()
71886
72285
  ];
71887
72286
  const availableScanDatesFor = (projectId, requestedDate) => {
71888
72287
  if (!requestedDate) return void 0;
71889
- const date = sql35`substr(${siteCrawlSnapshots.createdAt}, 1, 10)`;
72288
+ const date = sql36`substr(${siteCrawlSnapshots.createdAt}, 1, 10)`;
71890
72289
  const filters = visibleCrawlFilters(projectId);
71891
- const allDates = and82(...filters);
71892
- const matchingDates = and82(...filters, eq99(sql35`substr(${siteCrawlSnapshots.createdAt}, 6, 5)`, requestedDate.slice(5)));
71893
- const readDates = (where) => app.db.select({ date }).from(siteCrawlSnapshots).innerJoin(runs, eq99(siteCrawlSnapshots.runId, runs.id)).where(where).groupBy(date).orderBy(desc47(date)).limit(10).all().map((row) => row.date);
71894
- const countDates = (where) => app.db.select({ value: sql35`count(distinct ${date})` }).from(siteCrawlSnapshots).innerJoin(runs, eq99(siteCrawlSnapshots.runId, runs.id)).where(where).get()?.value ?? 0;
72290
+ const allDates = and83(...filters);
72291
+ const matchingDates = and83(...filters, eq100(sql36`substr(${siteCrawlSnapshots.createdAt}, 6, 5)`, requestedDate.slice(5)));
72292
+ const readDates = (where) => app.db.select({ date }).from(siteCrawlSnapshots).innerJoin(runs, eq100(siteCrawlSnapshots.runId, runs.id)).where(where).groupBy(date).orderBy(desc48(date)).limit(10).all().map((row) => row.date);
72293
+ const countDates = (where) => app.db.select({ value: sql36`count(distinct ${date})` }).from(siteCrawlSnapshots).innerJoin(runs, eq100(siteCrawlSnapshots.runId, runs.id)).where(where).get()?.value ?? 0;
71895
72294
  return {
71896
72295
  recentDates: readDates(allDates),
71897
72296
  totalDates: countDates(allDates),
@@ -71907,34 +72306,34 @@ async function technicalAeoRoutes(app, opts) {
71907
72306
  nextDate2.setUTCDate(nextDate2.getUTCDate() + 1);
71908
72307
  filters.push(gte27(siteCrawlSnapshots.createdAt, requestedDate), lt14(siteCrawlSnapshots.createdAt, nextDate2.toISOString()));
71909
72308
  }
71910
- const selectionFilters = scope.scoredOnly ? [...filters, sql35`exists (select 1 from ${siteAuditSnapshots} where ${siteAuditSnapshots.projectId} = ${siteCrawlSnapshots.projectId} and ${siteAuditSnapshots.runId} = ${siteCrawlSnapshots.runId})`] : filters;
71911
- const selected = app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs, eq99(siteCrawlSnapshots.runId, runs.id)).where(and82(...selectionFilters, runId ? eq99(siteCrawlSnapshots.runId, runId) : void 0)).orderBy(
71912
- desc47(sql35`substr(${siteCrawlSnapshots.createdAt}, 1, 10)`),
71913
- desc47(siteCrawlSnapshots.complete),
71914
- desc47(siteCrawlSnapshots.pagesFetched),
71915
- desc47(siteCrawlSnapshots.createdAt),
71916
- desc47(siteCrawlSnapshots.runId)
72309
+ const selectionFilters = scope.scoredOnly ? [...filters, sql36`exists (select 1 from ${siteAuditSnapshots} where ${siteAuditSnapshots.projectId} = ${siteCrawlSnapshots.projectId} and ${siteAuditSnapshots.runId} = ${siteCrawlSnapshots.runId})`] : filters;
72310
+ const selected = app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs, eq100(siteCrawlSnapshots.runId, runs.id)).where(and83(...selectionFilters, runId ? eq100(siteCrawlSnapshots.runId, runId) : void 0)).orderBy(
72311
+ desc48(sql36`substr(${siteCrawlSnapshots.createdAt}, 1, 10)`),
72312
+ desc48(siteCrawlSnapshots.complete),
72313
+ desc48(siteCrawlSnapshots.pagesFetched),
72314
+ desc48(siteCrawlSnapshots.createdAt),
72315
+ desc48(siteCrawlSnapshots.runId)
71917
72316
  ).limit(1).get();
71918
72317
  if (!selected) return void 0;
71919
72318
  const date = selected.snapshot.createdAt.slice(0, 10);
71920
72319
  const nextDate = /* @__PURE__ */ new Date(`${date}T00:00:00.000Z`);
71921
72320
  nextDate.setUTCDate(nextDate.getUTCDate() + 1);
71922
- const sameDate = and82(
72321
+ const sameDate = and83(
71923
72322
  ...filters,
71924
72323
  gte27(siteCrawlSnapshots.createdAt, date),
71925
72324
  lt14(siteCrawlSnapshots.createdAt, nextDate.toISOString())
71926
72325
  );
71927
- const sameDateRunCount = app.db.select({ value: count8() }).from(siteCrawlSnapshots).innerJoin(runs, eq99(siteCrawlSnapshots.runId, runs.id)).where(sameDate).get()?.value ?? 0;
72326
+ const sameDateRunCount = app.db.select({ value: count8() }).from(siteCrawlSnapshots).innerJoin(runs, eq100(siteCrawlSnapshots.runId, runs.id)).where(sameDate).get()?.value ?? 0;
71928
72327
  const candidates = app.db.select({
71929
72328
  runId: siteCrawlSnapshots.runId,
71930
72329
  createdAt: siteCrawlSnapshots.createdAt,
71931
72330
  complete: siteCrawlSnapshots.complete,
71932
72331
  pages: siteCrawlSnapshots.pagesFetched
71933
- }).from(siteCrawlSnapshots).innerJoin(runs, eq99(siteCrawlSnapshots.runId, runs.id)).where(sameDate).orderBy(
71934
- desc47(siteCrawlSnapshots.complete),
71935
- desc47(siteCrawlSnapshots.pagesFetched),
71936
- desc47(siteCrawlSnapshots.createdAt),
71937
- desc47(siteCrawlSnapshots.runId)
72332
+ }).from(siteCrawlSnapshots).innerJoin(runs, eq100(siteCrawlSnapshots.runId, runs.id)).where(sameDate).orderBy(
72333
+ desc48(siteCrawlSnapshots.complete),
72334
+ desc48(siteCrawlSnapshots.pagesFetched),
72335
+ desc48(siteCrawlSnapshots.createdAt),
72336
+ desc48(siteCrawlSnapshots.runId)
71938
72337
  ).limit(10).all();
71939
72338
  const runSelection = {
71940
72339
  reason: runId ? "explicit-run" : scope.scoredOnly ? "latest-scored-scan" : requestedDate ? selected.snapshot.complete ? "requested-date-complete" : "requested-date-most-pages" : selected.snapshot.complete ? "latest-date-complete" : "latest-date-most-pages",
@@ -71948,20 +72347,20 @@ async function technicalAeoRoutes(app, opts) {
71948
72347
  };
71949
72348
  const resolveAudit = (projectId, runId, recordedBefore) => {
71950
72349
  const filters = [
71951
- eq99(siteAuditSnapshots.projectId, projectId),
71952
- eq99(runs.projectId, projectId),
71953
- eq99(runs.kind, RunKinds["site-audit"]),
71954
- inArray43(runs.status, SURFACEABLE_STATUSES),
72350
+ eq100(siteAuditSnapshots.projectId, projectId),
72351
+ eq100(runs.projectId, projectId),
72352
+ eq100(runs.kind, RunKinds["site-audit"]),
72353
+ inArray44(runs.status, SURFACEABLE_STATUSES),
71955
72354
  notProbeRun(),
71956
72355
  ...recordedBefore ? [lt14(siteAuditSnapshots.createdAt, recordedBefore)] : []
71957
72356
  ];
71958
- const complete = sql35`case when ${runs.status} = ${RunStatuses.completed} then 1 else 0 end`;
71959
- const selectAudit = (pinnedRunId) => app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq99(siteAuditSnapshots.runId, runs.id)).where(and82(...filters, pinnedRunId ? eq99(siteAuditSnapshots.runId, pinnedRunId) : void 0)).orderBy(
71960
- desc47(sql35`substr(${siteAuditSnapshots.createdAt}, 1, 10)`),
71961
- desc47(complete),
71962
- desc47(siteAuditSnapshots.pagesAudited),
71963
- desc47(siteAuditSnapshots.createdAt),
71964
- desc47(siteAuditSnapshots.runId)
72357
+ const complete = sql36`case when ${runs.status} = ${RunStatuses.completed} then 1 else 0 end`;
72358
+ const selectAudit = (pinnedRunId) => app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq100(siteAuditSnapshots.runId, runs.id)).where(and83(...filters, pinnedRunId ? eq100(siteAuditSnapshots.runId, pinnedRunId) : void 0)).orderBy(
72359
+ desc48(sql36`substr(${siteAuditSnapshots.createdAt}, 1, 10)`),
72360
+ desc48(complete),
72361
+ desc48(siteAuditSnapshots.pagesAudited),
72362
+ desc48(siteAuditSnapshots.createdAt),
72363
+ desc48(siteAuditSnapshots.runId)
71965
72364
  ).limit(1).get();
71966
72365
  const auditOfCrawl = (target) => {
71967
72366
  const selected2 = target ? selectAudit(target.snapshot.runId) : void 0;
@@ -71977,22 +72376,22 @@ async function technicalAeoRoutes(app, opts) {
71977
72376
  const date = selected.snap.createdAt.slice(0, 10);
71978
72377
  const nextDate = /* @__PURE__ */ new Date(`${date}T00:00:00.000Z`);
71979
72378
  nextDate.setUTCDate(nextDate.getUTCDate() + 1);
71980
- const sameDate = and82(
72379
+ const sameDate = and83(
71981
72380
  ...filters,
71982
72381
  gte27(siteAuditSnapshots.createdAt, date),
71983
72382
  lt14(siteAuditSnapshots.createdAt, nextDate.toISOString())
71984
72383
  );
71985
- const sameDateRunCount = app.db.select({ value: count8() }).from(siteAuditSnapshots).innerJoin(runs, eq99(siteAuditSnapshots.runId, runs.id)).where(sameDate).get()?.value ?? 0;
72384
+ const sameDateRunCount = app.db.select({ value: count8() }).from(siteAuditSnapshots).innerJoin(runs, eq100(siteAuditSnapshots.runId, runs.id)).where(sameDate).get()?.value ?? 0;
71986
72385
  const candidates = app.db.select({
71987
72386
  runId: siteAuditSnapshots.runId,
71988
72387
  createdAt: siteAuditSnapshots.createdAt,
71989
72388
  complete,
71990
72389
  pages: siteAuditSnapshots.pagesAudited
71991
- }).from(siteAuditSnapshots).innerJoin(runs, eq99(siteAuditSnapshots.runId, runs.id)).where(sameDate).orderBy(
71992
- desc47(complete),
71993
- desc47(siteAuditSnapshots.pagesAudited),
71994
- desc47(siteAuditSnapshots.createdAt),
71995
- desc47(siteAuditSnapshots.runId)
72390
+ }).from(siteAuditSnapshots).innerJoin(runs, eq100(siteAuditSnapshots.runId, runs.id)).where(sameDate).orderBy(
72391
+ desc48(complete),
72392
+ desc48(siteAuditSnapshots.pagesAudited),
72393
+ desc48(siteAuditSnapshots.createdAt),
72394
+ desc48(siteAuditSnapshots.runId)
71996
72395
  ).limit(10).all().map((candidate) => ({ ...candidate, complete: Boolean(candidate.complete) }));
71997
72396
  const runSelection = {
71998
72397
  // A crawl exists here only when none stored a scorecard: this score is a fallback.
@@ -72007,9 +72406,9 @@ async function technicalAeoRoutes(app, opts) {
72007
72406
  };
72008
72407
  const detailScopeFor = (projectId, snapshot) => snapshot.detailsAvailable && snapshot.attemptId ? { projectId, runId: snapshot.runId, attemptId: snapshot.attemptId } : null;
72009
72408
  const crawlReasonGroups = (where) => {
72010
- const canonicalOther = sql35`case when ${siteCrawlPages.canonicalNodeKey} is not null
72409
+ const canonicalOther = sql36`case when ${siteCrawlPages.canonicalNodeKey} is not null
72011
72410
  and ${siteCrawlPages.canonicalNodeKey} <> ${siteCrawlPages.nodeKey} then 1 else 0 end`;
72012
- const canonicalReason = sql35`exists (select 1 from json_each(${siteCrawlPages.indexabilityReasons})
72411
+ const canonicalReason = sql36`exists (select 1 from json_each(${siteCrawlPages.indexabilityReasons})
72013
72412
  where value = ${SiteCrawlIndexabilityReasons.canonicalToOther})`;
72014
72413
  const groups = app.db.select({
72015
72414
  fetchState: siteCrawlPages.fetchState,
@@ -72018,7 +72417,7 @@ async function technicalAeoRoutes(app, opts) {
72018
72417
  canonicalOther,
72019
72418
  canonicalReason,
72020
72419
  pages: count8(),
72021
- exampleUrl: sql35`min(${siteCrawlPages.url})`
72420
+ exampleUrl: sql36`min(${siteCrawlPages.url})`
72022
72421
  }).from(siteCrawlPages).where(where).groupBy(
72023
72422
  siteCrawlPages.fetchState,
72024
72423
  siteCrawlPages.indexabilityState,
@@ -72054,10 +72453,10 @@ async function technicalAeoRoutes(app, opts) {
72054
72453
  };
72055
72454
  const inventorySummaryFor = (scope) => {
72056
72455
  if (!scope) return null;
72057
- const groups = crawlReasonGroups(and82(
72058
- eq99(siteCrawlPages.projectId, scope.projectId),
72059
- eq99(siteCrawlPages.runId, scope.runId),
72060
- eq99(siteCrawlPages.attemptId, scope.attemptId)
72456
+ const groups = crawlReasonGroups(and83(
72457
+ eq100(siteCrawlPages.projectId, scope.projectId),
72458
+ eq100(siteCrawlPages.runId, scope.runId),
72459
+ eq100(siteCrawlPages.attemptId, scope.attemptId)
72061
72460
  ));
72062
72461
  const total = groups.reduce((sum, group) => sum + group.pages, 0);
72063
72462
  const eligible = groups.reduce((sum, group) => sum + (group.inventoryEligible ? group.pages : 0), 0);
@@ -72077,34 +72476,34 @@ async function technicalAeoRoutes(app, opts) {
72077
72476
  const NO_CRAWL_COMPLETENESS = { complete: false, termination: null };
72078
72477
  const pageInScope = (scope, selector) => {
72079
72478
  if (!selector.nodeKey && !selector.url) return null;
72080
- return app.db.select().from(siteCrawlPages).where(and82(
72081
- eq99(siteCrawlPages.projectId, scope.projectId),
72082
- eq99(siteCrawlPages.runId, scope.runId),
72083
- eq99(siteCrawlPages.attemptId, scope.attemptId),
72084
- selector.nodeKey ? eq99(siteCrawlPages.nodeKey, selector.nodeKey) : eq99(siteCrawlPages.url, selector.url)
72085
- )).orderBy(asc21(siteCrawlPages.nodeKey)).limit(1).get() ?? null;
72086
- };
72087
- const rootPageInScope = (scope, rootUrl) => pageInScope(scope, { url: rootUrl }) ?? app.db.select().from(siteCrawlPages).where(and82(
72088
- eq99(siteCrawlPages.projectId, scope.projectId),
72089
- eq99(siteCrawlPages.runId, scope.runId),
72090
- eq99(siteCrawlPages.attemptId, scope.attemptId),
72091
- eq99(siteCrawlPages.depth, 0)
72092
- )).orderBy(asc21(siteCrawlPages.nodeKey)).limit(1).get() ?? null;
72093
- const isSurfaceableAuditRun = (projectId, runId) => Boolean(app.db.select({ id: runs.id }).from(runs).where(and82(
72094
- eq99(runs.id, runId),
72095
- eq99(runs.projectId, projectId),
72096
- eq99(runs.kind, RunKinds["site-audit"]),
72097
- inArray43(runs.status, SURFACEABLE_STATUSES),
72479
+ return app.db.select().from(siteCrawlPages).where(and83(
72480
+ eq100(siteCrawlPages.projectId, scope.projectId),
72481
+ eq100(siteCrawlPages.runId, scope.runId),
72482
+ eq100(siteCrawlPages.attemptId, scope.attemptId),
72483
+ selector.nodeKey ? eq100(siteCrawlPages.nodeKey, selector.nodeKey) : eq100(siteCrawlPages.url, selector.url)
72484
+ )).orderBy(asc22(siteCrawlPages.nodeKey)).limit(1).get() ?? null;
72485
+ };
72486
+ const rootPageInScope = (scope, rootUrl) => pageInScope(scope, { url: rootUrl }) ?? app.db.select().from(siteCrawlPages).where(and83(
72487
+ eq100(siteCrawlPages.projectId, scope.projectId),
72488
+ eq100(siteCrawlPages.runId, scope.runId),
72489
+ eq100(siteCrawlPages.attemptId, scope.attemptId),
72490
+ eq100(siteCrawlPages.depth, 0)
72491
+ )).orderBy(asc22(siteCrawlPages.nodeKey)).limit(1).get() ?? null;
72492
+ const isSurfaceableAuditRun = (projectId, runId) => Boolean(app.db.select({ id: runs.id }).from(runs).where(and83(
72493
+ eq100(runs.id, runId),
72494
+ eq100(runs.projectId, projectId),
72495
+ eq100(runs.kind, RunKinds["site-audit"]),
72496
+ inArray44(runs.status, SURFACEABLE_STATUSES),
72098
72497
  notProbeRun()
72099
72498
  )).limit(1).get());
72100
72499
  const assertKnownAuditRun = (projectId, runId) => {
72101
72500
  if (runId && !isSurfaceableAuditRun(projectId, runId)) throw notFound("Site crawl run", runId);
72102
72501
  };
72103
- const hasLegacyAudit = (projectId) => Boolean(app.db.select({ runId: siteAuditSnapshots.runId }).from(siteAuditSnapshots).innerJoin(runs, eq99(siteAuditSnapshots.runId, runs.id)).where(and82(
72104
- eq99(siteAuditSnapshots.projectId, projectId),
72105
- eq99(runs.projectId, projectId),
72106
- eq99(runs.kind, RunKinds["site-audit"]),
72107
- inArray43(runs.status, SURFACEABLE_STATUSES),
72502
+ const hasLegacyAudit = (projectId) => Boolean(app.db.select({ runId: siteAuditSnapshots.runId }).from(siteAuditSnapshots).innerJoin(runs, eq100(siteAuditSnapshots.runId, runs.id)).where(and83(
72503
+ eq100(siteAuditSnapshots.projectId, projectId),
72504
+ eq100(runs.projectId, projectId),
72505
+ eq100(runs.kind, RunKinds["site-audit"]),
72506
+ inArray44(runs.status, SURFACEABLE_STATUSES),
72108
72507
  notProbeRun()
72109
72508
  )).limit(1).get());
72110
72509
  const emptyCrawlSummary = (projectName, legacyAuditAvailable) => ({
@@ -72205,14 +72604,14 @@ async function technicalAeoRoutes(app, opts) {
72205
72604
  };
72206
72605
  }
72207
72606
  const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
72208
- const conds = [eq99(siteAuditPages.projectId, project.id), eq99(siteAuditPages.runId, latest.snap.runId)];
72209
- if (statusFilter) conds.push(eq99(siteAuditPages.status, statusFilter));
72210
- const where = and82(...conds);
72607
+ const conds = [eq100(siteAuditPages.projectId, project.id), eq100(siteAuditPages.runId, latest.snap.runId)];
72608
+ if (statusFilter) conds.push(eq100(siteAuditPages.status, statusFilter));
72609
+ const where = and83(...conds);
72211
72610
  const totalRow = app.db.select({ value: count8() }).from(siteAuditPages).where(where).get();
72212
72611
  const total = totalRow?.value ?? 0;
72213
72612
  const limit = parsePositiveInt(request.query.limit, 100, 500);
72214
72613
  const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
72215
- const orderBy = request.query.sort === "score-desc" ? desc47(siteAuditPages.overallScore) : request.query.sort === "url" ? asc21(siteAuditPages.url) : asc21(siteAuditPages.overallScore);
72614
+ const orderBy = request.query.sort === "score-desc" ? desc48(siteAuditPages.overallScore) : request.query.sort === "url" ? asc22(siteAuditPages.url) : asc22(siteAuditPages.overallScore);
72216
72615
  const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
72217
72616
  const pages = rows.map((row) => ({
72218
72617
  url: row.url,
@@ -72231,13 +72630,13 @@ async function technicalAeoRoutes(app, opts) {
72231
72630
  auditedAt: siteAuditSnapshots.auditedAt,
72232
72631
  aggregateScore: siteAuditSnapshots.aggregateScore,
72233
72632
  pagesAudited: siteAuditSnapshots.pagesAudited
72234
- }).from(siteAuditSnapshots).innerJoin(runs, eq99(siteAuditSnapshots.runId, runs.id)).where(and82(
72235
- eq99(siteAuditSnapshots.projectId, project.id),
72236
- eq99(runs.projectId, project.id),
72237
- eq99(runs.kind, RunKinds["site-audit"]),
72238
- inArray43(runs.status, SURFACEABLE_STATUSES),
72633
+ }).from(siteAuditSnapshots).innerJoin(runs, eq100(siteAuditSnapshots.runId, runs.id)).where(and83(
72634
+ eq100(siteAuditSnapshots.projectId, project.id),
72635
+ eq100(runs.projectId, project.id),
72636
+ eq100(runs.kind, RunKinds["site-audit"]),
72637
+ inArray44(runs.status, SURFACEABLE_STATUSES),
72239
72638
  notProbeRun()
72240
- )).orderBy(desc47(siteAuditSnapshots.createdAt)).limit(limit).all();
72639
+ )).orderBy(desc48(siteAuditSnapshots.createdAt)).limit(limit).all();
72241
72640
  return { project: project.name, points: rows.reverse() };
72242
72641
  });
72243
72642
  app.get("/projects/:name/technical-aeo/crawl", async (request) => {
@@ -72337,10 +72736,10 @@ async function technicalAeoRoutes(app, opts) {
72337
72736
  { projectId: project.id, runId: snapshot.runId, attemptId: snapshot.attemptId },
72338
72737
  snapshot.rootUrl
72339
72738
  )?.nodeKey ?? null;
72340
- const persistedLayout = app.db.select().from(siteCrawlGraphLayouts).where(and82(
72341
- eq99(siteCrawlGraphLayouts.projectId, project.id),
72342
- eq99(siteCrawlGraphLayouts.runId, snapshot.runId),
72343
- eq99(siteCrawlGraphLayouts.attemptId, snapshot.attemptId)
72739
+ const persistedLayout = app.db.select().from(siteCrawlGraphLayouts).where(and83(
72740
+ eq100(siteCrawlGraphLayouts.projectId, project.id),
72741
+ eq100(siteCrawlGraphLayouts.runId, snapshot.runId),
72742
+ eq100(siteCrawlGraphLayouts.attemptId, snapshot.attemptId)
72344
72743
  )).limit(1).get();
72345
72744
  if (!persistedLayout) {
72346
72745
  return {
@@ -72393,9 +72792,9 @@ async function technicalAeoRoutes(app, opts) {
72393
72792
  const maxNodes = parseBoundedLimit(request.query.maxNodes, SITE_CRAWL_GRAPH_DEFAULT_MAX_NODES, SITE_CRAWL_GRAPH_MAX_NODES);
72394
72793
  const maxEdges = parseBoundedLimit(request.query.maxEdges, SITE_CRAWL_GRAPH_DEFAULT_MAX_EDGES, SITE_CRAWL_GRAPH_MAX_EDGES);
72395
72794
  const graphScope = [
72396
- eq99(siteCrawlGraphNodes.projectId, project.id),
72397
- eq99(siteCrawlGraphNodes.runId, snapshot.runId),
72398
- eq99(siteCrawlGraphNodes.attemptId, snapshot.attemptId)
72795
+ eq100(siteCrawlGraphNodes.projectId, project.id),
72796
+ eq100(siteCrawlGraphNodes.runId, snapshot.runId),
72797
+ eq100(siteCrawlGraphNodes.attemptId, snapshot.attemptId)
72399
72798
  ];
72400
72799
  const nodeRows = app.db.select({
72401
72800
  nodeKey: siteCrawlPages.nodeKey,
@@ -72414,18 +72813,18 @@ async function technicalAeoRoutes(app, opts) {
72414
72813
  linkScoreNormalized: siteCrawlPages.linkScoreNormalized,
72415
72814
  x: siteCrawlGraphNodes.x,
72416
72815
  y: siteCrawlGraphNodes.y
72417
- }).from(siteCrawlGraphNodes).innerJoin(siteCrawlPages, and82(
72418
- eq99(siteCrawlPages.projectId, siteCrawlGraphNodes.projectId),
72419
- eq99(siteCrawlPages.runId, siteCrawlGraphNodes.runId),
72420
- eq99(siteCrawlPages.attemptId, siteCrawlGraphNodes.attemptId),
72421
- eq99(siteCrawlPages.nodeKey, siteCrawlGraphNodes.nodeKey)
72422
- )).where(and82(...graphScope, lt14(siteCrawlGraphNodes.sampleRank, maxNodes))).orderBy(asc21(siteCrawlGraphNodes.sampleRank)).all();
72816
+ }).from(siteCrawlGraphNodes).innerJoin(siteCrawlPages, and83(
72817
+ eq100(siteCrawlPages.projectId, siteCrawlGraphNodes.projectId),
72818
+ eq100(siteCrawlPages.runId, siteCrawlGraphNodes.runId),
72819
+ eq100(siteCrawlPages.attemptId, siteCrawlGraphNodes.attemptId),
72820
+ eq100(siteCrawlPages.nodeKey, siteCrawlGraphNodes.nodeKey)
72821
+ )).where(and83(...graphScope, lt14(siteCrawlGraphNodes.sampleRank, maxNodes))).orderBy(asc22(siteCrawlGraphNodes.sampleRank)).all();
72423
72822
  const nodes = nodeRows.map(({ indexabilityReasons, canonicalNodeKey, ...row }) => ({
72424
72823
  ...row,
72425
72824
  healthState: deriveSiteHealthState({ ...row, indexabilityReasons, canonicalNodeKey }),
72426
72825
  healthReason: deriveSiteHealthReason({ ...row, indexabilityReasons, canonicalNodeKey })
72427
72826
  }));
72428
- const graphLinkKindFilter = linkKind === SiteHealthLinkKinds.all ? void 0 : eq99(siteCrawlGraphEdges.isTemplate, linkKind === SiteHealthLinkKinds.template);
72827
+ const graphLinkKindFilter = linkKind === SiteHealthLinkKinds.all ? void 0 : eq100(siteCrawlGraphEdges.isTemplate, linkKind === SiteHealthLinkKinds.template);
72429
72828
  const edges = app.db.select({
72430
72829
  edgeKey: siteCrawlGraphEdges.edgeKey,
72431
72830
  sourceNodeKey: siteCrawlGraphEdges.sourceNodeKey,
@@ -72433,25 +72832,25 @@ async function technicalAeoRoutes(app, opts) {
72433
72832
  followable: siteCrawlGraphEdges.followable,
72434
72833
  occurrences: siteCrawlGraphEdges.occurrences,
72435
72834
  isTemplate: siteCrawlGraphEdges.isTemplate
72436
- }).from(siteCrawlGraphEdges).innerJoin(graphSourceNode, and82(
72437
- eq99(graphSourceNode.projectId, siteCrawlGraphEdges.projectId),
72438
- eq99(graphSourceNode.runId, siteCrawlGraphEdges.runId),
72439
- eq99(graphSourceNode.attemptId, siteCrawlGraphEdges.attemptId),
72440
- eq99(graphSourceNode.nodeKey, siteCrawlGraphEdges.sourceNodeKey),
72835
+ }).from(siteCrawlGraphEdges).innerJoin(graphSourceNode, and83(
72836
+ eq100(graphSourceNode.projectId, siteCrawlGraphEdges.projectId),
72837
+ eq100(graphSourceNode.runId, siteCrawlGraphEdges.runId),
72838
+ eq100(graphSourceNode.attemptId, siteCrawlGraphEdges.attemptId),
72839
+ eq100(graphSourceNode.nodeKey, siteCrawlGraphEdges.sourceNodeKey),
72441
72840
  lt14(graphSourceNode.sampleRank, maxNodes)
72442
- )).innerJoin(graphTargetNode, and82(
72443
- eq99(graphTargetNode.projectId, siteCrawlGraphEdges.projectId),
72444
- eq99(graphTargetNode.runId, siteCrawlGraphEdges.runId),
72445
- eq99(graphTargetNode.attemptId, siteCrawlGraphEdges.attemptId),
72446
- eq99(graphTargetNode.nodeKey, siteCrawlGraphEdges.targetNodeKey),
72841
+ )).innerJoin(graphTargetNode, and83(
72842
+ eq100(graphTargetNode.projectId, siteCrawlGraphEdges.projectId),
72843
+ eq100(graphTargetNode.runId, siteCrawlGraphEdges.runId),
72844
+ eq100(graphTargetNode.attemptId, siteCrawlGraphEdges.attemptId),
72845
+ eq100(graphTargetNode.nodeKey, siteCrawlGraphEdges.targetNodeKey),
72447
72846
  lt14(graphTargetNode.sampleRank, maxNodes)
72448
- )).where(and82(
72449
- eq99(siteCrawlGraphEdges.projectId, project.id),
72450
- eq99(siteCrawlGraphEdges.runId, snapshot.runId),
72451
- eq99(siteCrawlGraphEdges.attemptId, snapshot.attemptId),
72847
+ )).where(and83(
72848
+ eq100(siteCrawlGraphEdges.projectId, project.id),
72849
+ eq100(siteCrawlGraphEdges.runId, snapshot.runId),
72850
+ eq100(siteCrawlGraphEdges.attemptId, snapshot.attemptId),
72452
72851
  lt14(siteCrawlGraphEdges.sampleRank, maxEdges),
72453
72852
  graphLinkKindFilter
72454
- )).orderBy(asc21(siteCrawlGraphEdges.sampleRank)).all();
72853
+ )).orderBy(asc22(siteCrawlGraphEdges.sampleRank)).all();
72455
72854
  const totalNodes = persistedLayout.totalNodes;
72456
72855
  const totalEdges = persistedLayout.totalEdges;
72457
72856
  const totalTemplateEdges = persistedLayout.totalTemplateEdges;
@@ -72543,17 +72942,17 @@ async function technicalAeoRoutes(app, opts) {
72543
72942
  let frontier = [focus.nodeKey];
72544
72943
  let queryTruncated = false;
72545
72944
  for (let distance = 0; distance < hops && frontier.length > 0; distance += 1) {
72546
- const candidates = app.db.select().from(siteCrawlEdges).where(and82(
72547
- eq99(siteCrawlEdges.projectId, scope.projectId),
72548
- eq99(siteCrawlEdges.runId, scope.runId),
72549
- eq99(siteCrawlEdges.attemptId, scope.attemptId),
72550
- eq99(siteCrawlEdges.internal, true),
72945
+ const candidates = app.db.select().from(siteCrawlEdges).where(and83(
72946
+ eq100(siteCrawlEdges.projectId, scope.projectId),
72947
+ eq100(siteCrawlEdges.runId, scope.runId),
72948
+ eq100(siteCrawlEdges.attemptId, scope.attemptId),
72949
+ eq100(siteCrawlEdges.internal, true),
72551
72950
  isNotNull10(siteCrawlEdges.targetNodeKey),
72552
72951
  or21(
72553
- inArray43(siteCrawlEdges.sourceNodeKey, frontier),
72554
- inArray43(siteCrawlEdges.targetNodeKey, frontier)
72952
+ inArray44(siteCrawlEdges.sourceNodeKey, frontier),
72953
+ inArray44(siteCrawlEdges.targetNodeKey, frontier)
72555
72954
  )
72556
- )).orderBy(asc21(siteCrawlEdges.edgeKey)).limit(maxEdges + maxNodes + 1).all();
72955
+ )).orderBy(asc22(siteCrawlEdges.edgeKey)).limit(maxEdges + maxNodes + 1).all();
72557
72956
  if (candidates.length > maxEdges + maxNodes) queryTruncated = true;
72558
72957
  const next = /* @__PURE__ */ new Set();
72559
72958
  for (const edge of candidates.slice(0, maxEdges + maxNodes)) {
@@ -72584,22 +72983,22 @@ async function technicalAeoRoutes(app, opts) {
72584
72983
  frontier = [...next].sort();
72585
72984
  }
72586
72985
  const nodeKeys = [...distances.keys()];
72587
- const pageRows = nodeKeys.length === 0 ? [] : app.db.select().from(siteCrawlPages).where(and82(
72588
- eq99(siteCrawlPages.projectId, scope.projectId),
72589
- eq99(siteCrawlPages.runId, scope.runId),
72590
- eq99(siteCrawlPages.attemptId, scope.attemptId),
72591
- inArray43(siteCrawlPages.nodeKey, nodeKeys)
72986
+ const pageRows = nodeKeys.length === 0 ? [] : app.db.select().from(siteCrawlPages).where(and83(
72987
+ eq100(siteCrawlPages.projectId, scope.projectId),
72988
+ eq100(siteCrawlPages.runId, scope.runId),
72989
+ eq100(siteCrawlPages.attemptId, scope.attemptId),
72990
+ inArray44(siteCrawlPages.nodeKey, nodeKeys)
72592
72991
  )).all();
72593
72992
  const persistedNodeKeys = new Set(pageRows.map((row) => row.nodeKey));
72594
72993
  const outermostNodeKeys = pageRows.filter((row) => distances.get(row.nodeKey) === hops).map((row) => row.nodeKey);
72595
- const omittedOutermostEdge = outermostNodeKeys.length === 0 ? void 0 : app.db.select({ edgeKey: siteCrawlEdges.edgeKey }).from(siteCrawlEdges).where(and82(
72596
- eq99(siteCrawlEdges.projectId, scope.projectId),
72597
- eq99(siteCrawlEdges.runId, scope.runId),
72598
- eq99(siteCrawlEdges.attemptId, scope.attemptId),
72599
- eq99(siteCrawlEdges.internal, true),
72994
+ const omittedOutermostEdge = outermostNodeKeys.length === 0 ? void 0 : app.db.select({ edgeKey: siteCrawlEdges.edgeKey }).from(siteCrawlEdges).where(and83(
72995
+ eq100(siteCrawlEdges.projectId, scope.projectId),
72996
+ eq100(siteCrawlEdges.runId, scope.runId),
72997
+ eq100(siteCrawlEdges.attemptId, scope.attemptId),
72998
+ eq100(siteCrawlEdges.internal, true),
72600
72999
  isNotNull10(siteCrawlEdges.targetNodeKey),
72601
- inArray43(siteCrawlEdges.sourceNodeKey, outermostNodeKeys),
72602
- inArray43(siteCrawlEdges.targetNodeKey, outermostNodeKeys)
73000
+ inArray44(siteCrawlEdges.sourceNodeKey, outermostNodeKeys),
73001
+ inArray44(siteCrawlEdges.targetNodeKey, outermostNodeKeys)
72603
73002
  )).limit(1).get();
72604
73003
  if (omittedOutermostEdge && !seenEdgeKeys.has(omittedOutermostEdge.edgeKey)) {
72605
73004
  omittedEdgeKeys.add(omittedOutermostEdge.edgeKey);
@@ -72692,23 +73091,23 @@ async function technicalAeoRoutes(app, opts) {
72692
73091
  let found = fromPage.nodeKey === toPage.nodeKey;
72693
73092
  let truncated = false;
72694
73093
  for (let depth = 0; depth < maxDepth && frontier.length > 0 && !found; depth += 1) {
72695
- const candidates = app.db.select().from(siteCrawlEdges).where(and82(
72696
- eq99(siteCrawlEdges.projectId, scope.projectId),
72697
- eq99(siteCrawlEdges.runId, scope.runId),
72698
- eq99(siteCrawlEdges.attemptId, scope.attemptId),
72699
- eq99(siteCrawlEdges.internal, true),
72700
- eq99(siteCrawlEdges.followable, true),
72701
- eq99(siteCrawlEdges.relation, "anchor"),
73094
+ const candidates = app.db.select().from(siteCrawlEdges).where(and83(
73095
+ eq100(siteCrawlEdges.projectId, scope.projectId),
73096
+ eq100(siteCrawlEdges.runId, scope.runId),
73097
+ eq100(siteCrawlEdges.attemptId, scope.attemptId),
73098
+ eq100(siteCrawlEdges.internal, true),
73099
+ eq100(siteCrawlEdges.followable, true),
73100
+ eq100(siteCrawlEdges.relation, "anchor"),
72702
73101
  isNotNull10(siteCrawlEdges.targetNodeKey),
72703
- inArray43(siteCrawlEdges.sourceNodeKey, frontier)
72704
- )).orderBy(asc21(siteCrawlEdges.edgeKey)).limit(SITE_HEALTH_PATH_MAX_VISITED_NODES + 1).all();
73102
+ inArray44(siteCrawlEdges.sourceNodeKey, frontier)
73103
+ )).orderBy(asc22(siteCrawlEdges.edgeKey)).limit(SITE_HEALTH_PATH_MAX_VISITED_NODES + 1).all();
72705
73104
  if (candidates.length > SITE_HEALTH_PATH_MAX_VISITED_NODES) truncated = true;
72706
73105
  const candidateTargetKeys = [...new Set(candidates.map((edge) => edge.targetNodeKey).filter((nodeKey) => nodeKey != null))];
72707
- const persistedTargetKeys = new Set(candidateTargetKeys.length === 0 ? [] : app.db.select({ nodeKey: siteCrawlPages.nodeKey }).from(siteCrawlPages).where(and82(
72708
- eq99(siteCrawlPages.projectId, scope.projectId),
72709
- eq99(siteCrawlPages.runId, scope.runId),
72710
- eq99(siteCrawlPages.attemptId, scope.attemptId),
72711
- inArray43(siteCrawlPages.nodeKey, candidateTargetKeys)
73106
+ const persistedTargetKeys = new Set(candidateTargetKeys.length === 0 ? [] : app.db.select({ nodeKey: siteCrawlPages.nodeKey }).from(siteCrawlPages).where(and83(
73107
+ eq100(siteCrawlPages.projectId, scope.projectId),
73108
+ eq100(siteCrawlPages.runId, scope.runId),
73109
+ eq100(siteCrawlPages.attemptId, scope.attemptId),
73110
+ inArray44(siteCrawlPages.nodeKey, candidateTargetKeys)
72712
73111
  )).all().map((row) => row.nodeKey));
72713
73112
  const next = /* @__PURE__ */ new Set();
72714
73113
  for (const edge of candidates.slice(0, SITE_HEALTH_PATH_MAX_VISITED_NODES)) {
@@ -72749,11 +73148,11 @@ async function technicalAeoRoutes(app, opts) {
72749
73148
  pathEdges.unshift(step.edge);
72750
73149
  pathKeys.unshift(step.nodeKey);
72751
73150
  }
72752
- const pathRows = app.db.select().from(siteCrawlPages).where(and82(
72753
- eq99(siteCrawlPages.projectId, scope.projectId),
72754
- eq99(siteCrawlPages.runId, scope.runId),
72755
- eq99(siteCrawlPages.attemptId, scope.attemptId),
72756
- inArray43(siteCrawlPages.nodeKey, pathKeys)
73151
+ const pathRows = app.db.select().from(siteCrawlPages).where(and83(
73152
+ eq100(siteCrawlPages.projectId, scope.projectId),
73153
+ eq100(siteCrawlPages.runId, scope.runId),
73154
+ eq100(siteCrawlPages.attemptId, scope.attemptId),
73155
+ inArray44(siteCrawlPages.nodeKey, pathKeys)
72757
73156
  )).all();
72758
73157
  const pageByKey = new Map(pathRows.map((row) => [row.nodeKey, row]));
72759
73158
  const pathDetection = templateDetectionOf(snapshot.templateDetection);
@@ -72781,25 +73180,25 @@ async function technicalAeoRoutes(app, opts) {
72781
73180
  throw validationError("change must be all, added, removed, or changed");
72782
73181
  }
72783
73182
  const completeSnapshotFilters = [
72784
- eq99(siteCrawlSnapshots.projectId, project.id),
72785
- eq99(runs.projectId, project.id),
72786
- eq99(runs.kind, RunKinds["site-audit"]),
72787
- eq99(runs.status, RunStatuses.completed),
72788
- eq99(siteCrawlSnapshots.complete, true),
73183
+ eq100(siteCrawlSnapshots.projectId, project.id),
73184
+ eq100(runs.projectId, project.id),
73185
+ eq100(runs.kind, RunKinds["site-audit"]),
73186
+ eq100(runs.status, RunStatuses.completed),
73187
+ eq100(siteCrawlSnapshots.complete, true),
72789
73188
  notProbeRun()
72790
73189
  ];
72791
- const afterTarget = request.query.toRunId ? resolveCrawl(project.id, request.query.toRunId) : app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs, eq99(siteCrawlSnapshots.runId, runs.id)).where(and82(...completeSnapshotFilters)).orderBy(desc47(siteCrawlSnapshots.createdAt), desc47(siteCrawlSnapshots.runId)).limit(1).get();
73190
+ const afterTarget = request.query.toRunId ? resolveCrawl(project.id, request.query.toRunId) : app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs, eq100(siteCrawlSnapshots.runId, runs.id)).where(and83(...completeSnapshotFilters)).orderBy(desc48(siteCrawlSnapshots.createdAt), desc48(siteCrawlSnapshots.runId)).limit(1).get();
72792
73191
  if (request.query.toRunId && !afterTarget) throw notFound("Site crawl run", request.query.toRunId);
72793
- const beforeTarget = request.query.fromRunId ? resolveCrawl(project.id, request.query.fromRunId) : afterTarget ? app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs, eq99(siteCrawlSnapshots.runId, runs.id)).where(and82(
73192
+ const beforeTarget = request.query.fromRunId ? resolveCrawl(project.id, request.query.fromRunId) : afterTarget ? app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs, eq100(siteCrawlSnapshots.runId, runs.id)).where(and83(
72794
73193
  ...completeSnapshotFilters,
72795
73194
  or21(
72796
73195
  lt14(siteCrawlSnapshots.createdAt, afterTarget.snapshot.createdAt),
72797
- and82(
72798
- eq99(siteCrawlSnapshots.createdAt, afterTarget.snapshot.createdAt),
73196
+ and83(
73197
+ eq100(siteCrawlSnapshots.createdAt, afterTarget.snapshot.createdAt),
72799
73198
  lt14(siteCrawlSnapshots.runId, afterTarget.snapshot.runId)
72800
73199
  )
72801
73200
  )
72802
- )).orderBy(desc47(siteCrawlSnapshots.createdAt), desc47(siteCrawlSnapshots.runId)).limit(1).get() : void 0;
73201
+ )).orderBy(desc48(siteCrawlSnapshots.createdAt), desc48(siteCrawlSnapshots.runId)).limit(1).get() : void 0;
72803
73202
  if (request.query.fromRunId && !beforeTarget) throw notFound("Site crawl run", request.query.fromRunId);
72804
73203
  if (!afterTarget) {
72805
73204
  const latestCrawl = resolveCrawl(project.id);
@@ -72883,9 +73282,9 @@ async function technicalAeoRoutes(app, opts) {
72883
73282
  const summary4 = { pages: zero2(), links: zero2() };
72884
73283
  if (scopeFilter !== "links") {
72885
73284
  if (includeAdded || includeChanged) {
72886
- const addedExpression = includeAdded ? sql35`COALESCE(SUM(CASE WHEN previous.id IS NULL THEN 1 ELSE 0 END), 0)` : sql35`0`;
72887
- const changedExpression = includeChanged ? sql35`COALESCE(SUM(CASE WHEN previous.id IS NOT NULL AND ${PAGE_CHANGE_PREDICATE} THEN 1 ELSE 0 END), 0)` : sql35`0`;
72888
- const [counts2 = { added: 0, changed: 0 }] = app.db.all(sql35`
73285
+ const addedExpression = includeAdded ? sql36`COALESCE(SUM(CASE WHEN previous.id IS NULL THEN 1 ELSE 0 END), 0)` : sql36`0`;
73286
+ const changedExpression = includeChanged ? sql36`COALESCE(SUM(CASE WHEN previous.id IS NOT NULL AND ${PAGE_CHANGE_PREDICATE} THEN 1 ELSE 0 END), 0)` : sql36`0`;
73287
+ const [counts2 = { added: 0, changed: 0 }] = app.db.all(sql36`
72889
73288
  SELECT ${addedExpression} AS added, ${changedExpression} AS changed
72890
73289
  FROM site_crawl_pages AS current
72891
73290
  LEFT JOIN site_crawl_pages AS previous
@@ -72901,7 +73300,7 @@ async function technicalAeoRoutes(app, opts) {
72901
73300
  summary4.pages.changed = Number(counts2.changed);
72902
73301
  }
72903
73302
  if (includeRemoved) {
72904
- const [counts2 = { removed: 0 }] = app.db.all(sql35`
73303
+ const [counts2 = { removed: 0 }] = app.db.all(sql36`
72905
73304
  SELECT COUNT(*) AS removed
72906
73305
  FROM site_crawl_pages AS previous
72907
73306
  LEFT JOIN site_crawl_pages AS current
@@ -72919,9 +73318,9 @@ async function technicalAeoRoutes(app, opts) {
72919
73318
  }
72920
73319
  if (scopeFilter !== "pages") {
72921
73320
  if (includeAdded || includeChanged) {
72922
- const addedExpression = includeAdded ? sql35`COALESCE(SUM(CASE WHEN previous.id IS NULL THEN 1 ELSE 0 END), 0)` : sql35`0`;
72923
- const changedExpression = includeChanged ? sql35`COALESCE(SUM(CASE WHEN previous.id IS NOT NULL AND ${LINK_CHANGE_PREDICATE} THEN 1 ELSE 0 END), 0)` : sql35`0`;
72924
- const [counts2 = { added: 0, changed: 0 }] = app.db.all(sql35`
73321
+ const addedExpression = includeAdded ? sql36`COALESCE(SUM(CASE WHEN previous.id IS NULL THEN 1 ELSE 0 END), 0)` : sql36`0`;
73322
+ const changedExpression = includeChanged ? sql36`COALESCE(SUM(CASE WHEN previous.id IS NOT NULL AND ${LINK_CHANGE_PREDICATE} THEN 1 ELSE 0 END), 0)` : sql36`0`;
73323
+ const [counts2 = { added: 0, changed: 0 }] = app.db.all(sql36`
72925
73324
  SELECT ${addedExpression} AS added, ${changedExpression} AS changed
72926
73325
  FROM site_crawl_edges AS current
72927
73326
  LEFT JOIN site_crawl_edges AS previous
@@ -72939,7 +73338,7 @@ async function technicalAeoRoutes(app, opts) {
72939
73338
  summary4.links.changed = Number(counts2.changed);
72940
73339
  }
72941
73340
  if (includeRemoved) {
72942
- const [counts2 = { removed: 0 }] = app.db.all(sql35`
73341
+ const [counts2 = { removed: 0 }] = app.db.all(sql36`
72943
73342
  SELECT COUNT(*) AS removed
72944
73343
  FROM site_crawl_edges AS previous
72945
73344
  LEFT JOIN site_crawl_edges AS current
@@ -72969,48 +73368,48 @@ async function technicalAeoRoutes(app, opts) {
72969
73368
  const includeRemoved = changeFilter === "all" || changeFilter === "removed";
72970
73369
  const table = entity === "page" ? "site_crawl_pages" : "site_crawl_edges";
72971
73370
  const keyColumn = entity === "page" ? "node_key" : "edge_key";
72972
- const internalCurrent = entity === "link" ? sql35`AND current.internal = 1` : sql35``;
72973
- const internalPreviousJoin = entity === "link" ? sql35`AND previous.internal = 1` : sql35``;
72974
- const internalPrevious = entity === "link" ? sql35`AND previous.internal = 1` : sql35``;
72975
- const internalCurrentJoin = entity === "link" ? sql35`AND current.internal = 1` : sql35``;
73371
+ const internalCurrent = entity === "link" ? sql36`AND current.internal = 1` : sql36``;
73372
+ const internalPreviousJoin = entity === "link" ? sql36`AND previous.internal = 1` : sql36``;
73373
+ const internalPrevious = entity === "link" ? sql36`AND previous.internal = 1` : sql36``;
73374
+ const internalCurrentJoin = entity === "link" ? sql36`AND current.internal = 1` : sql36``;
72976
73375
  const difference2 = entity === "page" ? PAGE_CHANGE_PREDICATE : LINK_CHANGE_PREDICATE;
72977
- const afterKind = changeFilter === "added" ? sql35`previous.id IS NULL` : changeFilter === "changed" ? sql35`previous.id IS NOT NULL AND ${difference2}` : sql35`previous.id IS NULL OR (previous.id IS NOT NULL AND ${difference2})`;
72978
- const afterRows = includeAfter ? app.db.all(sql35`
73376
+ const afterKind = changeFilter === "added" ? sql36`previous.id IS NULL` : changeFilter === "changed" ? sql36`previous.id IS NOT NULL AND ${difference2}` : sql36`previous.id IS NULL OR (previous.id IS NOT NULL AND ${difference2})`;
73377
+ const afterRows = includeAfter ? app.db.all(sql36`
72979
73378
  SELECT ${entity} AS entity,
72980
73379
  CASE WHEN previous.id IS NULL THEN 'added' ELSE 'changed' END AS change,
72981
- current.${sql35.raw(keyColumn)} AS key
72982
- FROM ${sql35.raw(table)} AS current
72983
- LEFT JOIN ${sql35.raw(table)} AS previous
73380
+ current.${sql36.raw(keyColumn)} AS key
73381
+ FROM ${sql36.raw(table)} AS current
73382
+ LEFT JOIN ${sql36.raw(table)} AS previous
72984
73383
  ON previous.project_id = ${beforeScope.projectId}
72985
73384
  AND previous.run_id = ${beforeScope.runId}
72986
73385
  AND previous.attempt_id = ${beforeScope.attemptId}
72987
73386
  ${internalPreviousJoin}
72988
- AND previous.${sql35.raw(keyColumn)} = current.${sql35.raw(keyColumn)}
73387
+ AND previous.${sql36.raw(keyColumn)} = current.${sql36.raw(keyColumn)}
72989
73388
  WHERE current.project_id = ${afterScope.projectId}
72990
73389
  AND current.run_id = ${afterScope.runId}
72991
73390
  AND current.attempt_id = ${afterScope.attemptId}
72992
73391
  ${internalCurrent}
72993
- AND current.${sql35.raw(keyColumn)} > ${afterKey}
73392
+ AND current.${sql36.raw(keyColumn)} > ${afterKey}
72994
73393
  AND (${afterKind})
72995
- ORDER BY current.${sql35.raw(keyColumn)} ASC
73394
+ ORDER BY current.${sql36.raw(keyColumn)} ASC
72996
73395
  LIMIT ${wanted2}
72997
73396
  `) : [];
72998
- const removedRows = includeRemoved ? app.db.all(sql35`
72999
- SELECT ${entity} AS entity, 'removed' AS change, previous.${sql35.raw(keyColumn)} AS key
73000
- FROM ${sql35.raw(table)} AS previous
73001
- LEFT JOIN ${sql35.raw(table)} AS current
73397
+ const removedRows = includeRemoved ? app.db.all(sql36`
73398
+ SELECT ${entity} AS entity, 'removed' AS change, previous.${sql36.raw(keyColumn)} AS key
73399
+ FROM ${sql36.raw(table)} AS previous
73400
+ LEFT JOIN ${sql36.raw(table)} AS current
73002
73401
  ON current.project_id = ${afterScope.projectId}
73003
73402
  AND current.run_id = ${afterScope.runId}
73004
73403
  AND current.attempt_id = ${afterScope.attemptId}
73005
73404
  ${internalCurrentJoin}
73006
- AND current.${sql35.raw(keyColumn)} = previous.${sql35.raw(keyColumn)}
73405
+ AND current.${sql36.raw(keyColumn)} = previous.${sql36.raw(keyColumn)}
73007
73406
  WHERE previous.project_id = ${beforeScope.projectId}
73008
73407
  AND previous.run_id = ${beforeScope.runId}
73009
73408
  AND previous.attempt_id = ${beforeScope.attemptId}
73010
73409
  ${internalPrevious}
73011
- AND previous.${sql35.raw(keyColumn)} > ${afterKey}
73410
+ AND previous.${sql36.raw(keyColumn)} > ${afterKey}
73012
73411
  AND current.id IS NULL
73013
- ORDER BY previous.${sql35.raw(keyColumn)} ASC
73412
+ ORDER BY previous.${sql36.raw(keyColumn)} ASC
73014
73413
  LIMIT ${wanted2}
73015
73414
  `) : [];
73016
73415
  const merged = [];
@@ -73040,18 +73439,18 @@ async function technicalAeoRoutes(app, opts) {
73040
73439
  const visibleKeys = keyRows.slice(0, limit);
73041
73440
  const pageKeys = visibleKeys.filter((row) => row.entity === "page").map((row) => row.key);
73042
73441
  const linkKeys = visibleKeys.filter((row) => row.entity === "link").map((row) => row.key);
73043
- const pageRowsFor = (scope) => pageKeys.length === 0 ? [] : app.db.select().from(siteCrawlPages).where(and82(
73044
- eq99(siteCrawlPages.projectId, scope.projectId),
73045
- eq99(siteCrawlPages.runId, scope.runId),
73046
- eq99(siteCrawlPages.attemptId, scope.attemptId),
73047
- inArray43(siteCrawlPages.nodeKey, pageKeys)
73442
+ const pageRowsFor = (scope) => pageKeys.length === 0 ? [] : app.db.select().from(siteCrawlPages).where(and83(
73443
+ eq100(siteCrawlPages.projectId, scope.projectId),
73444
+ eq100(siteCrawlPages.runId, scope.runId),
73445
+ eq100(siteCrawlPages.attemptId, scope.attemptId),
73446
+ inArray44(siteCrawlPages.nodeKey, pageKeys)
73048
73447
  )).all();
73049
- const edgeRowsFor = (scope) => linkKeys.length === 0 ? [] : app.db.select().from(siteCrawlEdges).where(and82(
73050
- eq99(siteCrawlEdges.projectId, scope.projectId),
73051
- eq99(siteCrawlEdges.runId, scope.runId),
73052
- eq99(siteCrawlEdges.attemptId, scope.attemptId),
73053
- eq99(siteCrawlEdges.internal, true),
73054
- inArray43(siteCrawlEdges.edgeKey, linkKeys)
73448
+ const edgeRowsFor = (scope) => linkKeys.length === 0 ? [] : app.db.select().from(siteCrawlEdges).where(and83(
73449
+ eq100(siteCrawlEdges.projectId, scope.projectId),
73450
+ eq100(siteCrawlEdges.runId, scope.runId),
73451
+ eq100(siteCrawlEdges.attemptId, scope.attemptId),
73452
+ eq100(siteCrawlEdges.internal, true),
73453
+ inArray44(siteCrawlEdges.edgeKey, linkKeys)
73055
73454
  )).all();
73056
73455
  const beforePages = new Map(pageRowsFor(beforeScope).map((row) => [row.nodeKey, mapCrawlPage(row)]));
73057
73456
  const afterPages = new Map(pageRowsFor(afterScope).map((row) => [row.nodeKey, mapCrawlPage(row)]));
@@ -73224,16 +73623,16 @@ async function technicalAeoRoutes(app, opts) {
73224
73623
  }
73225
73624
  const inventorySummary = inventorySummaryFor(detailScopeFor(project.id, snapshot));
73226
73625
  const filters = [
73227
- eq99(siteCrawlPages.projectId, project.id),
73228
- eq99(siteCrawlPages.runId, snapshot.runId),
73229
- eq99(siteCrawlPages.attemptId, snapshot.attemptId)
73626
+ eq100(siteCrawlPages.projectId, project.id),
73627
+ eq100(siteCrawlPages.runId, snapshot.runId),
73628
+ eq100(siteCrawlPages.attemptId, snapshot.attemptId)
73230
73629
  ];
73231
- if (request.query.nodeKey) filters.push(eq99(siteCrawlPages.nodeKey, request.query.nodeKey));
73630
+ if (request.query.nodeKey) filters.push(eq100(siteCrawlPages.nodeKey, request.query.nodeKey));
73232
73631
  const inventoryEligible = parseBoolean2(request.query.inventoryEligible);
73233
- if (inventoryEligible != null) filters.push(eq99(siteCrawlPages.inventoryEligible, inventoryEligible));
73234
- if (request.query.fetchState) filters.push(eq99(siteCrawlPages.fetchState, request.query.fetchState));
73235
- if (request.query.indexabilityState) filters.push(eq99(siteCrawlPages.indexabilityState, request.query.indexabilityState));
73236
- if (request.query.auditState) filters.push(eq99(siteCrawlPages.auditState, request.query.auditState));
73632
+ if (inventoryEligible != null) filters.push(eq100(siteCrawlPages.inventoryEligible, inventoryEligible));
73633
+ if (request.query.fetchState) filters.push(eq100(siteCrawlPages.fetchState, request.query.fetchState));
73634
+ if (request.query.indexabilityState) filters.push(eq100(siteCrawlPages.indexabilityState, request.query.indexabilityState));
73635
+ if (request.query.auditState) filters.push(eq100(siteCrawlPages.auditState, request.query.auditState));
73237
73636
  const healthState = parseSiteHealthState(request.query.healthState);
73238
73637
  const limit = parseBoundedLimit(request.query.limit, 100, 200);
73239
73638
  const sort = ["score-desc", "score-asc", "path"].includes(request.query.sort ?? "") ? request.query.sort : "url";
@@ -73251,17 +73650,17 @@ async function technicalAeoRoutes(app, opts) {
73251
73650
  sort
73252
73651
  })).digest("base64url");
73253
73652
  const offset = decodeCrawlPageCursor(request.query.cursor, selectionKey);
73254
- const orderBy = sort === "score-desc" ? [desc47(siteCrawlPages.auditScore), asc21(siteCrawlPages.nodeKey)] : sort === "score-asc" ? [asc21(siteCrawlPages.auditScore), asc21(siteCrawlPages.nodeKey)] : sort === "path" ? [asc21(siteCrawlPages.path), asc21(siteCrawlPages.nodeKey)] : [asc21(siteCrawlPages.url), asc21(siteCrawlPages.nodeKey)];
73653
+ const orderBy = sort === "score-desc" ? [desc48(siteCrawlPages.auditScore), asc22(siteCrawlPages.nodeKey)] : sort === "score-asc" ? [asc22(siteCrawlPages.auditScore), asc22(siteCrawlPages.nodeKey)] : sort === "path" ? [asc22(siteCrawlPages.path), asc22(siteCrawlPages.nodeKey)] : [asc22(siteCrawlPages.url), asc22(siteCrawlPages.nodeKey)];
73255
73654
  let healthStateFilter = null;
73256
73655
  if (healthState) {
73257
- const legacyRow = app.db.select({ id: siteCrawlPages.id }).from(siteCrawlPages).where(and82(
73258
- eq99(siteCrawlPages.projectId, project.id),
73259
- eq99(siteCrawlPages.runId, snapshot.runId),
73260
- eq99(siteCrawlPages.attemptId, snapshot.attemptId),
73656
+ const legacyRow = app.db.select({ id: siteCrawlPages.id }).from(siteCrawlPages).where(and83(
73657
+ eq100(siteCrawlPages.projectId, project.id),
73658
+ eq100(siteCrawlPages.runId, snapshot.runId),
73659
+ eq100(siteCrawlPages.attemptId, snapshot.attemptId),
73261
73660
  isNull17(siteCrawlPages.healthState)
73262
73661
  )).limit(1).get();
73263
73662
  healthStateFilter = legacyRow ? "unavailable-legacy-scan" : "applied";
73264
- if (healthStateFilter === "applied") filters.push(eq99(siteCrawlPages.healthState, healthState));
73663
+ if (healthStateFilter === "applied") filters.push(eq100(siteCrawlPages.healthState, healthState));
73265
73664
  }
73266
73665
  if (healthStateFilter === "unavailable-legacy-scan") {
73267
73666
  return {
@@ -73278,7 +73677,7 @@ async function technicalAeoRoutes(app, opts) {
73278
73677
  pages: []
73279
73678
  };
73280
73679
  }
73281
- const where = and82(...filters);
73680
+ const where = and83(...filters);
73282
73681
  const total = app.db.select({ value: count8() }).from(siteCrawlPages).where(where).get()?.value ?? 0;
73283
73682
  const rows = app.db.select().from(siteCrawlPages).where(where).orderBy(...orderBy).limit(limit).offset(offset).all();
73284
73683
  const nextOffset = offset + rows.length;
@@ -73313,10 +73712,10 @@ async function technicalAeoRoutes(app, opts) {
73313
73712
  url: siteCrawlPages.url,
73314
73713
  inventoryEligible: siteCrawlPages.inventoryEligible,
73315
73714
  fetchState: siteCrawlPages.fetchState
73316
- }).from(siteCrawlPages).where(and82(
73317
- eq99(siteCrawlPages.projectId, project.id),
73318
- eq99(siteCrawlPages.runId, snapshot.runId),
73319
- eq99(siteCrawlPages.attemptId, snapshot.attemptId)
73715
+ }).from(siteCrawlPages).where(and83(
73716
+ eq100(siteCrawlPages.projectId, project.id),
73717
+ eq100(siteCrawlPages.runId, snapshot.runId),
73718
+ eq100(siteCrawlPages.attemptId, snapshot.attemptId)
73320
73719
  )).limit(MAX_STRUCTURE_SOURCE_ROWS + 1).all();
73321
73720
  if (sourceRows.length > MAX_STRUCTURE_SOURCE_ROWS) {
73322
73721
  throw validationError(`Persisted crawl exceeds the ${MAX_STRUCTURE_SOURCE_ROWS}-page structure limit`);
@@ -73398,21 +73797,21 @@ async function technicalAeoRoutes(app, opts) {
73398
73797
  };
73399
73798
  }
73400
73799
  const filters = [
73401
- eq99(siteCrawlEdges.projectId, project.id),
73402
- eq99(siteCrawlEdges.runId, snapshot.runId),
73403
- eq99(siteCrawlEdges.attemptId, snapshot.attemptId),
73404
- eq99(siteCrawlEdges.internal, true),
73800
+ eq100(siteCrawlEdges.projectId, project.id),
73801
+ eq100(siteCrawlEdges.runId, snapshot.runId),
73802
+ eq100(siteCrawlEdges.attemptId, snapshot.attemptId),
73803
+ eq100(siteCrawlEdges.internal, true),
73405
73804
  linkKindFilter(linkKind)
73406
73805
  ];
73407
- if (request.query.sourceUrl) filters.push(eq99(siteCrawlEdges.sourceUrl, request.query.sourceUrl));
73408
- if (request.query.targetUrl) filters.push(eq99(siteCrawlEdges.targetUrl, request.query.targetUrl));
73806
+ if (request.query.sourceUrl) filters.push(eq100(siteCrawlEdges.sourceUrl, request.query.sourceUrl));
73807
+ if (request.query.targetUrl) filters.push(eq100(siteCrawlEdges.targetUrl, request.query.targetUrl));
73409
73808
  const followable = parseBoolean2(request.query.followable);
73410
- if (followable != null) filters.push(eq99(siteCrawlEdges.followable, followable));
73411
- const where = and82(...filters);
73809
+ if (followable != null) filters.push(eq100(siteCrawlEdges.followable, followable));
73810
+ const where = and83(...filters);
73412
73811
  const total = app.db.select({ value: count8() }).from(siteCrawlEdges).where(where).get()?.value ?? 0;
73413
73812
  const limit = parseBoundedLimit(request.query.limit, 100, 200);
73414
73813
  const offset = decodeCursor(request.query.cursor);
73415
- const rows = app.db.select().from(siteCrawlEdges).where(where).orderBy(asc21(siteCrawlEdges.edgeKey)).limit(limit).offset(offset).all();
73814
+ const rows = app.db.select().from(siteCrawlEdges).where(where).orderBy(asc22(siteCrawlEdges.edgeKey)).limit(limit).offset(offset).all();
73416
73815
  const nextOffset = offset + rows.length;
73417
73816
  return {
73418
73817
  project: project.name,
@@ -73469,17 +73868,17 @@ async function technicalAeoRoutes(app, opts) {
73469
73868
  };
73470
73869
  }
73471
73870
  const scope = [
73472
- eq99(siteCrawlEdges.projectId, project.id),
73473
- eq99(siteCrawlEdges.runId, snapshot.runId),
73474
- eq99(siteCrawlEdges.attemptId, snapshot.attemptId),
73475
- eq99(siteCrawlEdges.internal, true),
73871
+ eq100(siteCrawlEdges.projectId, project.id),
73872
+ eq100(siteCrawlEdges.runId, snapshot.runId),
73873
+ eq100(siteCrawlEdges.attemptId, snapshot.attemptId),
73874
+ eq100(siteCrawlEdges.internal, true),
73476
73875
  linkKindFilter(linkKind)
73477
73876
  ];
73478
- const inboundMatch = request.query.nodeKey && request.query.url ? or21(eq99(siteCrawlEdges.targetNodeKey, request.query.nodeKey), eq99(siteCrawlEdges.targetUrl, request.query.url)) : request.query.nodeKey ? eq99(siteCrawlEdges.targetNodeKey, request.query.nodeKey) : eq99(siteCrawlEdges.targetUrl, request.query.url);
73479
- const outboundMatch = request.query.nodeKey && request.query.url ? or21(eq99(siteCrawlEdges.sourceNodeKey, request.query.nodeKey), eq99(siteCrawlEdges.sourceUrl, request.query.url)) : request.query.nodeKey ? eq99(siteCrawlEdges.sourceNodeKey, request.query.nodeKey) : eq99(siteCrawlEdges.sourceUrl, request.query.url);
73877
+ const inboundMatch = request.query.nodeKey && request.query.url ? or21(eq100(siteCrawlEdges.targetNodeKey, request.query.nodeKey), eq100(siteCrawlEdges.targetUrl, request.query.url)) : request.query.nodeKey ? eq100(siteCrawlEdges.targetNodeKey, request.query.nodeKey) : eq100(siteCrawlEdges.targetUrl, request.query.url);
73878
+ const outboundMatch = request.query.nodeKey && request.query.url ? or21(eq100(siteCrawlEdges.sourceNodeKey, request.query.nodeKey), eq100(siteCrawlEdges.sourceUrl, request.query.url)) : request.query.nodeKey ? eq100(siteCrawlEdges.sourceNodeKey, request.query.nodeKey) : eq100(siteCrawlEdges.sourceUrl, request.query.url);
73480
73879
  const limit = parseBoundedLimit(request.query.limit, 50, 100);
73481
- const inboundRows = app.db.select().from(siteCrawlEdges).where(and82(...scope, inboundMatch)).orderBy(asc21(siteCrawlEdges.edgeKey)).limit(limit + 1).all();
73482
- const outboundRows = app.db.select().from(siteCrawlEdges).where(and82(...scope, outboundMatch)).orderBy(asc21(siteCrawlEdges.edgeKey)).limit(limit + 1).all();
73880
+ const inboundRows = app.db.select().from(siteCrawlEdges).where(and83(...scope, inboundMatch)).orderBy(asc22(siteCrawlEdges.edgeKey)).limit(limit + 1).all();
73881
+ const outboundRows = app.db.select().from(siteCrawlEdges).where(and83(...scope, outboundMatch)).orderBy(asc22(siteCrawlEdges.edgeKey)).limit(limit + 1).all();
73483
73882
  return {
73484
73883
  project: project.name,
73485
73884
  hasCrawlData: true,
@@ -73513,11 +73912,11 @@ async function technicalAeoRoutes(app, opts) {
73513
73912
  if (!snapshot.attemptId) {
73514
73913
  return { project: project.name, runId: snapshot.runId, state: "partial", checkDeadLinks: true, checked: snapshot.deadLinksChecked, found: snapshot.deadLinksFound, unverified: snapshot.deadLinksUnverified, total: 0, nextCursor: null, deadLinks: [], runSelection: target.runSelection };
73515
73914
  }
73516
- const where = and82(
73517
- eq99(siteCrawlFindings.projectId, project.id),
73518
- eq99(siteCrawlFindings.runId, snapshot.runId),
73519
- eq99(siteCrawlFindings.attemptId, snapshot.attemptId),
73520
- eq99(siteCrawlFindings.findingType, "dead-link"),
73915
+ const where = and83(
73916
+ eq100(siteCrawlFindings.projectId, project.id),
73917
+ eq100(siteCrawlFindings.runId, snapshot.runId),
73918
+ eq100(siteCrawlFindings.attemptId, snapshot.attemptId),
73919
+ eq100(siteCrawlFindings.findingType, "dead-link"),
73521
73920
  // Belt and braces on the client-facing surface: a row without a status
73522
73921
  // code is not evidence of a broken link, and this response is where such
73523
73922
  // a row would be read as one. Migration 140 removed every stored row that
@@ -73527,12 +73926,12 @@ async function technicalAeoRoutes(app, opts) {
73527
73926
  // a fabricated broken link shown to a client is not worth the saved
73528
73927
  // predicate. Applied to `total` as well as the rows, or the count would
73529
73928
  // promise a page the list cannot fill.
73530
- isNotNull10(sql35`json_extract(${siteCrawlFindings.evidence}, '$.statusCode')`)
73929
+ isNotNull10(sql36`json_extract(${siteCrawlFindings.evidence}, '$.statusCode')`)
73531
73930
  );
73532
73931
  const total = app.db.select({ value: count8() }).from(siteCrawlFindings).where(where).get()?.value ?? 0;
73533
73932
  const limit = parseBoundedLimit(request.query.limit, 100, 200);
73534
73933
  const offset = decodeCursor(request.query.cursor);
73535
- const rows = app.db.select().from(siteCrawlFindings).where(where).orderBy(asc21(siteCrawlFindings.findingKey)).limit(limit).offset(offset).all();
73934
+ const rows = app.db.select().from(siteCrawlFindings).where(where).orderBy(asc22(siteCrawlFindings.findingKey)).limit(limit).offset(offset).all();
73536
73935
  const nextOffset = offset + rows.length;
73537
73936
  const deadLinks = rows.map((row) => ({
73538
73937
  findingKey: row.findingKey,
@@ -73567,19 +73966,19 @@ async function technicalAeoRoutes(app, opts) {
73567
73966
  createdAt: runs.createdAt,
73568
73967
  startedAt: runs.startedAt,
73569
73968
  finishedAt: runs.finishedAt
73570
- }).from(runs).where(and82(
73571
- eq99(runs.projectId, project.id),
73572
- eq99(runs.kind, RunKinds["site-audit"]),
73969
+ }).from(runs).where(and83(
73970
+ eq100(runs.projectId, project.id),
73971
+ eq100(runs.kind, RunKinds["site-audit"]),
73573
73972
  notProbeRun()
73574
- )).orderBy(desc47(runs.createdAt), desc47(runs.id)).limit(limit).all();
73973
+ )).orderBy(desc48(runs.createdAt), desc48(runs.id)).limit(limit).all();
73575
73974
  const runIds = rows.map((row) => row.runId);
73576
- const crawlRunIds = new Set(runIds.length === 0 ? [] : app.db.select({ runId: siteCrawlSnapshots.runId }).from(siteCrawlSnapshots).innerJoin(runs, eq99(siteCrawlSnapshots.runId, runs.id)).where(and82(
73577
- eq99(siteCrawlSnapshots.projectId, project.id),
73578
- eq99(runs.projectId, project.id),
73579
- eq99(runs.kind, RunKinds["site-audit"]),
73580
- inArray43(runs.status, SURFACEABLE_STATUSES),
73975
+ const crawlRunIds = new Set(runIds.length === 0 ? [] : app.db.select({ runId: siteCrawlSnapshots.runId }).from(siteCrawlSnapshots).innerJoin(runs, eq100(siteCrawlSnapshots.runId, runs.id)).where(and83(
73976
+ eq100(siteCrawlSnapshots.projectId, project.id),
73977
+ eq100(runs.projectId, project.id),
73978
+ eq100(runs.kind, RunKinds["site-audit"]),
73979
+ inArray44(runs.status, SURFACEABLE_STATUSES),
73581
73980
  notProbeRun(),
73582
- inArray43(siteCrawlSnapshots.runId, runIds)
73981
+ inArray44(siteCrawlSnapshots.runId, runIds)
73583
73982
  )).all().map((row) => row.runId));
73584
73983
  return {
73585
73984
  project: project.name,
@@ -73593,21 +73992,21 @@ async function technicalAeoRoutes(app, opts) {
73593
73992
  });
73594
73993
  app.get("/projects/:name/technical-aeo/runs/:runId/progress", async (request) => {
73595
73994
  const project = resolveProject(app.db, request.params.name);
73596
- const run = app.db.select().from(runs).where(and82(
73597
- eq99(runs.id, request.params.runId),
73598
- eq99(runs.projectId, project.id),
73599
- eq99(runs.kind, RunKinds["site-audit"]),
73995
+ const run = app.db.select().from(runs).where(and83(
73996
+ eq100(runs.id, request.params.runId),
73997
+ eq100(runs.projectId, project.id),
73998
+ eq100(runs.kind, RunKinds["site-audit"]),
73600
73999
  notProbeRun()
73601
74000
  )).get();
73602
74001
  if (!run) throw notFound("Site audit run", request.params.runId);
73603
- const attempt = app.db.select().from(siteCrawlAttempts).where(and82(
73604
- eq99(siteCrawlAttempts.projectId, project.id),
73605
- eq99(siteCrawlAttempts.runId, run.id)
73606
- )).orderBy(desc47(siteCrawlAttempts.attemptNumber), desc47(siteCrawlAttempts.updatedAt)).limit(1).get();
73607
- const persistedLayout = attempt ? app.db.select().from(siteCrawlGraphLayouts).where(and82(
73608
- eq99(siteCrawlGraphLayouts.projectId, project.id),
73609
- eq99(siteCrawlGraphLayouts.runId, run.id),
73610
- eq99(siteCrawlGraphLayouts.attemptId, attempt.id)
74002
+ const attempt = app.db.select().from(siteCrawlAttempts).where(and83(
74003
+ eq100(siteCrawlAttempts.projectId, project.id),
74004
+ eq100(siteCrawlAttempts.runId, run.id)
74005
+ )).orderBy(desc48(siteCrawlAttempts.attemptNumber), desc48(siteCrawlAttempts.updatedAt)).limit(1).get();
74006
+ const persistedLayout = attempt ? app.db.select().from(siteCrawlGraphLayouts).where(and83(
74007
+ eq100(siteCrawlGraphLayouts.projectId, project.id),
74008
+ eq100(siteCrawlGraphLayouts.runId, run.id),
74009
+ eq100(siteCrawlGraphLayouts.attemptId, attempt.id)
73611
74010
  )).get() : void 0;
73612
74011
  const layoutState = persistedLayout?.state === "ready" || persistedLayout?.state === "unavailable" ? persistedLayout.state : run.status === RunStatuses.completed || run.status === RunStatuses.partial ? "unavailable" : "pending";
73613
74012
  return {
@@ -73638,19 +74037,19 @@ async function technicalAeoRoutes(app, opts) {
73638
74037
  };
73639
74038
  });
73640
74039
  app.get("/projects/:name/technical-aeo/runs/:runId/page-health-preview", async (request) => app.db.transaction((tx) => {
73641
- const project = tx.select().from(projects).where(eq99(projects.name, request.params.name)).get();
74040
+ const project = tx.select().from(projects).where(eq100(projects.name, request.params.name)).get();
73642
74041
  if (!project) throw notFound("Project", request.params.name);
73643
- const run = tx.select().from(runs).where(and82(
73644
- eq99(runs.id, request.params.runId),
73645
- eq99(runs.projectId, project.id),
73646
- eq99(runs.kind, RunKinds["site-audit"]),
74042
+ const run = tx.select().from(runs).where(and83(
74043
+ eq100(runs.id, request.params.runId),
74044
+ eq100(runs.projectId, project.id),
74045
+ eq100(runs.kind, RunKinds["site-audit"]),
73647
74046
  notProbeRun()
73648
74047
  )).get();
73649
74048
  if (!run) throw notFound("Site audit run", request.params.runId);
73650
- const attempt = tx.select().from(siteCrawlAttempts).where(and82(
73651
- eq99(siteCrawlAttempts.projectId, project.id),
73652
- eq99(siteCrawlAttempts.runId, run.id)
73653
- )).orderBy(desc47(siteCrawlAttempts.attemptNumber), desc47(siteCrawlAttempts.updatedAt)).limit(1).get();
74049
+ const attempt = tx.select().from(siteCrawlAttempts).where(and83(
74050
+ eq100(siteCrawlAttempts.projectId, project.id),
74051
+ eq100(siteCrawlAttempts.runId, run.id)
74052
+ )).orderBy(desc48(siteCrawlAttempts.attemptNumber), desc48(siteCrawlAttempts.updatedAt)).limit(1).get();
73654
74053
  const state = run.status === RunStatuses.queued ? "waiting" : run.status === RunStatuses.running ? "collecting" : "terminal";
73655
74054
  const base = {
73656
74055
  project: project.name,
@@ -73663,11 +74062,11 @@ async function technicalAeoRoutes(app, opts) {
73663
74062
  if (!attempt) {
73664
74063
  return { ...base, pagesAudited: 0, examples: [] };
73665
74064
  }
73666
- const auditedWhere = and82(
73667
- eq99(siteCrawlPages.projectId, project.id),
73668
- eq99(siteCrawlPages.runId, run.id),
73669
- eq99(siteCrawlPages.attemptId, attempt.id),
73670
- eq99(siteCrawlPages.auditState, "success")
74065
+ const auditedWhere = and83(
74066
+ eq100(siteCrawlPages.projectId, project.id),
74067
+ eq100(siteCrawlPages.runId, run.id),
74068
+ eq100(siteCrawlPages.attemptId, attempt.id),
74069
+ eq100(siteCrawlPages.auditState, "success")
73671
74070
  );
73672
74071
  const pagesAudited = tx.select({ value: count8() }).from(siteCrawlPages).where(auditedWhere).get()?.value ?? 0;
73673
74072
  if (state !== "collecting") {
@@ -73678,11 +74077,11 @@ async function technicalAeoRoutes(app, opts) {
73678
74077
  url: siteCrawlPages.url,
73679
74078
  auditScore: siteCrawlPages.auditScore,
73680
74079
  auditFields: siteCrawlPages.auditFields
73681
- }).from(siteCrawlPages).where(and82(
74080
+ }).from(siteCrawlPages).where(and83(
73682
74081
  auditedWhere,
73683
74082
  isNotNull10(siteCrawlPages.auditScore),
73684
74083
  lt14(siteCrawlPages.auditScore, 70)
73685
- )).orderBy(asc21(siteCrawlPages.auditScore), asc21(siteCrawlPages.nodeKey)).limit(LIVE_PAGE_HEALTH_CANDIDATE_LIMIT).all();
74084
+ )).orderBy(asc22(siteCrawlPages.auditScore), asc22(siteCrawlPages.nodeKey)).limit(LIVE_PAGE_HEALTH_CANDIDATE_LIMIT).all();
73686
74085
  const examples = candidates.flatMap((candidate) => {
73687
74086
  if (candidate.auditScore == null) return [];
73688
74087
  const checksNeedingAttention = countLivePageHealthChecks(candidate.auditFields);
@@ -73714,13 +74113,13 @@ async function technicalAeoRoutes(app, opts) {
73714
74113
  status: runs.status,
73715
74114
  identityKey: siteCrawlRunRequests.identityKey,
73716
74115
  effectiveOptions: siteCrawlRunRequests.effectiveOptions
73717
- }).from(runs).leftJoin(siteCrawlRunRequests, and82(
73718
- eq99(siteCrawlRunRequests.projectId, runs.projectId),
73719
- eq99(siteCrawlRunRequests.runId, runs.id)
73720
- )).where(and82(
73721
- eq99(runs.projectId, project.id),
73722
- eq99(runs.kind, RunKinds["site-audit"]),
73723
- inArray43(runs.status, [RunStatuses.queued, RunStatuses.running])
74116
+ }).from(runs).leftJoin(siteCrawlRunRequests, and83(
74117
+ eq100(siteCrawlRunRequests.projectId, runs.projectId),
74118
+ eq100(siteCrawlRunRequests.runId, runs.id)
74119
+ )).where(and83(
74120
+ eq100(runs.projectId, project.id),
74121
+ eq100(runs.kind, RunKinds["site-audit"]),
74122
+ inArray44(runs.status, [RunStatuses.queued, RunStatuses.running])
73724
74123
  )).get();
73725
74124
  if (existing) {
73726
74125
  if (existing.identityKey === identityKey) {
@@ -73776,7 +74175,7 @@ async function technicalAeoRoutes(app, opts) {
73776
74175
  // ../api-routes/src/research.ts
73777
74176
  import crypto57 from "crypto";
73778
74177
  import { z as z7 } from "zod";
73779
- import { and as and83, count as count9, desc as desc48, eq as eq100, gte as gte28, lt as lt15, or as or22, sql as sql36 } from "drizzle-orm";
74178
+ import { and as and84, count as count9, desc as desc49, eq as eq101, gte as gte28, lt as lt15, or as or22, sql as sql37 } from "drizzle-orm";
73780
74179
  var sameLocation = (a, b) => a.label === b.label && a.city === b.city && a.region === b.region && a.country === b.country && a.timezone === b.timezone;
73781
74180
  var BATCH_RECEIPT_PREFIX = "__canonry_research_batch__:";
73782
74181
  async function researchRoutes(app, opts) {
@@ -73795,7 +74194,7 @@ async function researchRoutes(app, opts) {
73795
74194
  const input = parsed.data;
73796
74195
  if (input.idempotencyKey?.startsWith(BATCH_RECEIPT_PREFIX)) throw validationError("This idempotency key prefix is reserved for internal research batch receipts.");
73797
74196
  const requestHash2 = directResearchRequestHash(input);
73798
- const existingReceipt = input.idempotencyKey ? app.db.select().from(researchRuns).where(and83(eq100(researchRuns.projectId, project.id), eq100(researchRuns.idempotencyKey, input.idempotencyKey))).get() : void 0;
74197
+ const existingReceipt = input.idempotencyKey ? app.db.select().from(researchRuns).where(and84(eq101(researchRuns.projectId, project.id), eq101(researchRuns.idempotencyKey, input.idempotencyKey))).get() : void 0;
73799
74198
  if (existingReceipt) {
73800
74199
  assertSameDirectResearchRequest(existingReceipt, input, requestHash2);
73801
74200
  const result2 = getDetail(app, project.id, existingReceipt.id);
@@ -73830,7 +74229,7 @@ async function researchRoutes(app, opts) {
73830
74229
  const initiatedBy = researchPrincipal(request);
73831
74230
  const decision = app.db.transaction((tx) => {
73832
74231
  if (input.idempotencyKey) {
73833
- const existing = tx.select().from(researchRuns).where(and83(eq100(researchRuns.projectId, project.id), eq100(researchRuns.idempotencyKey, input.idempotencyKey))).get();
74232
+ const existing = tx.select().from(researchRuns).where(and84(eq101(researchRuns.projectId, project.id), eq101(researchRuns.idempotencyKey, input.idempotencyKey))).get();
73834
74233
  if (existing) {
73835
74234
  assertSameDirectResearchRequest(existing, input, requestHash2);
73836
74235
  return { reused: true, id: existing.id, shouldDispatch: existing.status === ResearchRunStatuses.queued };
@@ -73838,11 +74237,11 @@ async function researchRoutes(app, opts) {
73838
74237
  }
73839
74238
  if (initiatedBy?.limited) {
73840
74239
  const { start, end, date } = utcDayBounds(now);
73841
- const used = tx.select({ value: count9() }).from(researchRuns).where(and83(
73842
- eq100(researchRuns.projectId, project.id),
74240
+ const used = tx.select({ value: count9() }).from(researchRuns).where(and84(
74241
+ eq101(researchRuns.projectId, project.id),
73843
74242
  gte28(researchRuns.createdAt, start),
73844
74243
  lt15(researchRuns.createdAt, end),
73845
- sql36`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
74244
+ sql37`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
73846
74245
  )).get()?.value ?? 0;
73847
74246
  if (used >= viewerDailyRunLimit) {
73848
74247
  throw researchDailyLimitExceeded(project.name, viewerDailyRunLimit, date);
@@ -73870,15 +74269,15 @@ async function researchRoutes(app, opts) {
73870
74269
  if (input.idempotencyKey.startsWith(BATCH_RECEIPT_PREFIX)) throw validationError("This idempotency key prefix is reserved for internal research batch receipts.");
73871
74270
  const requestHash2 = researchBatchRequestHash(input);
73872
74271
  const receiptKeys = input.runs.map((_run, index) => batchChildReceiptKey(input.idempotencyKey, index));
73873
- const firstReceipt = app.db.select().from(researchRuns).where(and83(
73874
- eq100(researchRuns.projectId, project.id),
73875
- eq100(researchRuns.idempotencyKey, receiptKeys[0])
74272
+ const firstReceipt = app.db.select().from(researchRuns).where(and84(
74273
+ eq101(researchRuns.projectId, project.id),
74274
+ eq101(researchRuns.idempotencyKey, receiptKeys[0])
73876
74275
  )).get();
73877
74276
  if (firstReceipt) {
73878
74277
  if (firstReceipt.requestHash !== requestHash2) throw alreadyExists("Research batch idempotency key", input.idempotencyKey);
73879
- const saved = receiptKeys.map((key) => app.db.select().from(researchRuns).where(and83(
73880
- eq100(researchRuns.projectId, project.id),
73881
- eq100(researchRuns.idempotencyKey, key)
74278
+ const saved = receiptKeys.map((key) => app.db.select().from(researchRuns).where(and84(
74279
+ eq101(researchRuns.projectId, project.id),
74280
+ eq101(researchRuns.idempotencyKey, key)
73882
74281
  )).get());
73883
74282
  if (saved.some((row) => !row || row.requestHash !== requestHash2)) throw alreadyExists("Research batch idempotency key", input.idempotencyKey);
73884
74283
  const runs3 = saved.map((row) => getDetail(app, project.id, row.id));
@@ -73894,11 +74293,11 @@ async function researchRoutes(app, opts) {
73894
74293
  const decision = app.db.transaction((tx) => {
73895
74294
  if (initiatedBy?.limited) {
73896
74295
  const { start, end, date } = utcDayBounds(now);
73897
- const used = tx.select({ value: count9() }).from(researchRuns).where(and83(
73898
- eq100(researchRuns.projectId, project.id),
74296
+ const used = tx.select({ value: count9() }).from(researchRuns).where(and84(
74297
+ eq101(researchRuns.projectId, project.id),
73899
74298
  gte28(researchRuns.createdAt, start),
73900
74299
  lt15(researchRuns.createdAt, end),
73901
- sql36`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
74300
+ sql37`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
73902
74301
  )).get()?.value ?? 0;
73903
74302
  if (used + prepared.length > viewerDailyRunLimit) throw researchDailyLimitExceeded(project.name, viewerDailyRunLimit, date);
73904
74303
  }
@@ -73949,10 +74348,10 @@ async function researchRoutes(app, opts) {
73949
74348
  const requested = Number.parseInt(request.query.limit ?? "", 10);
73950
74349
  const limit = Number.isInteger(requested) && requested > 0 ? Math.min(requested, 100) : 20;
73951
74350
  const cursor = request.query.cursor ? parseResearchCursor(request.query.cursor, project.id) : null;
73952
- const rows = app.db.select().from(researchRuns).where(and83(
73953
- eq100(researchRuns.projectId, project.id),
73954
- cursor ? or22(lt15(researchRuns.createdAt, cursor.createdAt), and83(eq100(researchRuns.createdAt, cursor.createdAt), lt15(researchRuns.id, cursor.id))) : void 0
73955
- )).orderBy(desc48(researchRuns.createdAt), desc48(researchRuns.id)).limit(limit + 1).all();
74351
+ const rows = app.db.select().from(researchRuns).where(and84(
74352
+ eq101(researchRuns.projectId, project.id),
74353
+ cursor ? or22(lt15(researchRuns.createdAt, cursor.createdAt), and84(eq101(researchRuns.createdAt, cursor.createdAt), lt15(researchRuns.id, cursor.id))) : void 0
74354
+ )).orderBy(desc49(researchRuns.createdAt), desc49(researchRuns.id)).limit(limit + 1).all();
73956
74355
  const runs2 = rows.slice(0, limit).map(serializeRun);
73957
74356
  const last = runs2.at(-1);
73958
74357
  const nextCursor = rows.length > limit && last ? Buffer.from(JSON.stringify({ projectId: project.id, createdAt: last.createdAt, id: last.id })).toString("base64url") : null;
@@ -74101,9 +74500,9 @@ function resolveStoredResearchTemplate(stored, selection, idempotencyKey) {
74101
74500
  return stored;
74102
74501
  }
74103
74502
  function resolveResearchTemplate(app, projectId, selection, scope, location) {
74104
- const savedTemplate = app.db.select().from(measurementQueryTemplates).where(and83(
74105
- eq100(measurementQueryTemplates.projectId, projectId),
74106
- eq100(measurementQueryTemplates.id, selection.templateId)
74503
+ const savedTemplate = app.db.select().from(measurementQueryTemplates).where(and84(
74504
+ eq101(measurementQueryTemplates.projectId, projectId),
74505
+ eq101(measurementQueryTemplates.id, selection.templateId)
74107
74506
  )).get();
74108
74507
  if (!savedTemplate) throw validationError("Unknown or stale research template. Reload the selected template and try again.");
74109
74508
  const templateVersion = savedTemplate.updatedAt;
@@ -74112,7 +74511,7 @@ function resolveResearchTemplate(app, projectId, selection, scope, location) {
74112
74511
  }
74113
74512
  const bindingLocation = selection.bindingLocation === void 0 ? location : selection.bindingLocation;
74114
74513
  if (bindingLocation) {
74115
- const project = app.db.select({ locations: projects.locations }).from(projects).where(eq100(projects.id, projectId)).get();
74514
+ const project = app.db.select({ locations: projects.locations }).from(projects).where(eq101(projects.id, projectId)).get();
74116
74515
  if (!project.locations.some((item2) => sameLocation(item2, bindingLocation))) throw validationError("Research template binding location must match a configured project location.");
74117
74516
  }
74118
74517
  const { bindings, output } = expandResearchTemplate(savedTemplate, scope, bindingLocation);
@@ -74125,9 +74524,9 @@ function classifyResearchQueries(project, plan, queries2) {
74125
74524
  return queries2.map((query) => classifier?.classify(query) ?? null);
74126
74525
  }
74127
74526
  function getDetail(app, projectId, id) {
74128
- const row = app.db.select().from(researchRuns).where(and83(eq100(researchRuns.id, id), eq100(researchRuns.projectId, projectId))).get();
74527
+ const row = app.db.select().from(researchRuns).where(and84(eq101(researchRuns.id, id), eq101(researchRuns.projectId, projectId))).get();
74129
74528
  if (!row) throw notFound("Research run", id);
74130
- const queries2 = app.db.select().from(researchRunQueries).where(eq100(researchRunQueries.researchRunId, id)).orderBy(researchRunQueries.position).all().map(serializeQuery);
74529
+ const queries2 = app.db.select().from(researchRunQueries).where(eq101(researchRunQueries.researchRunId, id)).orderBy(researchRunQueries.position).all().map(serializeQuery);
74131
74530
  return { ...serializeRun(row), queries: queries2 };
74132
74531
  }
74133
74532
  function serializeRun(row) {
@@ -74182,7 +74581,7 @@ function utcDayBounds(now) {
74182
74581
 
74183
74582
  // ../api-routes/src/simple-measurement-definitions.ts
74184
74583
  import crypto58 from "crypto";
74185
- import { and as and84, eq as eq101 } from "drizzle-orm";
74584
+ import { and as and85, eq as eq102 } from "drizzle-orm";
74186
74585
  function captureSimpleMeasurementDefinition(db, input) {
74187
74586
  return db.transaction((tx) => {
74188
74587
  const run = tx.select({
@@ -74190,7 +74589,7 @@ function captureSimpleMeasurementDefinition(db, input) {
74190
74589
  trigger: runs.trigger,
74191
74590
  status: runs.status,
74192
74591
  measurementPlanVersionId: runs.measurementPlanVersionId
74193
- }).from(runs).where(and84(eq101(runs.projectId, input.projectId), eq101(runs.id, input.runId))).get();
74592
+ }).from(runs).where(and85(eq102(runs.projectId, input.projectId), eq102(runs.id, input.runId))).get();
74194
74593
  if (!run) throw notFound("Run", input.runId);
74195
74594
  if (run.kind !== RunKinds["answer-visibility"] || run.trigger === RunTriggers.probe || run.measurementPlanVersionId !== null) {
74196
74595
  return null;
@@ -74211,7 +74610,7 @@ function captureSimpleMeasurementDefinition(db, input) {
74211
74610
  throw validationError("Qualified aliases must be a sorted subset of the captured aliases.");
74212
74611
  }
74213
74612
  }
74214
- const existing = tx.select().from(simpleMeasurementDefinitions).where(and84(eq101(simpleMeasurementDefinitions.projectId, input.projectId), eq101(simpleMeasurementDefinitions.runId, input.runId))).get();
74613
+ const existing = tx.select().from(simpleMeasurementDefinitions).where(and85(eq102(simpleMeasurementDefinitions.projectId, input.projectId), eq102(simpleMeasurementDefinitions.runId, input.runId))).get();
74215
74614
  if (existing) {
74216
74615
  const frozen = simpleMeasurementDefinitionSchema.parse(existing.definition);
74217
74616
  const { qualifiedAliases: _liveQualifiedAliases, ...liveIdentity } = definition2.identity;
@@ -74237,15 +74636,15 @@ function captureSimpleMeasurementDefinition(db, input) {
74237
74636
  if (run.status !== RunStatuses.running) {
74238
74637
  throw validationError("Only a running run can capture a new measurement definition.");
74239
74638
  }
74240
- if (tx.select({ id: querySnapshots.id }).from(querySnapshots).where(eq101(querySnapshots.runId, input.runId)).limit(1).get()) {
74639
+ if (tx.select({ id: querySnapshots.id }).from(querySnapshots).where(eq102(querySnapshots.runId, input.runId)).limit(1).get()) {
74241
74640
  throw validationError("This run already has stored answers. Its measurement definition cannot be inferred afterward.");
74242
74641
  }
74243
- const projectQueryIds = new Set(tx.select({ id: queries.id }).from(queries).where(eq101(queries.projectId, input.projectId)).all().map((query) => query.id));
74642
+ const projectQueryIds = new Set(tx.select({ id: queries.id }).from(queries).where(eq102(queries.projectId, input.projectId)).all().map((query) => query.id));
74244
74643
  if (definition2.queries.some((query) => !projectQueryIds.has(query.queryId))) {
74245
74644
  throw validationError("Every captured query must belong to the run project.");
74246
74645
  }
74247
74646
  if (definition2.competitors !== void 0) {
74248
- const liveCompetitorDomains = new Set(tx.select({ domain: competitors.domain }).from(competitors).where(eq101(competitors.projectId, input.projectId)).orderBy(competitors.domain).all().map((competitor) => competitor.domain));
74647
+ const liveCompetitorDomains = new Set(tx.select({ domain: competitors.domain }).from(competitors).where(eq102(competitors.projectId, input.projectId)).orderBy(competitors.domain).all().map((competitor) => competitor.domain));
74249
74648
  const capturedDomains = new Set(definition2.competitors.map((competitor) => competitor.domain));
74250
74649
  if (liveCompetitorDomains.size !== capturedDomains.size || [...liveCompetitorDomains].some((domain) => !capturedDomains.has(domain))) {
74251
74650
  throw validationError("Captured competitors must exactly match the project competitors dispatched for this run.");