@canonry/canonry 7.65.0 → 7.66.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 (137) 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 +1 -1
  3. package/assets/assets/{AgentSetupPage-DPHRo4OR.js → AgentSetupPage-DXFxOJ1W.js} +1 -1
  4. package/assets/assets/{AuditHistoryPanel-qhIB8KRK.js → AuditHistoryPanel-CS8UfTHc.js} +1 -1
  5. package/assets/assets/{BacklinksPage-iWrZ0-Pp.js → BacklinksPage-D5IA2tNY.js} +1 -1
  6. package/assets/assets/{HistoryPage-BP1_GGlT.js → HistoryPage-DsQAUkgR.js} +1 -1
  7. package/assets/assets/MeasurementPropertyPage-CdCnjCaS.js +4 -0
  8. package/assets/assets/ProjectPage-D3oFKnBG.js +9 -0
  9. package/assets/assets/{RunsPage-Cc98Px3T.js → RunsPage-BgjN1N3t.js} +1 -1
  10. package/assets/assets/{SettingsPage-CMyCE-H2.js → SettingsPage-BQuqnEvm.js} +1 -1
  11. package/assets/assets/SiteHealthSection-C6QE9xNk.js +2 -0
  12. package/assets/assets/ToneBadge-DNva5_AM.js +60 -0
  13. package/assets/assets/{TrafficPage-DAef3d2A.js → TrafficPage-BBqpvwl5.js} +2 -2
  14. package/assets/assets/{TrafficSourceDetailPage-CdoL5Dt9.js → TrafficSourceDetailPage-uxd0ojry.js} +1 -1
  15. package/assets/assets/{account-context-CWRd3KeS.js → account-context-wRthfm6K.js} +1 -1
  16. package/assets/assets/{card-Cb_fM36G.js → card-Bq64-UB5.js} +1 -1
  17. package/assets/assets/{extract-error-message-DSIAoHTR.js → extract-error-message-BRDc8As2.js} +1 -1
  18. package/assets/assets/{format-helpers-CxNDa1oa.js → format-helpers-D5wFtgg2.js} +1 -1
  19. package/assets/assets/{index-Da8t1_vu.css → index-C6U4u18w.css} +1 -1
  20. package/assets/assets/index-Ttz5_bL3.js +60 -0
  21. package/assets/assets/{query-client-DLdAMlsX.js → query-client-IopmAOx0.js} +1 -1
  22. package/assets/assets/react-query-CISHjSch.js +1 -0
  23. package/assets/assets/{react-sigma_core.esm.min-DgK5sRqv.js → react-sigma_core.esm.min-d72rX0Uo.js} +1 -1
  24. package/assets/assets/{service-Dv9FDyFY.js → service-DsDxSQuN.js} +1 -1
  25. package/assets/index.html +8 -8
  26. package/dist/{ads-D4WQOT5K.js → ads-IFKZU7SC.js} +3 -3
  27. package/dist/{agent-E3IYC7OJ.js → agent-3KMENHL3.js} +7 -7
  28. package/dist/{agent-ask-ZSKRPB2Z.js → agent-ask-7WVQLLD5.js} +3 -3
  29. package/dist/{agent-conversations-YHPYPQE2.js → agent-conversations-5LR67TBD.js} +3 -3
  30. package/dist/{agent-memory-6OWOOD5O.js → agent-memory-GKUCCKVX.js} +3 -3
  31. package/dist/{agent-providers-7KZK37UO.js → agent-providers-TVCRM3YZ.js} +3 -3
  32. package/dist/{agent-transcript-3O7GPML6.js → agent-transcript-MUTCOOWG.js} +3 -3
  33. package/dist/{analytics-P2LLIUYK.js → analytics-2IXSG5ML.js} +3 -3
  34. package/dist/{apply-VBNNJOCT.js → apply-7RIYK4VC.js} +3 -3
  35. package/dist/{backfill-F5SSURCH.js → backfill-VU3FV6DQ.js} +9 -9
  36. package/dist/{backlinks-4RE3TNNA.js → backlinks-DLSRYEX2.js} +3 -3
  37. package/dist/{bing-V6UN7D44.js → bing-4CXTZUH4.js} +3 -3
  38. package/dist/{bootstrap-SHTSPAI5.js → bootstrap-OOHSBSDQ.js} +10 -10
  39. package/dist/{cdp-GDL2VFI3.js → cdp-Y53SRSRV.js} +7 -7
  40. package/dist/{chunk-ZWZOQNLD.js → chunk-2YBEI6LE.js} +1 -1
  41. package/dist/{chunk-SL3Y7IOX.js → chunk-6ASEI24F.js} +1 -1
  42. package/dist/{chunk-HI2HCYLB.js → chunk-743NYXIK.js} +2 -2
  43. package/dist/{chunk-BV6NXSAH.js → chunk-ALQEGGYM.js} +21 -3
  44. package/dist/{chunk-GQXSZE2Z.js → chunk-AVQQRTMD.js} +146 -39
  45. package/dist/{chunk-Q2WZAD5W.js → chunk-BCRRAH5C.js} +1 -1
  46. package/dist/{chunk-ZPRJCSSR.js → chunk-CKTA5374.js} +1 -1
  47. package/dist/{chunk-GZPGDAAF.js → chunk-DE2UI37M.js} +2 -2
  48. package/dist/{chunk-2Y7RN5HG.js → chunk-DH5QRKFW.js} +3 -3
  49. package/dist/{chunk-CRKOGLKJ.js → chunk-DVYS2RMM.js} +4 -4
  50. package/dist/{chunk-F7NZEUXP.js → chunk-EIMXB6UH.js} +14 -2
  51. package/dist/{chunk-C2PYKUD3.js → chunk-EYWC24VC.js} +1 -1
  52. package/dist/{chunk-2AROR73F.js → chunk-FG7KMVA6.js} +5 -5
  53. package/dist/{chunk-QNRMYDK6.js → chunk-FNI3QSVD.js} +1 -1
  54. package/dist/{chunk-H6HIJAIU.js → chunk-H37CDAZX.js} +2 -2
  55. package/dist/{chunk-XMKXXWMO.js → chunk-IHOVPHUW.js} +2 -2
  56. package/dist/{chunk-M3OZL6PQ.js → chunk-IPF2S5G2.js} +4 -4
  57. package/dist/{chunk-3QKKHU7D.js → chunk-J5YZXRD5.js} +8 -8
  58. package/dist/{chunk-XOMUSIM7.js → chunk-JALKQREN.js} +1 -1
  59. package/dist/{chunk-Z2EENWM3.js → chunk-JYOMHNRP.js} +182 -20
  60. package/dist/{chunk-66DPEVLJ.js → chunk-K2HDTMRI.js} +3 -3
  61. package/dist/{chunk-O3INLME6.js → chunk-L7DQYID4.js} +4 -4
  62. package/dist/{chunk-HO5W6QC7.js → chunk-LBRG6RKI.js} +2 -1
  63. package/dist/{chunk-HPUZMBS6.js → chunk-OSF27DCY.js} +2 -2
  64. package/dist/{chunk-737TOG2F.js → chunk-PFKBQTYD.js} +1 -1
  65. package/dist/{chunk-S4VCXCRQ.js → chunk-QRBZBJ4U.js} +1 -1
  66. package/dist/{chunk-X4AHYJCE.js → chunk-SGZWOUIZ.js} +5 -5
  67. package/dist/{chunk-AE6HJZBU.js → chunk-TJZLICMB.js} +3 -3
  68. package/dist/{chunk-GM5ZOWBK.js → chunk-VGUZPC3I.js} +4 -4
  69. package/dist/{chunk-DQVJB6CZ.js → chunk-WNCOBSPN.js} +82 -8
  70. package/dist/{chunk-LTX3TJ3E.js → chunk-XC44Q5XR.js} +1 -1
  71. package/dist/{chunk-IYB3EGSD.js → chunk-YJYLFIVT.js} +1 -1
  72. package/dist/{chunk-IJKKK7F4.js → chunk-YRX5UOM3.js} +1 -1
  73. package/dist/{chunk-6FN2OQKL.js → chunk-ZRSNMEB3.js} +364 -336
  74. package/dist/{citations-GO4SPZYS.js → citations-ZTQIYYDB.js} +3 -3
  75. package/dist/{cli-catalog-6XYQY3PI.js → cli-catalog-IZJXAHFQ.js} +3 -3
  76. package/dist/{cli-commands-AMURHC27.js → cli-commands-XYM2VMAK.js} +7 -7
  77. package/dist/cli.js +13 -13
  78. package/dist/{competitor-JBTKRJ6Y.js → competitor-SIXAPHOM.js} +3 -3
  79. package/dist/{content-LKXXQ7X4.js → content-XPS3D3Y6.js} +3 -3
  80. package/dist/{dashboard-LUA7UX6B.js → dashboard-FMJDM5PA.js} +6 -6
  81. package/dist/{demo-I7JDZJUW.js → demo-QQKSHTPO.js} +1 -1
  82. package/dist/{demo-server-GNGGWERV.js → demo-server-LQ3QXUAS.js} +71 -11
  83. package/dist/{discover-TODAYYJ2.js → discover-K5HFEU2T.js} +3 -3
  84. package/dist/{discover-eval-FWQESEYB.js → discover-eval-6536Z2YN.js} +3 -3
  85. package/dist/{doctor-AXNJT2UC.js → doctor-STNH3PHH.js} +3 -3
  86. package/dist/{evidence-2XGEY4ZC.js → evidence-W2EWKBAV.js} +3 -3
  87. package/dist/{export-cmd-7RMYBIBZ.js → export-cmd-EGHMY3M6.js} +3 -3
  88. package/dist/{ga-HZDIZGCO.js → ga-DVIG3PVW.js} +3 -3
  89. package/dist/{gbp-SMRBWJEL.js → gbp-SNS4XZBO.js} +3 -3
  90. package/dist/{google-2CN345XS.js → google-KGPHZG6E.js} +4 -4
  91. package/dist/{google-marketing-W7T2VTVW.js → google-marketing-VZR6W3L6.js} +1 -1
  92. package/dist/{health-cmd-W7OWZ4ZI.js → health-cmd-2KWO2GOM.js} +3 -3
  93. package/dist/{history-UVNCBHTE.js → history-ZY27B7JE.js} +3 -3
  94. package/dist/index.d.ts +236 -161
  95. package/dist/index.js +24 -24
  96. package/dist/{init-MDGOTK2L.js → init-5TXIKII2.js} +11 -11
  97. package/dist/{insights-PQ52G5LU.js → insights-MUYEOSOG.js} +3 -3
  98. package/dist/intelligence-service-U4NSGZUH.js +15 -0
  99. package/dist/{journeys-3PP5GYVJ.js → journeys-YO54IRVC.js} +3 -3
  100. package/dist/{keys-CWZUX7IQ.js → keys-M4QZGYVJ.js} +3 -3
  101. package/dist/{keyword-5OMMULLY.js → keyword-MU62MGWT.js} +3 -3
  102. package/dist/{logs-3FTE42E3.js → logs-BAQM334Q.js} +3 -3
  103. package/dist/{mcp-FK5DTI7I.js → mcp-LYS5P3PB.js} +2 -2
  104. package/dist/mcp.js +9 -9
  105. package/dist/{measurement-plan-ICO2BQNT.js → measurement-plan-HVSTZWTW.js} +5 -5
  106. package/dist/{notify-ML4PYD3L.js → notify-XSP5JXU3.js} +3 -3
  107. package/dist/{organic-evidence-4U4H3DCR.js → organic-evidence-4HYW4PVO.js} +3 -3
  108. package/dist/{overview-NF427VFO.js → overview-XH72WTZT.js} +4 -4
  109. package/dist/{project-AAO65OF7.js → project-UKH7FTQX.js} +3 -3
  110. package/dist/{query-7ZOUQNF5.js → query-RRD5M4QV.js} +3 -3
  111. package/dist/{research-UQ7AF5N3.js → research-V2IGDUKL.js} +3 -3
  112. package/dist/{results-export-CZCLC4ZH.js → results-export-5FC7ZT6O.js} +3 -3
  113. package/dist/{run-4XJZW52R.js → run-B2A4LMWU.js} +4 -4
  114. package/dist/{schedule-D3E5FDBE.js → schedule-PLY6CHQT.js} +3 -3
  115. package/dist/{search-DYET2IL4.js → search-ACVK3WH5.js} +3 -3
  116. package/dist/{serve-R2CI3RWQ.js → serve-2LTQZPEK.js} +25 -25
  117. package/dist/{session-7CW2V5EH.js → session-IHXMPETX.js} +12 -12
  118. package/dist/{settings-ZISHACFX.js → settings-ZHBNX2I5.js} +3 -3
  119. package/dist/{skills-WU6IN3NL.js → skills-KWRTCXTF.js} +2 -2
  120. package/dist/{snapshot-KCKQDJ2W.js → snapshot-GBDCCV6L.js} +3 -3
  121. package/dist/{src-WIOU5G4V.js → src-NOTQWGYV.js} +2 -2
  122. package/dist/{status-CYZFVMYA.js → status-FGDDGBMJ.js} +4 -4
  123. package/dist/{technical-aeo-P5CS22AR.js → technical-aeo-AK4QI63Q.js} +3 -3
  124. package/dist/{telemetry-LPK5F2WU.js → telemetry-6QPWYMCR.js} +4 -4
  125. package/dist/{tools-E3B2GZ6Q.js → tools-PBNTVVN7.js} +11 -11
  126. package/dist/{traffic-5KZYXXIQ.js → traffic-AS4PIOQL.js} +5 -5
  127. package/dist/{users-2F346HIW.js → users-BNYKF35J.js} +3 -3
  128. package/dist/{visibility-stats-ITA6DNF3.js → visibility-stats-TWL3GJMD.js} +3 -3
  129. package/dist/{wordpress-ZHCFT6V3.js → wordpress-XBWXAACD.js} +3 -3
  130. package/package.json +1 -1
  131. package/assets/assets/MeasurementPropertyPage-DfPHZNGo.js +0 -4
  132. package/assets/assets/ProjectPage-D4n2oqPP.js +0 -9
  133. package/assets/assets/SiteHealthSection-CGKKUtqQ.js +0 -2
  134. package/assets/assets/ToneBadge-CJ6ObMRe.js +0 -60
  135. package/assets/assets/index-DEXKV7TW.js +0 -60
  136. package/assets/assets/react-query-BktZZtSC.js +0 -1
  137. package/dist/intelligence-service-IAUGKMDO.js +0 -15
@@ -2,13 +2,13 @@ import {
2
2
  bucketCliCommandDuration,
3
3
  isTelemetryEnabled,
4
4
  trackEvent
5
- } from "./chunk-XMKXXWMO.js";
5
+ } from "./chunk-IHOVPHUW.js";
6
6
  import {
7
7
  getConfigDir
8
- } from "./chunk-C2PYKUD3.js";
8
+ } from "./chunk-EYWC24VC.js";
9
9
  import {
10
10
  currentOutcomeAttribution
11
- } from "./chunk-IJKKK7F4.js";
11
+ } from "./chunk-YRX5UOM3.js";
12
12
  import {
13
13
  AGENT_NONE,
14
14
  OutcomeSurfaces,
@@ -19,7 +19,7 @@ import {
19
19
  integrationConnectionPropertiesSchema,
20
20
  normalizeAgentSlug,
21
21
  usageSurfaceSchema
22
- } from "./chunk-DQVJB6CZ.js";
22
+ } from "./chunk-WNCOBSPN.js";
23
23
 
24
24
  // src/outcome-telemetry.ts
25
25
  import fs from "fs";
@@ -7,27 +7,27 @@ import {
7
7
  reparseStoredResult4,
8
8
  reparseStoredResult5,
9
9
  resolveStoredApiResponse
10
- } from "./chunk-HPUZMBS6.js";
10
+ } from "./chunk-OSF27DCY.js";
11
11
  import {
12
12
  loadConfig
13
- } from "./chunk-C2PYKUD3.js";
13
+ } from "./chunk-EYWC24VC.js";
14
14
  import {
15
15
  isMachineFormat
16
16
  } from "./chunk-HGMYEDAQ.js";
17
17
  import {
18
18
  determineCitationState
19
- } from "./chunk-S4VCXCRQ.js";
19
+ } from "./chunk-QRBZBJ4U.js";
20
20
  import {
21
21
  backfillProjectAnswerMentions,
22
22
  competitorIdentityColumns,
23
23
  createRunCompetitorResolver,
24
24
  readMarketCompetitorNames,
25
25
  refreshSnapshotMentionIdentity
26
- } from "./chunk-Z2EENWM3.js";
26
+ } from "./chunk-JYOMHNRP.js";
27
27
  import {
28
28
  classifyAiUserFetch,
29
29
  classifyCrawler
30
- } from "./chunk-Q2WZAD5W.js";
30
+ } from "./chunk-BCRRAH5C.js";
31
31
  import {
32
32
  aiUserFetchEventsHourly,
33
33
  auditLog,
@@ -42,7 +42,7 @@ import {
42
42
  querySnapshots,
43
43
  rawEventSamples,
44
44
  runs
45
- } from "./chunk-XOMUSIM7.js";
45
+ } from "./chunk-JALKQREN.js";
46
46
  import {
47
47
  ProviderNames,
48
48
  RetrievalContracts,
@@ -59,7 +59,7 @@ import {
59
59
  extractRecommendedCompetitors,
60
60
  normalizeUrlPath,
61
61
  openRouterRouteModel
62
- } from "./chunk-DQVJB6CZ.js";
62
+ } from "./chunk-WNCOBSPN.js";
63
63
 
64
64
  // src/commands/backfill.ts
65
65
  import { setImmediate as yieldToEventLoop } from "timers/promises";
@@ -437,7 +437,7 @@ No DB writes performed. Re-run without --dry-run to apply.`);
437
437
  }
438
438
  }
439
439
  async function backfillInsightsCommand(project, opts) {
440
- const { IntelligenceService } = await import("./intelligence-service-IAUGKMDO.js");
440
+ const { IntelligenceService } = await import("./intelligence-service-U4NSGZUH.js");
441
441
  const config = loadConfig();
442
442
  const db = createClient(config.database);
443
443
  migrate(db);
@@ -20,7 +20,7 @@ import {
20
20
  templateLinkRatio,
21
21
  templateLinkUbiquityAvailable,
22
22
  validationError
23
- } from "./chunk-DQVJB6CZ.js";
23
+ } from "./chunk-WNCOBSPN.js";
24
24
  import {
25
25
  __export
26
26
  } from "./chunk-MLKGABMK.js";
@@ -9,14 +9,14 @@ import {
9
9
  normalizeCloudflareWorkerEvent,
10
10
  sessionizeJourneyRequests,
11
11
  verifyRequestSignature
12
- } from "./chunk-Q2WZAD5W.js";
12
+ } from "./chunk-BCRRAH5C.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-YRX5UOM3.js";
20
20
  import {
21
21
  SentimentIdempotencyConflict,
22
22
  SentimentRepository,
@@ -154,7 +154,7 @@ import {
154
154
  userSessions,
155
155
  users,
156
156
  writeNegativeReviewMaxStars
157
- } from "./chunk-XOMUSIM7.js";
157
+ } from "./chunk-JALKQREN.js";
158
158
  import {
159
159
  ADS_ACTIVATE_SCOPE,
160
160
  ADS_ACTIVATION_ABSOLUTE_MAX_ENTITIES,
@@ -338,6 +338,8 @@ import {
338
338
  SENTIMENT_ATTEMPT_PAGE_DEFAULT,
339
339
  SENTIMENT_INTERVAL_LIMITATION,
340
340
  SENTIMENT_QUERY_PAGE_DEFAULT,
341
+ SEO_MARKET_RANKING_MAX_SEARCHES,
342
+ SEO_PROJECT_MARKET_KEY,
341
343
  SEO_RUN_SCOPE,
342
344
  SEO_WRITE_SCOPE,
343
345
  SITE_AUDIT_ISSUES_DEFAULT_LIMIT,
@@ -949,6 +951,7 @@ import {
949
951
  providersFailing,
950
952
  qualifiedNameIncludesName,
951
953
  queryClassFilterSchema,
954
+ queryClassSchema,
952
955
  queryDtoSchema,
953
956
  queryGenerateRequestSchema,
954
957
  queryGenerateResponseSchema,
@@ -1042,6 +1045,8 @@ import {
1042
1045
  seoDomainIntelligenceListSchema,
1043
1046
  seoEvidenceQuerySchema,
1044
1047
  seoKeywordMetricsListSchema,
1048
+ seoMarketRankingListSchema,
1049
+ seoMarketRankingQuerySchema,
1045
1050
  seoOperatorTaskSchema,
1046
1051
  seoOverviewSchema,
1047
1052
  seoProfileInputSchema,
@@ -1055,6 +1060,7 @@ import {
1055
1060
  seoRunDetailSchema,
1056
1061
  seoRunListSchema,
1057
1062
  seoRunSchema,
1063
+ seoSearchTextForQuery,
1058
1064
  seoStatusSchema,
1059
1065
  seoSyncInputSchema,
1060
1066
  serializeRunError,
@@ -1167,7 +1173,7 @@ import {
1167
1173
  wordpressSchemaDeployResultDtoSchema,
1168
1174
  wordpressSchemaStatusResultDtoSchema,
1169
1175
  wordpressStatusDtoSchema
1170
- } from "./chunk-DQVJB6CZ.js";
1176
+ } from "./chunk-WNCOBSPN.js";
1171
1177
 
1172
1178
  // ../api-routes/src/user-session.ts
1173
1179
  import crypto2 from "crypto";
@@ -15214,7 +15220,10 @@ function runDto(job, counts2) {
15214
15220
  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
15221
  }
15216
15222
  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());
15223
+ 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), marketRankings: results.marketRankings.concat(task.results.marketRankings ?? []) }), emptySeoResults());
15224
+ }
15225
+ function compareMarketRankings(a, b) {
15226
+ return compareText(a.regionLabel ?? "", b.regionLabel ?? "") || compareText(a.marketLabel, b.marketLabel) || compareText(a.searchText, b.searchText) || compareText(a.queryText, b.queryText);
15218
15227
  }
15219
15228
  function scopedPlanTargets(plan, scope) {
15220
15229
  const market = scope.kind === SeoScopeKinds.market ? plan.reportingScopes?.find((market2) => market2.stableKey === scope.key) : void 0;
@@ -15222,9 +15231,14 @@ function scopedPlanTargets(plan, scope) {
15222
15231
  return { targets: plan.targets.filter((target) => targetKeys.includes(target.stableKey)), market, targetKeys };
15223
15232
  }
15224
15233
  function scopeCapabilities(kind, targets) {
15225
- if (kind === SeoScopeKinds.project) return [SeoCapabilities["keyword-metrics"], SeoCapabilities["domain-intelligence"], SeoCapabilities["ranked-keywords"]];
15234
+ if (kind === SeoScopeKinds.project) return [SeoCapabilities["keyword-metrics"], SeoCapabilities["domain-intelligence"], SeoCapabilities["ranked-keywords"], SeoCapabilities["market-rankings"]];
15226
15235
  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"]];
15236
+ const markets = kind === SeoScopeKinds.group || kind === SeoScopeKinds.market ? [SeoCapabilities["market-rankings"]] : [];
15237
+ return exactHosts ? [SeoCapabilities["keyword-metrics"], SeoCapabilities["ranked-keywords"], ...markets] : [SeoCapabilities["keyword-metrics"], ...markets];
15238
+ }
15239
+ function scopeReason(capabilities) {
15240
+ const markets = capabilities.includes(SeoCapabilities["market-rankings"]);
15241
+ 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
15242
  }
15229
15243
  function cancelSeoRefresh(db, projectId, id) {
15230
15244
  db.transaction((tx) => {
@@ -15252,7 +15266,7 @@ function resolveScope2(db, projectId, input) {
15252
15266
  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
15267
  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
15268
  }
15255
- if (input.scope.kind !== SeoScopeKinds.project && input.capabilities.some((capability) => capability !== SeoCapabilities["keyword-metrics"])) {
15269
+ if (input.scope.kind !== SeoScopeKinds.project && input.capabilities.includes(SeoCapabilities["ranked-keywords"])) {
15256
15270
  const matchers = targets.flatMap((target) => target.urlMatchers).filter((matcher) => matcher.kind === "host");
15257
15271
  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
15272
  }
@@ -15271,12 +15285,128 @@ function normalizeProfile(input) {
15271
15285
  const keywords = [...new Set(input.keywords)];
15272
15286
  if (input.capabilities.includes(SeoCapabilities["keyword-metrics"]) && !keywords.length) throw validationError("Keyword metrics requires at least one explicit keyword.");
15273
15287
  if (input.capabilities.some((capability) => capability !== SeoCapabilities["keyword-metrics"]) && !targets.length) throw validationError("Domain capabilities require at least one explicit Target.");
15288
+ if (input.capabilities.includes(SeoCapabilities["market-rankings"])) {
15289
+ if (input.capabilities.length !== 1) throw validationError("Keep market rankings in their own profile: they run one search for each tracked query and market.");
15290
+ if (!input.rankings) throw validationError("Market rankings need search settings: market locations, result depth, device and search text.");
15291
+ if (targets.length !== 1) throw validationError("Market rankings need exactly one Target: the site whose position they report.");
15292
+ if (keywords.length) throw validationError("Market rankings search the project's tracked queries. Leave keywords empty.");
15293
+ const keys = input.rankings.locations.map((location) => location.key);
15294
+ if (new Set(keys).size !== keys.length) throw validationError("Set each market location once.");
15295
+ } else if (input.rankings) {
15296
+ throw validationError("Search settings apply only to market rankings.");
15297
+ }
15274
15298
  return { ...input, targets, keywords };
15275
15299
  }
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: "" })));
15300
+ var NO_MARKET_RANKINGS = { searches: [] };
15301
+ function advancedMarketQueries(plan, scope) {
15302
+ const groups = new Map(plan.groups.map((group) => [group.stableKey, group]));
15303
+ const lineage = (groupKey) => {
15304
+ const keys = [];
15305
+ for (let key = groupKey; key && !keys.includes(key); key = groups.get(key)?.parentGroupKey) keys.push(key);
15306
+ return keys;
15307
+ };
15308
+ const nonBrandEdge = new Set(plan.assignments.filter((assignment) => assignment.queryClass === queryClassSchema.enum["non-brand"]).map((assignment) => `${assignment.targetKey}\0${assignment.queryId}`));
15309
+ const queryText = new Map(plan.querySnapshots.map((snapshot) => [snapshot.queryId, snapshot.queryText]));
15310
+ const queriesFor = (edges) => {
15311
+ const nodes = /* @__PURE__ */ new Map();
15312
+ for (const edge of edges) nodes.set(edge.queryId, (nodes.get(edge.queryId) ?? /* @__PURE__ */ new Set()).add(edge.executionNodeKey));
15313
+ return [...nodes].flatMap(([id, keys]) => {
15314
+ const text = queryText.get(id);
15315
+ return text ? [{ id, text, executionNodeKeys: [...keys] }] : [];
15316
+ });
15317
+ };
15318
+ const markets = plan.reportingScopes ?? [];
15319
+ if (!markets.length) {
15320
+ const { targetKeys } = scopedPlanTargets(plan, scope);
15321
+ 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}`))) }];
15322
+ }
15323
+ return markets.flatMap((market) => {
15324
+ const chain = lineage(market.groupKey);
15325
+ const inScope = scope.kind === SeoScopeKinds.project || (scope.kind === SeoScopeKinds.market ? market.stableKey === scope.key : scope.kind === SeoScopeKinds.group && chain.includes(scope.key));
15326
+ if (!inScope) return [];
15327
+ const region = chain.at(-1);
15328
+ return [{
15329
+ context: { marketKey: market.stableKey, marketLabel: market.label, regionKey: region ?? null, regionLabel: region ? groups.get(region)?.label ?? region : null },
15330
+ locationKeys: [market.stableKey, ...chain],
15331
+ queries: queriesFor(market.usageEdges.filter((edge) => nonBrandEdge.has(`${edge.targetKey}\0${edge.queryId}`)))
15332
+ }];
15333
+ });
15278
15334
  }
15279
- function planSeoTasks(opts, input) {
15335
+ function simpleMarketQueries(db, project) {
15336
+ const rows = db.select({ id: queries.id, query: queries.query }).from(queries).where(eq32(queries.projectId, project.id)).all();
15337
+ const classes = readQueryClasses(db, project, rows);
15338
+ if (rows.length > 0 && rows.every((row) => classes.get(row.id) == null)) {
15339
+ throw validationError("Canonry cannot tell this project's branded queries apart without a brand name. Add one before setting up market rankings.");
15340
+ }
15341
+ const nonBrand = rows.filter((row) => classes.get(row.id) === queryClassSchema.enum["non-brand"]).map((row) => ({ id: row.id, text: row.query, executionNodeKeys: [] }));
15342
+ const locations = project.locations.length > 0 ? project.locations.map((location) => ({ key: location.label, label: location.label })) : [{ key: SEO_PROJECT_MARKET_KEY, label: "Project" }];
15343
+ return locations.map((location) => ({ context: { marketKey: location.key, marketLabel: location.label, regionKey: null, regionLabel: null }, locationKeys: [location.key], queries: nonBrand }));
15344
+ }
15345
+ function marketRankingPlan(db, projectId, input) {
15346
+ if (!input.capabilities.includes(SeoCapabilities["market-rankings"]) || !input.rankings) return NO_MARKET_RANKINGS;
15347
+ const settings = input.rankings;
15348
+ const project = db.select().from(projects).where(eq32(projects.id, projectId)).get();
15349
+ if (!project) throw notFound("Project", projectId);
15350
+ const own = new Set(effectiveDomains({ canonicalDomain: project.canonicalDomain, ownedDomains: project.ownedDomains }).map((domain) => registrableDomain(domain)).filter(Boolean));
15351
+ const target = input.targets[0];
15352
+ if (!target || !own.has(registrableDomain(target.value))) throw validationError("The market-rankings Target must be one of this project's own domains.");
15353
+ const active = activeMeasurementPlan(db, projectId);
15354
+ const advanced = active?.plan.schemaVersion === 2 ? { versionId: active.version.id, plan: active.plan } : null;
15355
+ const markets = advanced ? advancedMarketQueries(advanced.plan, input.scope) : simpleMarketQueries(db, project);
15356
+ const rival = (domain) => {
15357
+ const registrable = registrableDomain(normalizeHostname(domain) ?? domain);
15358
+ return registrable && !own.has(registrable) ? [registrable] : [];
15359
+ };
15360
+ const projectCompetitors = db.select({ domain: competitors.domain }).from(competitors).where(eq32(competitors.projectId, projectId)).all().flatMap((row) => rival(row.domain));
15361
+ const pinnedFor = createPlanPinLookup(db);
15362
+ const competitorsFor = (query) => [.../* @__PURE__ */ new Set([...projectCompetitors, ...query.executionNodeKeys.flatMap((key) => pinnedFor(advanced?.versionId, key)).flatMap(rival)])].sort(compareText);
15363
+ const locations = new Map(settings.locations.map((location) => [location.key, location]));
15364
+ const searches = /* @__PURE__ */ new Map();
15365
+ const unmapped = [];
15366
+ for (const market of markets) {
15367
+ const mapped = market.locationKeys.map((key) => locations.get(key)).find((location2) => location2 !== void 0);
15368
+ const location = mapped ?? (market.context.marketKey === SEO_PROJECT_MARKET_KEY ? { locationCode: input.locationCode, label: `Location ${input.locationCode}` } : void 0);
15369
+ if (!location) {
15370
+ unmapped.push(market.context.marketLabel);
15371
+ continue;
15372
+ }
15373
+ for (const query of market.queries) {
15374
+ const searchText = seoSearchTextForQuery(query.text, market.context.marketKey === SEO_PROJECT_MARKET_KEY ? null : market.context.marketLabel, settings.searchText);
15375
+ if (!searchText) continue;
15376
+ const answers = {
15377
+ context: { ...market.context, queryId: query.id, queryText: query.text, queryClass: queryClassSchema.enum["non-brand"] },
15378
+ competitors: competitorsFor(query)
15379
+ };
15380
+ const key = `${location.locationCode}\0${searchText.toLowerCase()}`;
15381
+ const existing = searches.get(key);
15382
+ if (existing) existing.markets.push(answers);
15383
+ else searches.set(key, { searchText, locationCode: location.locationCode, locationLabel: location.label, markets: [answers] });
15384
+ }
15385
+ }
15386
+ 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" : ""}.`);
15387
+ if (!searches.size) throw validationError("There are no non-brand tracked queries to search in this scope.");
15388
+ 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.`);
15389
+ return { searches: [...searches.values()].sort((a, b) => compareText(a.locationLabel, b.locationLabel) || compareText(a.searchText, b.searchText)) };
15390
+ }
15391
+ function taskRequests(profile, rankings) {
15392
+ return profile.capabilities.flatMap((capability) => {
15393
+ if (capability === SeoCapabilities["market-rankings"]) {
15394
+ const settings = profile.rankings;
15395
+ return rankings.searches.map((search) => ({
15396
+ capability,
15397
+ target: profile.targets[0],
15398
+ keywords: [search.searchText],
15399
+ locationCode: search.locationCode,
15400
+ languageCode: profile.languageCode,
15401
+ limit: profile.limit,
15402
+ tag: "",
15403
+ search: { device: settings.device, depth: settings.depth, locationLabel: search.locationLabel, markets: search.markets }
15404
+ }));
15405
+ }
15406
+ 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: "" }));
15407
+ });
15408
+ }
15409
+ function planSeoTasks(opts, input, rankings = NO_MARKET_RANKINGS) {
15280
15410
  const runtime = opts.getSeoRuntime?.() ?? null;
15281
15411
  const connection = runtime?.connections.find((connection2) => connection2.id === input.connectionId);
15282
15412
  if (!runtime || !opts.onSeoRunRequested) throw missingDependency("SEO execution is not configured on this host.");
@@ -15284,7 +15414,7 @@ function planSeoTasks(opts, input) {
15284
15414
  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
15415
  let planned;
15286
15416
  try {
15287
- planned = taskRequests(input).map((request) => {
15417
+ planned = taskRequests(input, rankings).map((request) => {
15288
15418
  const estimate = connection.adapter.estimate(request);
15289
15419
  return { request, reservedMicros: estimate.maxCostMicros, catalogRevision: estimate.catalogRevision };
15290
15420
  });
@@ -15322,7 +15452,7 @@ function startScheduledSeoRefreshes(db, opts, input) {
15322
15452
  if (prior?.runId) return { profileId: profile.id, status: "replayed", runId: prior.runId };
15323
15453
  try {
15324
15454
  const scopeSnapshot = resolveScope2(db, input.projectId, profile);
15325
- const runtime = planSeoTasks(opts, profile);
15455
+ const runtime = planSeoTasks(opts, profile, marketRankingPlan(db, input.projectId, profile));
15326
15456
  const now = (/* @__PURE__ */ new Date()).toISOString();
15327
15457
  const account = runtime.connection.adapter.accountKey;
15328
15458
  const requestedId = crypto17.randomUUID();
@@ -15394,7 +15524,7 @@ async function seoRoutes(app, opts) {
15394
15524
  const available = !!runtime?.connections.length && !!opts.onSeoRunRequested;
15395
15525
  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
15526
  };
15397
- const planTasks = (input) => planSeoTasks(opts, input);
15527
+ const planTasks = (projectId, input) => planSeoTasks(opts, input, marketRankingPlan(app.db, projectId, input));
15398
15528
  app.get("/seo/operator/tasks/:id", async (request) => {
15399
15529
  requireOperator(request);
15400
15530
  const task = app.db.select().from(seoTasks).where(eq32(seoTasks.id, request.params.id)).get();
@@ -15422,7 +15552,7 @@ async function seoRoutes(app, opts) {
15422
15552
  const plan = active?.plan.schemaVersion === 2 ? active.plan : null;
15423
15553
  const scopes = plan ? planScopeOptions(plan, { marketLinks: true }).map((option) => {
15424
15554
  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." };
15555
+ 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
15556
  }) : [{ 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
15557
  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
15558
  });
@@ -15438,7 +15568,7 @@ async function seoRoutes(app, opts) {
15438
15568
  if (!parsed.success) throw validationError("Invalid SEO profile.", { issues: parsed.error.issues });
15439
15569
  const input = normalizeProfile(parsed.data);
15440
15570
  resolveScope2(app.db, project.id, input);
15441
- planTasks(input);
15571
+ planTasks(project.id, input);
15442
15572
  const now = (/* @__PURE__ */ new Date()).toISOString();
15443
15573
  app.db.transaction((tx) => {
15444
15574
  const current = tx.select().from(seoProfiles).where(and24(eq32(seoProfiles.projectId, project.id), eq32(seoProfiles.id, request.params.id))).get();
@@ -15454,7 +15584,7 @@ async function seoRoutes(app, opts) {
15454
15584
  if (!parsed.success) throw validationError("Select an SEO profile.");
15455
15585
  const saved = profile(project.id, parsed.data.profileId);
15456
15586
  const scope = resolveScope2(app.db, project.id, saved);
15457
- const planned = planTasks(saved);
15587
+ const planned = planTasks(project.id, saved);
15458
15588
  const expiresAt = new Date(Date.now() + 5 * 6e4).toISOString();
15459
15589
  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
15590
  const signature = crypto17.createHmac("sha256", intentSecret).update(payload).digest("base64url");
@@ -15491,7 +15621,7 @@ async function seoRoutes(app, opts) {
15491
15621
  const saved = profile(project.id, claims.profileId);
15492
15622
  const scopeSnapshot = resolveScope2(app.db, project.id, saved);
15493
15623
  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);
15624
+ const runtime = planTasks(project.id, saved);
15495
15625
  if (runtime.intentHash !== claims.intentHash) throw validationError("The SEO connection or host policy changed. Create a new refresh intent.");
15496
15626
  const now = (/* @__PURE__ */ new Date()).toISOString();
15497
15627
  const account = runtime.connection.adapter.accountKey;
@@ -15515,7 +15645,7 @@ async function seoRoutes(app, opts) {
15515
15645
  const row = job(resolveProject(app.db, request.params.name).id, request.params.id);
15516
15646
  const rows = tasks(row.id);
15517
15647
  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) };
15648
+ 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), 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
15649
  });
15520
15650
  app.post("/projects/:name/seo/runs/:id/cancel", { config: { paidRead: true, writeScope: SEO_RUN_SCOPE } }, async (request) => {
15521
15651
  requireScope(request, SEO_RUN_SCOPE);
@@ -15534,7 +15664,7 @@ async function seoRoutes(app, opts) {
15534
15664
  const results = resultRows(rows);
15535
15665
  const primary = row?.profile.targets[0];
15536
15666
  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 };
15667
+ 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
15668
  });
15539
15669
  for (const [path8, key] of [["keyword-metrics", "keywordMetrics"], ["domain-intelligence", "domainIntelligence"], ["ranked-keywords", "rankedKeywords"]]) app.get(`/projects/:name/seo/${path8}`, async (request) => {
15540
15670
  const query = queryInput(request.query);
@@ -15544,6 +15674,17 @@ async function seoRoutes(app, opts) {
15544
15674
  const offset = query.offset ?? 0;
15545
15675
  return { runId: row?.id ?? null, items: items.slice(offset, offset + limit), total: items.length, limit, offset };
15546
15676
  });
15677
+ app.get("/projects/:name/seo/market-rankings", async (request) => {
15678
+ const raw = request.query ?? {};
15679
+ const parsed = seoMarketRankingQuerySchema.safeParse({ ...raw, ...raw.limit === void 0 ? {} : { limit: Number(raw.limit) }, ...raw.offset === void 0 ? {} : { offset: Number(raw.offset) } });
15680
+ if (!parsed.success) throw validationError("Invalid SEO evidence selection.");
15681
+ const query = parsed.data;
15682
+ const row = selected(resolveProject(app.db, request.params.name).id, query, true);
15683
+ 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);
15684
+ const limit = query.limit ?? 50;
15685
+ const offset = query.offset ?? 0;
15686
+ return { runId: row?.id ?? null, items: items.slice(offset, offset + limit), total: items.length, limit, offset };
15687
+ });
15547
15688
  }
15548
15689
 
15549
15690
  // ../api-routes/src/runs.ts
@@ -33750,6 +33891,7 @@ var SCHEMA_TABLE = {
33750
33891
  SeoKeywordMetricsList: seoKeywordMetricsListSchema,
33751
33892
  SeoDomainIntelligenceList: seoDomainIntelligenceListSchema,
33752
33893
  SeoRankedKeywordList: seoRankedKeywordListSchema,
33894
+ SeoMarketRankingList: seoMarketRankingListSchema,
33753
33895
  SentimentSummary: sentimentSummarySchema,
33754
33896
  SentimentSettings: sentimentSettingsSchema,
33755
33897
  SentimentSettingsUpdate: sentimentSettingsUpdateSchema,
@@ -34977,6 +35119,26 @@ var routeCatalog = [
34977
35119
  409: errorResponse("Selection, intent or execution identity changed.")
34978
35120
  }
34979
35121
  },
35122
+ {
35123
+ method: "get",
35124
+ path: "/api/v1/projects/{name}/seo/market-rankings",
35125
+ summary: "Read stored market rankings",
35126
+ tags: ["seo"],
35127
+ 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. A null rank means the site was not in the checked results.",
35128
+ parameters: [
35129
+ nameParameter,
35130
+ ...seoEvidenceParameters,
35131
+ { name: "marketKey", in: "query", description: "Only rows for this market (an Advanced market key or a Simple location label).", schema: stringSchema },
35132
+ { name: "regionKey", in: "query", description: "Only rows whose market sits under this top-level group.", schema: stringSchema }
35133
+ ],
35134
+ responses: {
35135
+ 200: jsonResponse("SEO response.", "SeoMarketRankingList"),
35136
+ 400: errorResponse("Invalid scope or input."),
35137
+ 403: errorResponse("Insufficient authority."),
35138
+ 404: errorResponse("Project or selected evidence not found."),
35139
+ 409: errorResponse("Selection, intent or execution identity changed.")
35140
+ }
35141
+ },
34980
35142
  {
34981
35143
  method: "put",
34982
35144
  path: "/api/v1/projects/{name}/seo/profiles/{id}",
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  createApiClient
3
- } from "./chunk-F7NZEUXP.js";
3
+ } from "./chunk-EIMXB6UH.js";
4
4
  import {
5
5
  emitJsonl
6
6
  } from "./chunk-Z7IUAJOF.js";
7
7
  import {
8
8
  measurementDraftOperationSchema,
9
9
  runMeasurementDraftAction
10
- } from "./chunk-ZPRJCSSR.js";
10
+ } from "./chunk-CKTA5374.js";
11
11
  import {
12
12
  isMachineFormat,
13
13
  systemError
@@ -37,7 +37,7 @@ import {
37
37
  unattributedAnswersLabel,
38
38
  uncheckedSourcesLabel,
39
39
  visibilityReportRequestSchema
40
- } from "./chunk-DQVJB6CZ.js";
40
+ } from "./chunk-WNCOBSPN.js";
41
41
 
42
42
  // src/commands/measurement-plan.ts
43
43
  import crypto from "crypto";
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  registeredProviderNames
3
- } from "./chunk-IYB3EGSD.js";
3
+ } from "./chunk-YJYLFIVT.js";
4
4
  import {
5
5
  configExists,
6
6
  loadConfigRaw
7
- } from "./chunk-C2PYKUD3.js";
7
+ } from "./chunk-EYWC24VC.js";
8
8
 
9
9
  // src/setup-state.ts
10
10
  import fs from "fs";
@@ -28,7 +28,7 @@ async function buildSetupState() {
28
28
  }
29
29
  if (dbPath && fs.existsSync(dbPath)) {
30
30
  try {
31
- const { createClient, projects, queries: queriesTable } = await import("./src-WIOU5G4V.js");
31
+ const { createClient, projects, queries: queriesTable } = await import("./src-NOTQWGYV.js");
32
32
  const db = createClient(dbPath);
33
33
  project_count = db.select({ id: projects.id }).from(projects).all().length;
34
34
  const firstQuery = db.select({ id: queriesTable.id }).from(queriesTable).limit(1).all();
@@ -47,7 +47,7 @@ function queueServeHandoff() {
47
47
  async function runPendingServeHandoff(format) {
48
48
  if (!pendingServeHandoff) return;
49
49
  pendingServeHandoff = false;
50
- const { serveCommand } = await import("./serve-R2CI3RWQ.js");
50
+ const { serveCommand } = await import("./serve-2LTQZPEK.js");
51
51
  await serveCommand(format);
52
52
  }
53
53
 
@@ -5,7 +5,7 @@ import {
5
5
  AppError,
6
6
  parseWindowIn,
7
7
  resolveDateRange
8
- } from "./chunk-DQVJB6CZ.js";
8
+ } from "./chunk-WNCOBSPN.js";
9
9
 
10
10
  // src/cli-access.ts
11
11
  var CLI_ACCESS_BY_ID = Object.freeze({
@@ -331,6 +331,7 @@ var CLI_ACCESS_BY_ID = Object.freeze({
331
331
  "seo.context": "stored",
332
332
  "seo.domain-intelligence": "stored",
333
333
  "seo.keyword-metrics": "stored",
334
+ "seo.market-rankings": "stored",
334
335
  "seo.operator.reconcile": "write",
335
336
  "seo.operator.task": "stored",
336
337
  "seo.overview": "stored",
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  parseJsonColumn
3
- } from "./chunk-XOMUSIM7.js";
3
+ } from "./chunk-JALKQREN.js";
4
4
  import {
5
5
  AI_ENGINE_DOMAINS,
6
6
  AI_ENGINE_SELF_DOMAINS,
@@ -26,7 +26,7 @@ import {
26
26
  retryAfterDelayMs,
27
27
  usageCount,
28
28
  withRetry
29
- } from "./chunk-DQVJB6CZ.js";
29
+ } from "./chunk-WNCOBSPN.js";
30
30
 
31
31
  // ../provider-openai/src/normalize.ts
32
32
  import OpenAI from "openai";
@@ -28,7 +28,7 @@ import {
28
28
  unknownUsageObservation,
29
29
  usdToMicros,
30
30
  withRetry
31
- } from "./chunk-DQVJB6CZ.js";
31
+ } from "./chunk-WNCOBSPN.js";
32
32
 
33
33
  // ../provider-answer-services/src/core/http.ts
34
34
  var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  hostMatchesDomain
3
- } from "./chunk-DQVJB6CZ.js";
3
+ } from "./chunk-WNCOBSPN.js";
4
4
 
5
5
  // src/citation-utils.ts
6
6
  function pickProjectCitedDomain(citedDomains, projectDomains) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  pickProjectCitedDomain
3
- } from "./chunk-S4VCXCRQ.js";
3
+ } from "./chunk-QRBZBJ4U.js";
4
4
  import {
5
5
  GBP_INSIGHT_PROVIDER,
6
6
  PERSISTENT_GAP_THRESHOLD,
@@ -14,10 +14,10 @@ import {
14
14
  mergeGscQueryTotalsWithFallback,
15
15
  readGscQueryDailyRows,
16
16
  readLatestGscDataDate
17
- } from "./chunk-Z2EENWM3.js";
17
+ } from "./chunk-JYOMHNRP.js";
18
18
  import {
19
19
  getRequestContext
20
- } from "./chunk-IJKKK7F4.js";
20
+ } from "./chunk-YRX5UOM3.js";
21
21
  import {
22
22
  competitors,
23
23
  gbpDailyMetrics,
@@ -35,7 +35,7 @@ import {
35
35
  queries,
36
36
  querySnapshots,
37
37
  runs
38
- } from "./chunk-XOMUSIM7.js";
38
+ } from "./chunk-JALKQREN.js";
39
39
  import {
40
40
  CitationStates,
41
41
  RunKinds,
@@ -45,7 +45,7 @@ import {
45
45
  effectiveDomains,
46
46
  redactLogString,
47
47
  redactLogValue
48
- } from "./chunk-DQVJB6CZ.js";
48
+ } from "./chunk-WNCOBSPN.js";
49
49
 
50
50
  // src/intelligence-service.ts
51
51
  import { eq, desc, asc, and, ne, or, inArray, gte, lte, isNull, sql, exists } from "drizzle-orm";
@@ -3,7 +3,7 @@ import {
3
3
  resolveCliAccess,
4
4
  resolveCliAccessForInvocation,
5
5
  windowOptions
6
- } from "./chunk-HO5W6QC7.js";
6
+ } from "./chunk-LBRG6RKI.js";
7
7
  import {
8
8
  emitJsonl
9
9
  } from "./chunk-Z7IUAJOF.js";
@@ -14,7 +14,7 @@ import {
14
14
  } from "./chunk-HGMYEDAQ.js";
15
15
  import {
16
16
  OperationAccesses
17
- } from "./chunk-DQVJB6CZ.js";
17
+ } from "./chunk-WNCOBSPN.js";
18
18
 
19
19
  // src/cli-catalog.ts
20
20
  import { createRequire } from "module";
@@ -111,7 +111,7 @@ async function dispatchRegisteredCommand(args, fallbackFormat, specs) {
111
111
  const accessValue = accessFilterFromArgs(argsWithoutHelp);
112
112
  validateAccessFilter(accessValue);
113
113
  if (fallbackFormat !== "text") {
114
- const { printCliCatalog: printCliCatalog2 } = await import("./cli-catalog-6XYQY3PI.js");
114
+ const { printCliCatalog: printCliCatalog2 } = await import("./cli-catalog-IZJXAHFQ.js");
115
115
  const group2 = argsWithoutHelp.find((arg) => !arg.startsWith("--"));
116
116
  printCliCatalog2(specs, fallbackFormat, group2, accessValue);
117
117
  return true;